Skip to main content

What is the REST Transport?

The REST transport automatically creates standard HTTP endpoints from your service methods. It uses naming conventions to determine HTTP methods (GET, POST, PUT, DELETE) and paths, so you don’t need to write routing code. REST is the most familiar protocol for web APIs. It’s what you use when calling APIs with curl, fetch in JavaScript, or any HTTP client.

Quick Start

Mount your service with the REST transport:
import (
    "github.com/go-mizu/mizu"
    contract "github.com/go-mizu/mizu/contract/v2"
    "github.com/go-mizu/mizu/contract/v2/transport/rest"

    "yourapp/todo"
)

// Your interface is defined in the todo package as todo.API:
// type API interface {
//     Create(ctx context.Context, in *CreateInput) (*Todo, error)
//     List(ctx context.Context) (*ListOutput, error)
//     Get(ctx context.Context, in *GetInput) (*Todo, error)
//     Delete(ctx context.Context, in *DeleteInput) error
// }

// Create your service implementation
impl := todo.NewService()

// Register your service
svc := contract.Register[todo.API](impl,
    contract.WithDefaultResource("todos"),
)

// Create mizu app
app := mizu.New()

// Mount REST endpoints
rest.Mount(app.Router, svc)

// Start server
app.Listen(":8080")
Your service now has these endpoints:
MethodPathHandler
POST/todosCreate
GET/todosList
GET/todos/{id}Get
DELETE/todos/{id}Delete

Method Name to HTTP Mapping

Contract automatically maps method names to HTTP verbs:
Method Name Starts WithHTTP MethodPath Pattern
Create, Add, NewPOST/{resource}
List, All, Search, Find*sGET/{resource}
Get, Find, Fetch, ReadGET/{resource}/{id}
Update, Edit, Modify, SetPUT/{resource}/{id}
Delete, RemoveDELETE/{resource}/{id}
PatchPATCH/{resource}/{id}
Other namesPOST/{resource}/{action}

Examples

package product

// API defines the contract for product operations
type API interface {
    // POST /products
    Create(ctx context.Context, in *CreateInput) (*Product, error)

    // GET /products
    List(ctx context.Context) (*ListOutput, error)

    // GET /products (with query params)
    Search(ctx context.Context, in *SearchInput) (*ListOutput, error)

    // GET /products/{id}
    Get(ctx context.Context, in *GetInput) (*Product, error)

    // PUT /products/{id}
    Update(ctx context.Context, in *UpdateInput) (*Product, error)

    // DELETE /products/{id}
    Delete(ctx context.Context, in *DeleteInput) error

    // PATCH /products/{id}
    Patch(ctx context.Context, in *PatchInput) (*Product, error)

    // POST /products/archive (custom action)
    Archive(ctx context.Context, in *ArchiveInput) error

    // POST /products/publish (custom action)
    Publish(ctx context.Context, in *PublishInput) (*Product, error)
}

Mount Options

Mount

Mount all routes directly on the router:
rest.Mount(app.Router, svc)

MountAt

Mount routes under a prefix:
rest.MountAt(app.Router, "/api/v1", svc)

// Now endpoints are:
// POST /api/v1/todos
// GET /api/v1/todos
// etc.

Getting Routes

Get route definitions without mounting:
routes, err := rest.Routes(svc)
if err != nil {
    log.Fatal(err)
}

for _, route := range routes {
    fmt.Printf("%s %s -> %s.%s\n", route.Method, route.Path, route.Resource, route.Handler)
}

Making Requests

Create (POST)

curl -X POST http://localhost:8080/todos \
  -H "Content-Type: application/json" \
  -d '{"title": "Buy groceries"}'
Response:
{
  "id": "todo_1",
  "title": "Buy groceries",
  "completed": false
}

List (GET)

curl http://localhost:8080/todos
Response:
{
  "items": [
    {"id": "todo_1", "title": "Buy groceries", "completed": false},
    {"id": "todo_2", "title": "Walk the dog", "completed": true}
  ],
  "count": 2
}

Get (GET with ID)

curl http://localhost:8080/todos/todo_1
Response:
{
  "id": "todo_1",
  "title": "Buy groceries",
  "completed": false
}

Update (PUT)

curl -X PUT http://localhost:8080/todos/todo_1 \
  -H "Content-Type: application/json" \
  -d '{"id": "todo_1", "title": "Buy groceries", "completed": true}'
Response:
{
  "id": "todo_1",
  "title": "Buy groceries",
  "completed": true
}

Delete (DELETE)

curl -X DELETE http://localhost:8080/todos/todo_1
Response: HTTP 204 No Content (empty body)

Path Parameters

For methods that need an ID from the URL path, use the path tag:
type GetInput struct {
    ID string `json:"id" path:"id"`  // Extracted from {id} in the path
}

type UpdateInput struct {
    ID        string `json:"id" path:"id"`
    Title     string `json:"title"`
    Completed bool   `json:"completed"`
}

Multiple Path Parameters

For nested resources:
// Path: /users/{userId}/posts/{id}
type GetPostInput struct {
    UserID string `json:"userId" path:"userId"`
    PostID string `json:"postId" path:"id"`
}
Register with custom HTTP binding:
svc := contract.Register[PostAPI](impl,
    contract.WithMethodHTTP("GetPost", "GET", "/users/{userId}/posts/{id}"),
)

Query Parameters

By default, input comes from the request body. For GET requests, you may want query parameters:
type ListInput struct {
    Limit  int    `json:"limit" query:"limit"`
    Offset int    `json:"offset" query:"offset"`
    Status string `json:"status" query:"status"`
}
Request:
curl "http://localhost:8080/todos?limit=10&offset=0&status=completed"

Custom HTTP Bindings

Override the automatic HTTP mapping with WithMethodHTTP:
svc := contract.Register[TodoAPI](impl,
    contract.WithDefaultResource("todos"),

    // Custom path for archive action
    contract.WithMethodHTTP("Archive", "POST", "/todos/{id}/archive"),

    // API versioning
    contract.WithMethodHTTP("Create", "POST", "/v2/todos"),
)
Or set multiple bindings at once:
svc := contract.Register[TodoAPI](impl,
    contract.WithHTTP(map[string]contract.HTTPBinding{
        "Create": {Method: "POST", Path: "/v2/todos"},
        "Get":    {Method: "GET", Path: "/v2/todos/{id}"},
        "List":   {Method: "GET", Path: "/v2/todos"},
    }),
)

Complete Example

This example shows a complete REST service using the recommended package-based organization:
// todo/types.go
package todo

type Todo struct {
    ID        string `json:"id"`
    Title     string `json:"title"`
    Completed bool   `json:"completed"`
}

type CreateInput struct {
    Title string `json:"title"`
}

type GetInput struct {
    ID string `json:"id" path:"id"`
}

type UpdateInput struct {
    ID        string `json:"id" path:"id"`
    Title     string `json:"title"`
    Completed bool   `json:"completed"`
}

type DeleteInput struct {
    ID string `json:"id" path:"id"`
}

type ListOutput struct {
    Items []*Todo `json:"items"`
    Count int     `json:"count"`
}
// todo/api.go
package todo

import "context"

// API defines the contract for todo operations
type API interface {
    Create(ctx context.Context, in *CreateInput) (*Todo, error)
    List(ctx context.Context) (*ListOutput, error)
    Get(ctx context.Context, in *GetInput) (*Todo, error)
    Update(ctx context.Context, in *UpdateInput) (*Todo, error)
    Delete(ctx context.Context, in *DeleteInput) error
}
// todo/service.go
package todo

import (
    "context"
    "fmt"
    "sync"

    contract "github.com/go-mizu/mizu/contract/v2"
)

// Service implements todo.API
type Service struct {
    mu     sync.RWMutex
    todos  map[string]*Todo
    nextID int
}

// Compile-time check
var _ API = (*Service)(nil)

func NewService() *Service {
    return &Service{todos: make(map[string]*Todo)}
}

func (s *Service) Create(ctx context.Context, in *CreateInput) (*Todo, error) {
    if in.Title == "" {
        return nil, contract.ErrInvalidArgument("title is required")
    }

    s.mu.Lock()
    defer s.mu.Unlock()

    s.nextID++
    todo := &Todo{
        ID:    fmt.Sprintf("todo_%d", s.nextID),
        Title: in.Title,
    }
    s.todos[todo.ID] = todo
    return todo, nil
}

func (s *Service) List(ctx context.Context) (*ListOutput, error) {
    s.mu.RLock()
    defer s.mu.RUnlock()

    items := make([]*Todo, 0, len(s.todos))
    for _, t := range s.todos {
        items = append(items, t)
    }
    return &ListOutput{Items: items, Count: len(items)}, nil
}

func (s *Service) Get(ctx context.Context, in *GetInput) (*Todo, error) {
    s.mu.RLock()
    defer s.mu.RUnlock()

    todo, ok := s.todos[in.ID]
    if !ok {
        return nil, contract.ErrNotFound("todo not found")
    }
    return todo, nil
}

func (s *Service) Update(ctx context.Context, in *UpdateInput) (*Todo, error) {
    s.mu.Lock()
    defer s.mu.Unlock()

    todo, ok := s.todos[in.ID]
    if !ok {
        return nil, contract.ErrNotFound("todo not found")
    }
    todo.Title = in.Title
    todo.Completed = in.Completed
    return todo, nil
}

func (s *Service) Delete(ctx context.Context, in *DeleteInput) error {
    s.mu.Lock()
    defer s.mu.Unlock()

    if _, ok := s.todos[in.ID]; !ok {
        return contract.ErrNotFound("todo not found")
    }
    delete(s.todos, in.ID)
    return nil
}
// main.go
package main

import (
    "fmt"

    "github.com/go-mizu/mizu"
    contract "github.com/go-mizu/mizu/contract/v2"
    "github.com/go-mizu/mizu/contract/v2/transport/rest"

    "yourapp/todo"
)

func main() {
    impl := todo.NewService()

    svc := contract.Register[todo.API](impl,
        contract.WithName("Todo"),
        contract.WithDefaultResource("todos"),
    )

    app := mizu.New()

    // Mount REST
    rest.Mount(app.Router, svc)

    // Print routes
    fmt.Println("REST Endpoints:")
    fmt.Println("  POST   /todos      - Create a todo")
    fmt.Println("  GET    /todos      - List all todos")
    fmt.Println("  GET    /todos/{id} - Get a todo")
    fmt.Println("  PUT    /todos/{id} - Update a todo")
    fmt.Println("  DELETE /todos/{id} - Delete a todo")

    app.Listen(":8080")
}

Error Responses

Use Contract errors for proper HTTP status codes:
// HTTP 404
return nil, contract.ErrNotFound("todo not found")

// HTTP 400
return nil, contract.ErrInvalidArgument("title is required")

// HTTP 401
return nil, contract.ErrUnauthenticated("please log in")

// HTTP 403
return nil, contract.ErrPermissionDenied("admin access required")

// HTTP 500
return nil, contract.ErrInternal("database error")
Error response format:
{
  "error": {
    "code": "NOT_FOUND",
    "message": "todo not found"
  }
}

Multiple Services

Mount multiple services on the same router:
todoSvc := contract.Register[TodoAPI](todoImpl, contract.WithDefaultResource("todos"))
userSvc := contract.Register[UserAPI](userImpl, contract.WithDefaultResource("users"))

app := mizu.New()

rest.Mount(app.Router, todoSvc)  // /todos/*
rest.Mount(app.Router, userSvc)  // /users/*

app.Listen(":8080")

Using with Middleware

Apply middleware to your routes:
app := mizu.New()

// Global middleware
app.Use(loggingMiddleware)
app.Use(corsMiddleware)

// Auth middleware for specific routes
app.Use(authMiddleware, "/todos/*")

// Mount REST
rest.Mount(app.Router, svc)

app.Listen(":8080")

OpenAPI Documentation

Generate OpenAPI spec from your service:
spec, err := rest.OpenAPI(svc.Descriptor())
if err != nil {
    log.Fatal(err)
}

// Serve the spec
app.Get("/openapi.json", func(c *mizu.Ctx) error {
    return c.JSON(200, spec)
})
See the OpenAPI documentation for more details.

Common Questions

How do I change the path for a method?

Use WithMethodHTTP:
contract.WithMethodHTTP("Archive", "POST", "/todos/{id}/archive")

How do I add query parameters?

Use the query tag on input struct fields:
type ListInput struct {
    Page int `json:"page" query:"page"`
}

Can I use different content types?

By default, REST uses application/json. For other content types, use standard mizu handlers instead of Contract.

How do I handle file uploads?

File uploads aren’t currently supported through Contract. Use standard mizu file handling for upload endpoints.

How do I add authentication?

Use mizu middleware to add authentication before requests reach your service methods. See Mizu Middleware documentation.

What’s Next?