Skip to main content
When you build an API with Contract, you write your business logic once. But different clients need to talk to your API in different ways. That’s what transports are for - they let the same code speak multiple languages.

What Is a Transport?

Think of a transport like a translator at the United Nations. The speaker (your service) gives the same message, but each translator converts it into a language their audience understands. In Contract terms:
  • Your service is the speaker - it has the business logic
  • Transports are the translators - they convert requests/responses
  • Clients are the audience - browsers, apps, AI assistants, etc.
┌──────────────────────────────────────────────────────────────────┐
│                        Your Service                               │
│                   (Written once, in Go)                           │
└──────────────────────────────────────────────────────────────────┘

        ┌─────────────────────┼─────────────────────┐
        ▼                     ▼                     ▼
┌──────────────┐      ┌──────────────┐      ┌──────────────┐
│     REST     │      │   JSON-RPC   │      │     MCP      │
│  (Familiar)  │      │  (Powerful)  │      │ (AI-Ready)   │
└──────────────┘      └──────────────┘      └──────────────┘
        │                     │                     │
        ▼                     ▼                     ▼
   Web Browsers          Backend Services        AI Assistants
   Mobile Apps           Microservices           Claude Desktop

Available Transports

Contract supports three transports plus documentation generation:
TransportWhat It IsBest For
RESTTraditional HTTP APIWeb apps, mobile apps, curl
JSON-RPCRemote procedure calls over HTTPBatch operations, microservices
MCPModel Context ProtocolAI assistants like Claude
OpenAPIAPI documentation standardDocs, code generation

Decision Guide: Which Transport Should I Use?

Use this flowchart to pick the right transport for your use case:
Start Here


Who is calling your API?

    ├─► Web browsers, mobile apps, or curl
    │   → Use REST (most familiar)

    ├─► Other backend services
    │   → Use JSON-RPC (supports batching)

    ├─► AI assistants (Claude, etc.)
    │   → Use MCP (designed for AI)

    └─► Need API documentation
        → Add OpenAPI (generates docs)
Pro tip: You can (and should!) use multiple transports at once. They don’t conflict.

Detailed Comparison

Feature Comparison

FeatureRESTJSON-RPCMCP
HTTP MethodsGET/POST/PUT/DELETEPOST onlyPOST only
Batch RequestsNoYesNo
CacheableYes (GET requests)NoNo
Self-DocumentingVia OpenAPINoVia tools/list
Error FormatHTTP status codesJSON-RPC errorsisError flag

REST - The Classic Choice

REST is the most widely known API style. It uses HTTP methods to represent actions:
# Create a todo (POST = create new resource)
curl -X POST http://localhost:8080/todos \
  -H "Content-Type: application/json" \
  -d '{"title": "Buy milk"}'

# List all todos (GET = retrieve data)
curl http://localhost:8080/todos

# Get one todo (GET with ID)
curl http://localhost:8080/todos/123

# Update a todo (PUT = replace resource)
curl -X PUT http://localhost:8080/todos/123 \
  -d '{"title": "Buy milk", "completed": true}'

# Delete a todo (DELETE = remove resource)
curl -X DELETE http://localhost:8080/todos/123
When to use REST:
  • Your clients are web browsers or mobile apps
  • You want caching (browsers cache GET requests)
  • Your team is familiar with REST APIs
  • You’re building a public API
Pros: Familiar, cacheable, works everywhere Cons: No batching, multiple round trips for complex operations

JSON-RPC - The Power User’s Choice

JSON-RPC is a simple protocol where you explicitly name the method to call. All requests use POST.
# Call a method
curl -X POST http://localhost:8080/rpc \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "todos.create",
    "params": {"title": "Buy milk"}
  }'

# Response
{"jsonrpc": "2.0", "id": 1, "result": {"id": "1", "title": "Buy milk"}}
The killer feature is batching - send multiple operations in one request:
curl -X POST http://localhost:8080/rpc \
  -H "Content-Type: application/json" \
  -d '[
    {"jsonrpc": "2.0", "id": 1, "method": "todos.create", "params": {"title": "First"}},
    {"jsonrpc": "2.0", "id": 2, "method": "todos.create", "params": {"title": "Second"}},
    {"jsonrpc": "2.0", "id": 3, "method": "todos.list"}
  ]'

# All three operations execute, returns array of results
When to use JSON-RPC:
  • Service-to-service communication
  • You need to batch multiple operations
  • Network latency is a concern
  • Your operations don’t map cleanly to REST verbs
Pros: Batching, explicit method names, standard protocol Cons: POST only, less familiar to web developers

MCP - The AI-Native Choice

MCP (Model Context Protocol) is designed for AI assistants. It lets AI models discover and use your API as “tools”.
# Step 1: Initialize the connection
curl -X POST http://localhost:8080/mcp \
  -d '{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2025-06-18"}}'

# Step 2: Discover available tools
curl -X POST http://localhost:8080/mcp \
  -d '{"jsonrpc": "2.0", "id": 2, "method": "tools/list"}'

# Response shows what the AI can do:
{
  "result": {
    "tools": [
      {
        "name": "todos.create",
        "description": "Create a new todo",
        "inputSchema": {...}
      }
    ]
  }
}

# Step 3: Call a tool
curl -X POST http://localhost:8080/mcp \
  -d '{
    "jsonrpc": "2.0",
    "id": 3,
    "method": "tools/call",
    "params": {
      "name": "todos.create",
      "arguments": {"title": "Buy milk"}
    }
  }'
When to use MCP:
  • You want Claude or other AI assistants to use your API
  • You’re building AI-powered applications
  • You want automatic tool discovery
Pros: AI assistants understand it natively, self-documenting Cons: Specialized use case, more complex protocol

OpenAPI - The Documentation Choice

OpenAPI isn’t really a transport - it generates documentation from your service. Use it alongside other transports.
# Get the OpenAPI specification
curl http://localhost:8080/openapi.json
The spec can be used with:
  • Swagger UI: Interactive API documentation
  • Code generators: Generate client SDKs in any language
  • API testing tools: Import into Postman, Insomnia, etc.

Using All Transports Together

Here’s how to serve your API via all protocols at once:
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/jsonrpc"
    "github.com/go-mizu/mizu/contract/v2/transport/mcp"
    "github.com/go-mizu/mizu/contract/v2/transport/rest"

    "yourapp/todo"
)

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

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

    app := mizu.New()

    // REST: Traditional HTTP API
    // Endpoints: POST/GET /todos, GET/PUT/DELETE /todos/{id}
    rest.Mount(app.Router, svc)

    // JSON-RPC: RPC-style with batching
    // Endpoint: POST /rpc
    jsonrpc.Mount(app.Router, "/rpc", svc)

    // MCP: AI assistant integration
    // Endpoint: POST /mcp
    mcp.Mount(app.Router, "/mcp", svc)

    // OpenAPI: Serve API documentation
    spec, _ := rest.OpenAPI(svc.Descriptor())
    app.Get("/openapi.json", func(c *mizu.Ctx) error {
        return c.JSON(200, spec)
    })

    fmt.Println("Server running on http://localhost:8080")
    app.Listen(":8080")
}
Now the same service is accessible via:
  • http://localhost:8080/todos (REST)
  • http://localhost:8080/rpc (JSON-RPC)
  • http://localhost:8080/mcp (MCP)
  • http://localhost:8080/openapi.json (OpenAPI spec)

How Errors Work Across Transports

One of Contract’s best features is consistent error handling. When you return an error:
return nil, contract.ErrNotFound("todo not found")
Each transport formats it appropriately:
TransportError Response
RESTHTTP 404 status, body: {"error": {"code": "NOT_FOUND", "message": "todo not found"}}
JSON-RPC{"error": {"code": -32603, "message": "todo not found", "data": {"code": "NOT_FOUND"}}}
MCP{"content": [{"type": "text", "text": "todo not found"}], "isError": true}
You don’t need to handle each protocol separately.

Complete Example

Here’s a complete example with all transports. We organize the code with the todo service in its own package:
// 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 ListOutput struct {
    Items []*Todo `json:"items"`
}
// 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)
}
// 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("%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}, 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
}
// 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/jsonrpc"
    "github.com/go-mizu/mizu/contract/v2/transport/mcp"
    "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 all transports
    rest.Mount(app.Router, svc)
    jsonrpc.Mount(app.Router, "/rpc", svc)
    mcp.Mount(app.Router, "/mcp", svc,
        mcp.WithInstructions("Use these tools to manage a todo list."),
    )

    // Serve OpenAPI spec
    spec, _ := rest.OpenAPI(svc.Descriptor())
    app.Get("/openapi.json", func(c *mizu.Ctx) error {
        return c.JSON(200, spec)
    })

    fmt.Println("Server running on http://localhost:8080")
    fmt.Println()
    fmt.Println("Endpoints:")
    fmt.Println("  REST:     POST/GET /todos, GET /todos/{id}")
    fmt.Println("  JSON-RPC: POST /rpc")
    fmt.Println("  MCP:      POST /mcp")
    fmt.Println("  OpenAPI:  GET /openapi.json")

    app.Listen(":8080")
}

Practical Recommendations

Starting a New Project

Start with REST + OpenAPI:
rest.Mount(app.Router, svc)

spec, _ := rest.OpenAPI(svc.Descriptor())
app.Get("/openapi.json", func(c *mizu.Ctx) error {
    return c.JSON(200, spec)
})
Add more transports as needed.

Building a Web Application

REST is perfect for web applications:
rest.Mount(app.Router, svc)

Building Microservices

JSON-RPC for service-to-service (batching is valuable):
jsonrpc.Mount(app.Router, "/rpc", svc)

Building AI-Powered Apps

MCP for AI assistants, plus REST for human debugging:
mcp.Mount(app.Router, "/mcp", svc)
rest.Mount(app.Router, svc)  // For testing with curl

Building for Everything

Use all of them! There’s no conflict:
rest.Mount(app.Router, svc)
jsonrpc.Mount(app.Router, "/rpc", svc)
mcp.Mount(app.Router, "/mcp", svc)

spec, _ := rest.OpenAPI(svc.Descriptor())
app.Get("/openapi.json", func(c *mizu.Ctx) error {
    return c.JSON(200, spec)
})

Common Questions

Can I use multiple transports at once?

Yes! Each transport uses different paths, so they don’t conflict. This is actually recommended.

Which transport is fastest?

They’re all similar in performance. The overhead is minimal compared to your actual business logic.

Do I need to write different code for each transport?

No! That’s the whole point of Contract. Write your service once, expose it via any transport.

Can I customize how a transport works?

Yes, each transport has options for customization. See the individual transport pages for details.

How are method names formatted for each transport?

All transports use the resource.method pattern:
Interface MethodTransport Method Name
Createtodos.create
Listtodos.list
Gettodos.get
For REST, method names are mapped to HTTP verbs and paths automatically.

What’s Next?

Each transport has its own detailed documentation: