ZAP Protocol

Gateway

HTTP/REST gateway for exposing ZAP services to web clients

Gateway

The ZAP Gateway provides HTTP/REST access to ZAP services, enabling browser clients and legacy systems to interact with your services.

Overview

┌─────────────────────────────────────────────────────┐
│                   HTTP Clients                      │
│            (Browsers, REST clients)                 │
└───────────────────────┬─────────────────────────────┘
                        │ HTTP/JSON

┌─────────────────────────────────────────────────────┐
│                   ZAP Gateway                       │
│  ┌─────────────┐ ┌─────────────┐ ┌───────────────┐  │
│  │ REST Router │ │ WebSocket   │ │ SSE Handler   │  │
│  └─────────────┘ └─────────────┘ └───────────────┘  │
│  ┌─────────────────────────────────────────────────┐│
│  │           JSON <-> Cap'n Proto Transcoder       ││
│  └─────────────────────────────────────────────────┘│
└───────────────────────┬─────────────────────────────┘
                        │ ZAP Protocol

┌─────────────────────────────────────────────────────┐
│                   ZAP Services                      │
└─────────────────────────────────────────────────────┘

Quick Start

Enable Gateway

server := zap.NewServer(
    zap.WithAddress(":9000"),
)

// Add HTTP gateway on port 8080
gateway := zap.NewGateway(server,
    zap.WithGatewayAddress(":8080"),
)

// Start both
go server.ListenAndServe()
gateway.ListenAndServe()

Automatic Endpoint Mapping

ZAP automatically maps RPC methods to HTTP endpoints:

RPC MethodHTTP MethodPath
Service.MethodPOST/Service/Method
Calculator.AddPOST/Calculator/Add
Users.GetUserPOST/Users/GetUser

Making Requests

# Call Calculator.Add
curl -X POST http://localhost:8080/Calculator/Add \
  -H "Content-Type: application/json" \
  -d '{"a": 10, "b": 5}'

# Response
{"result": 15}

Custom Routing

HTTP Annotations

Define custom routes in your schema:

interface Calculator
add (a Float64, b Float64) -> (result Float64)
  $http(method = "GET", path = "/calc/add")

subtract (a Float64, b Float64) -> (result Float64)
  $http(method = "GET", path = "/calc/subtract")

interface Users
getUser (id Text) -> (user User)
  $http(method = "GET", path = "/users/{id}")

createUser (user User) -> (id Text)
  $http(method = "POST", path = "/users")

deleteUser (id Text) -> ()
  $http(method = "DELETE", path = "/users/{id}")

Programmatic Routes

gateway := zap.NewGateway(server,
    zap.WithRoute("GET", "/health", healthHandler),
    zap.WithRoute("GET", "/api/v1/calc/add", addHandler),
)

Path Parameters

Extract parameters from the URL path:

interface Users
getUser (id Text) -> (user User)
  $http(method = "GET", path = "/users/{id}")

getPost (userId Text, postId Text) -> (post Post)
  $http(method = "GET", path = "/users/{userId}/posts/{postId}")

Query Parameters

Map query parameters to method arguments:

interface Search
search (query Text, limit UInt32, offset UInt32) -> (results List(Result))
  $http(
    method = "GET",
    path = "/search",
    query = ["query", "limit", "offset"]
  )

Usage:

curl "http://localhost:8080/search?query=hello&limit=10&offset=0"

JSON Transcoding

Automatic Conversion

ZAP automatically converts between JSON and Cap'n Proto:

Cap'n Proto TypeJSON Type
Boolboolean
Int8-Int64number
UInt8-UInt64number
Float32, Float64number
Textstring
Database64 string
Listarray
Structobject
Enumstring

Example Conversions

struct User
id    UInt64
name  Text
email Text
roles List(Text)

JSON representation:

{
  "id": 12345,
  "name": "Alice",
  "email": "alice@example.com",
  "roles": ["admin", "user"]
}

Custom Field Names

Use annotations for JSON field names:

struct User
id        UInt64 $json(name = "user_id")
firstName Text   $json(name = "first_name")
lastName  Text   $json(name = "last_name")

Streaming

Server-Sent Events (SSE)

For streaming responses, use SSE:

interface Feed
subscribe (topic Text) -> stream (event Event)
  $http(method = "GET", path = "/feed/{topic}", stream = "sse")

Client usage:

const eventSource = new EventSource('/feed/updates');
eventSource.onmessage = (event) => {
  const data = JSON.parse(event.data);
  console.log('Received:', data);
};

WebSocket

For bidirectional streaming:

interface Chat
connect stream (message Message) -> stream (message Message)
  $http(path = "/chat", stream = "websocket")

Client usage:

const ws = new WebSocket('ws://localhost:8080/chat');

ws.onmessage = (event) => {
  const message = JSON.parse(event.data);
  console.log('Received:', message);
};

ws.send(JSON.stringify({ text: 'Hello!' }));

Authentication

Bearer Token

gateway := zap.NewGateway(server,
    zap.WithAuth(zap.BearerAuth{
        Validator: func(token string) (context.Context, error) {
            claims, err := validateJWT(token)
            if err != nil {
                return nil, err
            }
            return context.WithValue(ctx, "user", claims.UserID), nil
        },
    }),
)

API Key

gateway := zap.NewGateway(server,
    zap.WithAuth(zap.APIKeyAuth{
        Header: "X-API-Key",
        Validator: func(key string) (context.Context, error) {
            if !isValidAPIKey(key) {
                return nil, zap.ErrUnauthorized
            }
            return ctx, nil
        },
    }),
)

Custom Authentication

gateway := zap.NewGateway(server,
    zap.WithAuthMiddleware(func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            // Custom auth logic
            if !authorized(r) {
                http.Error(w, "Unauthorized", 401)
                return
            }
            next.ServeHTTP(w, r)
        })
    }),
)

CORS Configuration

gateway := zap.NewGateway(server,
    zap.WithCORS(zap.CORSConfig{
        AllowedOrigins: []string{
            "https://app.example.com",
            "https://dashboard.example.com",
        },
        AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"},
        AllowedHeaders: []string{"Authorization", "Content-Type"},
        MaxAge:         86400,
    }),
)

Rate Limiting

gateway := zap.NewGateway(server,
    zap.WithRateLimit(zap.RateLimitConfig{
        RequestsPerSecond: 100,
        Burst:             200,
        KeyFunc: func(r *http.Request) string {
            return r.Header.Get("X-API-Key")
        },
    }),
)

Error Handling

Standard Error Format

ZAP Gateway returns errors in a consistent format:

{
  "error": {
    "code": "INVALID_ARGUMENT",
    "message": "Parameter 'id' is required",
    "details": {
      "field": "id",
      "constraint": "required"
    }
  }
}

HTTP Status Codes

ZAP ErrorHTTP Status
NotFound404
InvalidArgument400
PermissionDenied403
Unauthenticated401
ResourceExhausted429
Internal500
Unavailable503

Custom Error Mapping

gateway := zap.NewGateway(server,
    zap.WithErrorMapper(func(err error) (int, interface{}) {
        if errors.Is(err, ErrCustomError) {
            return 422, map[string]string{
                "error": "custom_error",
                "message": err.Error(),
            }
        }
        return 0, nil // Use default mapping
    }),
)

Middleware

Logging

gateway := zap.NewGateway(server,
    zap.WithMiddleware(zap.LoggingMiddleware(logger)),
)

Metrics

gateway := zap.NewGateway(server,
    zap.WithMiddleware(zap.MetricsMiddleware(zap.MetricsConfig{
        Namespace: "myapp",
        Subsystem: "gateway",
    })),
)

Custom Middleware

gateway := zap.NewGateway(server,
    zap.WithMiddleware(func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            start := time.Now()
            next.ServeHTTP(w, r)
            log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
        })
    }),
)

OpenAPI Generation

Generate OpenAPI spec from your schema:

zap openapi service.capnp --out=openapi.yaml

Serve the spec:

gateway := zap.NewGateway(server,
    zap.WithOpenAPI("/openapi.yaml"),
    zap.WithSwaggerUI("/docs"),
)

Next Steps

On this page