Documentation Index
Fetch the complete documentation index at: https://docs.go-mizu.dev/llms.txt
Use this file to discover all available pages before exploring further.
What is Registration?
Registration is the process of telling Contract about your service. Think of it like introducing your service to Contract: βHereβs my interface (what I can do) and my implementation (how I do it). Please make me available to clients.β
When you register a service, Contract does several things behind the scenes:
- Inspects your interface - Uses Goβs reflection to discover all methods in your interface
- Extracts type information - Finds all input and output types for each method
- Generates schemas - Creates JSON schemas from your Go types (used for validation and documentation)
- Creates invokers - Builds efficient callable wrappers for your methods
- Prepares HTTP bindings - Determines HTTP methods and paths for each operation
After registration, you have a *RegisteredService that can be mounted on any transport (REST, JSON-RPC, MCP, etc.).
Basic Registration
The simplest registration takes your implementation and interface:
package main
import (
"context"
contract "github.com/go-mizu/mizu/contract/v2"
"yourapp/todo" // Your todo package
)
func main() {
// Create your service implementation
impl := todo.NewService()
// Register - the generic parameter [todo.API] specifies which interface to use
// Contract analyzes todo.API to discover all methods and their types
svc := contract.Register[todo.API](impl)
// svc is now a *RegisteredService ready to be mounted on transports
}
The generic parameter [todo.API] is crucial - it tells Contract which interface defines your API. Your implementation must satisfy this interface, which Goβs compiler verifies at compile time.
Why the Generic Parameter?
You might wonder why we explicitly specify the interface instead of just passing the implementation. There are good reasons:
package todo
// The interface defines what's exposed as API
type API interface {
Create(ctx context.Context, in *CreateInput) (*Todo, error)
List(ctx context.Context) (*ListOutput, error)
}
// The implementation may have additional methods
type Service struct {
db *sql.DB
}
// These methods ARE part of the API (defined in the interface)
func (s *Service) Create(ctx context.Context, in *CreateInput) (*Todo, error) { ... }
func (s *Service) List(ctx context.Context) (*ListOutput, error) { ... }
// These methods are NOT part of the API (not in the interface)
// They won't be exposed as endpoints - they're internal helpers
func (s *Service) validateTitle(title string) error { ... }
func (s *Service) generateID() string { ... }
func (s *Service) logOperation(ctx context.Context, op string) { ... }
// In main.go
svc := contract.Register[todo.API](impl)
// Only Create and List become API endpoints
// validateTitle, generateID, logOperation remain internal
This separation between βwhatβs publicβ (interface) and βwhatβs internalβ (implementation) is a fundamental benefit of the interface-first approach.
Registration Options
Options let you customize how your service is registered. Pass them as additional arguments after the implementation:
svc := contract.Register[todo.API](impl,
contract.WithName("Todo"),
contract.WithDescription("Todo management API for task tracking"),
contract.WithDefaultResource("todos"),
)
Letβs explore each option in detail.
WithName
Sets the service name. This name appears in documentation and is used for method namespacing:
svc := contract.Register[todo.API](impl,
contract.WithName("Todo"), // Service name is "Todo"
)
Default: If not specified, Contract uses the interface name (e.g., βAPIβ from todo.API).
Where the name appears:
- OpenAPI specification: The
info.title field
- JSON-RPC: Method prefix (e.g.,
Todo.Create or todos.create depending on resource)
- MCP: Tool group name shown to AI assistants
Example:
// Without WithName - uses interface name "API"
svc := contract.Register[todo.API](impl)
// JSON-RPC method: API.create
// With WithName - uses custom name
svc := contract.Register[todo.API](impl,
contract.WithName("TodoService"),
)
// JSON-RPC method: TodoService.create
WithDescription
Adds a human-readable description to your service. This helps users understand what your API does:
svc := contract.Register[todo.API](impl,
contract.WithDescription("A todo list API for managing tasks. Supports creating, listing, updating, and deleting todo items."),
)
Where the description appears:
- OpenAPI specification: The
info.description field
- MCP: Server description shown to AI assistants before they use your tools
- Generated documentation: Any auto-generated API docs
Tip: Write descriptions that help both humans and AI understand your API. Be specific about what operations are available and what the service is for.
WithDefaultResource
Groups all methods under a resource name. This is one of the most important options for REST APIs:
svc := contract.Register[todo.API](impl,
contract.WithDefaultResource("todos"),
)
What it does:
Without a resource, your methods would be at the root path (rarely what you want):
Create -> POST /create
List -> GET /list
Get -> GET /get/{id}
With WithDefaultResource("todos"), methods get proper RESTful paths:
Create -> POST /todos
List -> GET /todos
Get -> GET /todos/{id}
Delete -> DELETE /todos/{id}
It also affects JSON-RPC method names:
Without resource: create, list, get, delete
With resource: todos.create, todos.list, todos.get, todos.delete
Why βtodosβ (plural)?: REST convention uses plural nouns for collections. /todos represents the collection of all todos, /todos/{id} represents a single todo.
WithResource
Groups specific methods under a resource name. Use this when one interface manages multiple resources:
package api
// API manages both users and orders in one interface
type API interface {
CreateUser(ctx context.Context, in *CreateUserInput) (*User, error)
GetUser(ctx context.Context, in *GetUserInput) (*User, error)
ListUsers(ctx context.Context) (*ListUsersOutput, error)
CreateOrder(ctx context.Context, in *CreateOrderInput) (*Order, error)
GetOrder(ctx context.Context, in *GetOrderInput) (*Order, error)
ListOrders(ctx context.Context) (*ListOrdersOutput, error)
}
svc := contract.Register[api.API](impl,
// Group user methods under "users" resource
contract.WithResource("users", "CreateUser", "GetUser", "ListUsers"),
// Group order methods under "orders" resource
contract.WithResource("orders", "CreateOrder", "GetOrder", "ListOrders"),
)
// Result:
// POST /users -> CreateUser
// GET /users/{id} -> GetUser
// GET /users -> ListUsers
// POST /orders -> CreateOrder
// GET /orders/{id} -> GetOrder
// GET /orders -> ListOrders
Tip: For cleaner organization, consider using separate packages and interfaces for each resource:
// Cleaner: separate packages
todoSvc := contract.Register[todo.API](todoImpl, contract.WithDefaultResource("todos"))
userSvc := contract.Register[user.API](userImpl, contract.WithDefaultResource("users"))
rest.Mount(app.Router, todoSvc)
rest.Mount(app.Router, userSvc)
WithMethodHTTP
Override the HTTP binding for a specific method. Use this when automatic inference doesnβt match your needs:
svc := contract.Register[todo.API](impl,
contract.WithDefaultResource("todos"),
// Custom path with version prefix
contract.WithMethodHTTP("Create", "POST", "/v1/todos"),
// Custom action path (non-CRUD operation)
contract.WithMethodHTTP("Archive", "POST", "/todos/{id}/archive"),
// Nested resource
contract.WithMethodHTTP("GetComments", "GET", "/todos/{id}/comments"),
)
Common use cases:
-
API versioning: Add version prefix to paths
contract.WithMethodHTTP("Create", "POST", "/v2/todos")
-
Custom actions: Operations that donβt fit CRUD
contract.WithMethodHTTP("Archive", "POST", "/todos/{id}/archive")
contract.WithMethodHTTP("Publish", "POST", "/posts/{id}/publish")
contract.WithMethodHTTP("SendEmail", "POST", "/users/{id}/send-email")
-
Nested resources: Related sub-resources
contract.WithMethodHTTP("ListComments", "GET", "/posts/{id}/comments")
contract.WithMethodHTTP("AddTag", "POST", "/todos/{id}/tags")
WithHTTP
Set HTTP bindings for multiple methods at once. Useful when you need to customize many methods:
svc := contract.Register[todo.API](impl,
contract.WithHTTP(map[string]contract.HTTPBinding{
"Create": {Method: "POST", Path: "/v1/todos"},
"Get": {Method: "GET", Path: "/v1/todos/{id}"},
"List": {Method: "GET", Path: "/v1/todos"},
"Update": {Method: "PUT", Path: "/v1/todos/{id}"},
"Delete": {Method: "DELETE", Path: "/v1/todos/{id}"},
}),
)
This is equivalent to calling WithMethodHTTP for each method, but more concise when customizing multiple bindings.
WithDefaults
Set global defaults for the service that appear in generated specifications:
svc := contract.Register[todo.API](impl,
contract.WithDefaults(contract.Defaults{
// Base URL for the API (used in OpenAPI servers array)
BaseURL: "https://api.example.com",
// Default headers (used in client generation)
Headers: map[string]string{
"X-API-Version": "2024-01",
},
}),
)
Where defaults are used:
- OpenAPI specification:
servers array includes the BaseURL
- Client generators: Generated clients use these as defaults
- Documentation: Shows users the production URL
WithStreaming
Mark a method as supporting streaming responses:
package chat
type API interface {
// Chat returns a stream of message chunks
Chat(ctx context.Context, in *ChatInput) (*ChatOutput, error)
}
svc := contract.Register[chat.API](impl,
contract.WithDefaultResource("chat"),
contract.WithStreaming("Chat", contract.StreamSSE),
)
Available streaming modes:
| Mode | Description | Use Case |
|---|
StreamSSE | Server-Sent Events | Real-time updates over HTTP (chat, live data) |
StreamWS | WebSocket | Bidirectional communication |
StreamGRPC | gRPC streaming | High-performance service-to-service |
StreamAsync | Async messaging | Message queue integration |
The Registered Service
After registration, you get a *RegisteredService. This object provides several useful methods:
Descriptor
Get the service descriptor containing all metadata about your service:
svc := contract.Register[todo.API](impl,
contract.WithName("Todo"),
contract.WithDefaultResource("todos"),
)
desc := svc.Descriptor()
// Service-level information
fmt.Println("Name:", desc.Name) // "Todo"
fmt.Println("Description:", desc.Description) // ""
// Iterate through resources
for _, res := range desc.Resources {
fmt.Printf("Resource: %s\n", res.Name)
// Iterate through methods in this resource
for _, m := range res.Methods {
fmt.Printf(" %s %s -> %s\n",
m.HTTP.Method, // "GET", "POST", etc.
m.HTTP.Path, // "/todos", "/todos/{id}", etc.
m.Name, // "create", "list", etc.
)
}
}
// Iterate through registered types
for _, t := range desc.Types {
fmt.Printf("Type: %s (kind: %s)\n", t.Name, t.Kind)
}
Use cases for Descriptor:
- Generating custom documentation
- Building admin dashboards that show available endpoints
- Debugging registration issues
Invoke a method programmatically without going through HTTP:
svc := contract.Register[todo.API](impl,
contract.WithDefaultResource("todos"),
)
ctx := context.Background()
input := &todo.CreateInput{Title: "Buy groceries"}
// Call a method by resource and method name
// Parameters: (ctx, resource, method, input)
result, err := svc.Call(ctx, "todos", "create", input)
if err != nil {
log.Fatal(err)
}
// Type assert the result
createdTodo := result.(*todo.Todo)
fmt.Println("Created:", createdTodo.ID, createdTodo.Title)
Parameters:
ctx - Context for the call (timeouts, cancellation)
resource - Resource name as string (e.g., βtodosβ)
method - Method name in lowercase (e.g., βcreateβ, βlistβ)
input - Input value, or nil for methods without input
Use cases for Call:
- Testing without HTTP
- Internal service-to-service calls
- Building CLI tools that use the same service logic
Create a new instance of a methodβs input type. This is useful for transports and testing:
svc := contract.Register[todo.API](impl,
contract.WithDefaultResource("todos"),
)
// Create a new input instance for the "create" method
input, err := svc.NewInput("todos", "create")
if err != nil {
log.Fatal(err)
}
// Type assert and populate
createInput := input.(*todo.CreateInput)
createInput.Title = "Learn Go"
// Now use it
result, _ := svc.Call(ctx, "todos", "create", createInput)
Why is this useful?: Transports need to create input instances before unmarshaling JSON into them. They use NewInput to get the correct type, then unmarshal the request body into it.
Contract transforms your Go method names when exposing them as API methods. The transformation is simple:
| Go Interface Method | API Method Name |
|---|
Create | create |
CreateTodo | createTodo |
GetByID | getByID |
ListAll | listAll |
Rule: The first letter is lowercased, the rest stays the same.
This follows the JavaScript/JSON convention of using camelCase for property and method names. When clients call your API, they use these lowercase method names.
How HTTP Bindings are Inferred
Contract automatically determines the HTTP method and path based on your Go method name. This inference follows REST conventions.
Method Name to HTTP Verb
Contract looks at how your method name starts to determine the HTTP verb:
| Method Name Starts With | HTTP Method | Why |
|---|
Create, Add, New | POST | Creating a new resource |
List, All, Search, Find*s (plural) | GET | Retrieving multiple resources |
Get, Find, Fetch, Read | GET | Retrieving a single resource |
Update, Edit, Modify, Set | PUT | Replacing a resource |
Delete, Remove | DELETE | Removing a resource |
Patch | PATCH | Partially updating a resource |
| Everything else | POST | Custom action (default to POST) |
Path Generation
The path is determined by the method pattern and resource name:
| Method Pattern | Generated Path | Example |
|---|
| Create-like | /{resource} | POST /todos |
| List-like | /{resource} | GET /todos |
| Get-like | /{resource}/{id} | GET /todos/ |
| Update-like | /{resource}/{id} | PUT /todos/ |
| Delete-like | /{resource}/{id} | DELETE /todos/ |
| Other | /{resource}/{methodName} | POST /todos/archive |
Complete Example
package product
type API interface {
// Inferred: POST /products
Create(ctx context.Context, in *CreateInput) (*Product, error)
// Inferred: GET /products
List(ctx context.Context) (*ListOutput, error)
// Inferred: GET /products (same as List, uses query params)
Search(ctx context.Context, in *SearchInput) (*ListOutput, error)
// Inferred: GET /products/{id}
Get(ctx context.Context, in *GetInput) (*Product, error)
// Inferred: PUT /products/{id}
Update(ctx context.Context, in *UpdateInput) (*Product, error)
// Inferred: PATCH /products/{id}
Patch(ctx context.Context, in *PatchInput) (*Product, error)
// Inferred: DELETE /products/{id}
Delete(ctx context.Context, in *DeleteInput) error
// Inferred: POST /products/archive (custom action)
Archive(ctx context.Context, in *ArchiveInput) error
// Inferred: POST /products/publish (custom action)
Publish(ctx context.Context, in *PublishInput) (*Product, error)
}
For methods with {id} in the path (Get, Update, Delete), Contract needs to know which field in your input struct contains the ID.
Default Behavior
Contract looks for these fields in your input struct, in order:
- Field with
path:"id" tag - Explicit path parameter binding
- Field named
ID - Standard Go naming
- Field ending in
ID - Like TodoID, UserID
Using the path Tag
The path tag explicitly marks a field as a path parameter:
package todo
// GetInput identifies which todo to retrieve
type GetInput struct {
// The path:"id" tag maps this field to {id} in the URL path
ID string `json:"id" path:"id"`
}
// UpdateInput includes both path parameter and update data
type UpdateInput struct {
ID string `json:"id" path:"id"` // Path parameter
Title string `json:"title"` // Body field
Completed bool `json:"completed"` // Body field
}
When a client calls GET /todos/abc123, Contract:
- Extracts
abc123 from the URL path
- Creates a new
GetInput instance
- Sets the
ID field to "abc123"
- Passes this to your method
Multiple Path Parameters
For nested resources, you can have multiple path parameters:
package comment
// GetInput retrieves a comment on a specific post
type GetInput struct {
PostID string `json:"postId" path:"postId"` // First path param
CommentID string `json:"commentId" path:"id"` // Second path param
}
svc := contract.Register[comment.API](impl,
contract.WithMethodHTTP("Get", "GET", "/posts/{postId}/comments/{id}"),
)
// GET /posts/post-123/comments/comment-456
// -> GetInput{PostID: "post-123", CommentID: "comment-456"}
Complete Registration Example
Hereβs a full example demonstrating various registration options with 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 ArchiveInput struct {
ID string `json:"id" path:"id"`
}
type ListOutput struct {
Items []*Todo `json:"items"`
Total int `json:"total"`
}
// todo/api.go
package todo
import "context"
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
Archive(ctx context.Context, in *ArchiveInput) error
}
// todo/service.go
package todo
type Service struct {
// dependencies...
}
var _ API = (*Service)(nil)
func NewService() *Service {
return &Service{}
}
// Implement all methods...
// 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"
"github.com/go-mizu/mizu/contract/v2/transport/jsonrpc"
"yourapp/todo"
)
func main() {
impl := todo.NewService()
// Register with full configuration
svc := contract.Register[todo.API](impl,
// Service metadata
contract.WithName("Todo"),
contract.WithDescription("A todo list management API for tracking tasks"),
// Resource organization - all methods grouped under "todos"
contract.WithDefaultResource("todos"),
// Custom HTTP binding for the Archive action
contract.WithMethodHTTP("Archive", "POST", "/todos/{id}/archive"),
// Global defaults for generated specs
contract.WithDefaults(contract.Defaults{
BaseURL: "https://api.example.com",
}),
)
// Print what was registered (useful for debugging)
desc := svc.Descriptor()
fmt.Printf("Service: %s\n", desc.Name)
fmt.Printf("Description: %s\n", desc.Description)
fmt.Println("\nEndpoints:")
for _, res := range desc.Resources {
for _, m := range res.Methods {
fmt.Printf(" %-7s %-30s -> %s\n",
m.HTTP.Method,
m.HTTP.Path,
m.Name,
)
}
}
// Create app and mount transports
app := mizu.New()
rest.Mount(app.Router, svc)
jsonrpc.Mount(app.Router, "/rpc", svc)
fmt.Println("\nServer starting on http://localhost:8080")
app.Listen(":8080")
}
Output:
Service: Todo
Description: A todo list management API for tracking tasks
Endpoints:
POST /todos -> create
GET /todos -> list
GET /todos/{id} -> get
PUT /todos/{id} -> update
DELETE /todos/{id} -> delete
POST /todos/{id}/archive -> archive
Server starting on http://localhost:8080
Registration Errors
Registration can fail if your interface doesnβt follow Contractβs rules. Here are common issues:
Missing Context Parameter
Every method must have context.Context as the first parameter:
// WRONG: no context parameter
type BadAPI interface {
Create(in *CreateInput) (*Output, error) // Missing ctx!
}
// CORRECT: context as first parameter
type GoodAPI interface {
Create(ctx context.Context, in *CreateInput) (*Output, error)
}
Why context is required: Context carries request-scoped values like timeouts, cancellation signals, and authentication information. Every API method needs this.
Invalid Return Type
Methods can only return (*Output, error) or just error:
// WRONG: multiple return values
type BadAPI interface {
Get(ctx context.Context, in *GetInput) (*User, *Profile, error) // Too many returns!
}
// CORRECT: wrap multiple values in a struct
type GetUserResult struct {
User *User `json:"user"`
Profile *Profile `json:"profile"`
}
type GoodAPI interface {
Get(ctx context.Context, in *GetInput) (*GetUserResult, error)
}
Implementation Doesnβt Match Interface
The implementation must implement all methods in the interface with exact signatures:
package todo
type API interface {
Create(ctx context.Context, in *CreateInput) (*Todo, error)
}
type Service struct{}
// MISSING: Service doesn't have Create method
// This causes a compile error when you try to register
// Compile error:
// cannot use &Service{} (type *Service) as type todo.API in argument to contract.Register:
// *Service does not implement todo.API (missing Create method)
svc := contract.Register[todo.API](&Service{})
Tip: Add a compile-time check in your service file:
var _ API = (*Service)(nil) // Fails at compile time if Service doesn't implement API
Best Practices
Good names and descriptions help users (and AI assistants) understand your API:
svc := contract.Register[todo.API](impl,
contract.WithName("Todo Management"),
contract.WithDescription(`
API for managing todo items. Supports full CRUD operations:
- Create new todos with a title
- List all todos with pagination
- Get, update, or delete specific todos by ID
- Archive completed todos
`),
)
Organize with Separate Packages
Prefer separate packages over one giant interface:
// GOOD: Separate packages for each domain
import (
"yourapp/todo"
"yourapp/user"
"yourapp/order"
)
todoSvc := contract.Register[todo.API](todoImpl, contract.WithDefaultResource("todos"))
userSvc := contract.Register[user.API](userImpl, contract.WithDefaultResource("users"))
orderSvc := contract.Register[order.API](orderImpl, contract.WithDefaultResource("orders"))
rest.Mount(app.Router, todoSvc)
rest.Mount(app.Router, userSvc)
rest.Mount(app.Router, orderSvc)
Create a Register Function
Keep registration logic close to the implementation:
// todo/register.go
package todo
import contract "github.com/go-mizu/mizu/contract/v2"
// Register creates a registered service with standard configuration.
// This keeps registration options close to the service definition.
func Register(impl API) *contract.RegisteredService {
return contract.Register[API](impl,
contract.WithName("Todo"),
contract.WithDescription("Todo management API"),
contract.WithDefaultResource("todos"),
)
}
// main.go
func main() {
impl := todo.NewService(db)
svc := todo.Register(impl) // Clean and encapsulated
rest.Mount(app.Router, svc)
}
Common Questions
Can I register multiple services?
Yes! Register each service separately and mount them all:
todoSvc := contract.Register[todo.API](todoImpl, contract.WithDefaultResource("todos"))
userSvc := contract.Register[user.API](userImpl, contract.WithDefaultResource("users"))
// Mount both on the same router
rest.Mount(app.Router, todoSvc)
rest.Mount(app.Router, userSvc)
// Both are accessible:
// GET /todos, POST /todos, etc.
// GET /users, POST /users, etc.
Can I modify registration after itβs created?
No, registration is immutable. If you need different options, create a new registration:
// Create different registrations for different purposes
devSvc := contract.Register[todo.API](impl,
contract.WithDefaultResource("todos"),
)
prodSvc := contract.Register[todo.API](impl,
contract.WithDefaultResource("todos"),
contract.WithDefaults(contract.Defaults{
BaseURL: "https://api.example.com",
}),
)
How do I access the original implementation?
The registered service wraps your implementation. Keep a reference if you need direct access:
impl := todo.NewService(db)
svc := contract.Register[todo.API](impl)
// svc is the registered wrapper
// impl is your original implementation
// If you need to call internal methods directly:
impl.SomeInternalMethod()
Whatβs Next?
Now that you understand registration:
- Type System - How Go types are converted to JSON schemas
- Transports - Mount your service on REST, JSON-RPC, or MCP
- Error Handling - Handle errors consistently across protocols