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.
Client Generation
One of the best things about Contract is that you can generate typed clients in any language. Your Go service becomes a TypeScript client, Python client, or any other language - with full type safety.
Native SDK Generation : Mizu now includes built-in SDK generators for Go, Python, and TypeScript that produce idiomatic, zero-dependency clients with better developer experience than OpenAPI-based generation. See SDK Generation for details.
Two Approaches to Client Generation
Approach Best For Pros Cons Native SDKs Go, Python, TypeScript Idiomatic code, zero deps, better DX, streaming support Limited language support OpenAPI (this page) Other languages, ecosystem compatibility 40+ languages, mature tooling Generic code, more dependencies
Native SDK Generation Built-in generators for Go, Python, and TypeScript with OpenAI-style developer experience
OpenAPI Generation Use OpenAPI spec with third-party generators for any language (continue reading below)
How It Works
Contract provides machine-readable descriptions of your API:
Your Go Service
↓
OpenAPI Spec / MCP Tools
↓
Client Generator (openapi-generator, etc.)
↓
TypeScript Client, Python Client, Go Client, etc.
Endpoint Format Best For /openapi.jsonOpenAPI 3.1 Most languages, best tooling /mcp (tools/list)MCP tools AI assistants
OpenAPI Client Generation
OpenAPI has the most mature tooling. This is the recommended approach.
Step 1: Serve OpenAPI Spec
import (
" 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/openapi "
" yourapp/todo "
)
impl := todo . NewService ()
svc := contract . Register [ todo . API ]( impl ,
contract . WithDefaultResource ( "todos" ),
)
app := mizu . New ()
rest . Mount ( app . Router , svc )
openapi . Mount ( app . Router , "/openapi.json" , svc )
app . Listen ( ":8080" )
Step 2: Download the Spec
curl http://localhost:8080/openapi.json > openapi.json
Step 3: Generate Clients
Use any OpenAPI code generator. Here are the most popular:
TypeScript
Using openapi-typescript (types only):
npm install -D openapi-typescript
npx openapi-typescript openapi.json -o ./src/api/types.ts
Using openapi-typescript-codegen (full client):
npm install -D openapi-typescript-codegen
npx openapi-typescript-codegen --input openapi.json --output ./src/api
Using openapi-generator:
npm install -g @openapitools/openapi-generator-cli
openapi-generator-cli generate -i openapi.json -g typescript-fetch -o ./api-client
Using openapi-python-client:
pip install openapi-python-client
openapi-python-client generate --path openapi.json --output-path ./client
Using openapi-generator:
openapi-generator-cli generate -i openapi.json -g python -o ./python-client
Using oapi-codegen:
go install github.com/deepmap/oapi-codegen/v2/cmd/oapi-codegen@latest
oapi-codegen -package client openapi.json > client/client.go
Using openapi-generator:
openapi-generator-cli generate -i openapi.json -g go -o ./go-client
Other Languages
# Rust
openapi-generator-cli generate -i openapi.json -g rust -o ./rust-client
# Java
openapi-generator-cli generate -i openapi.json -g java -o ./java-client
# Ruby
openapi-generator-cli generate -i openapi.json -g ruby -o ./ruby-client
# PHP
openapi-generator-cli generate -i openapi.json -g php -o ./php-client
Handwritten Clients
Sometimes you don’t need code generation - a simple handwritten client works fine.
Go Client
package todoclient
import (
" bytes "
" context "
" encoding/json "
" fmt "
" net/http "
)
type Client struct {
baseURL string
httpClient * http . Client
}
func New ( baseURL string ) * Client {
return & Client {
baseURL : baseURL ,
httpClient : & http . Client {},
}
}
// Types
type Todo struct {
ID string `json:"id"`
Title string `json:"title"`
Done bool `json:"done"`
}
type CreateInput struct {
Title string `json:"title"`
}
type ListOutput struct {
Items [] * Todo `json:"items"`
Count int `json:"count"`
}
// Methods
func ( c * Client ) Create ( ctx context . Context , input * CreateInput ) ( * Todo , error ) {
return doRequest [ Todo ]( ctx , c , "POST" , "/todos" , input )
}
func ( c * Client ) List ( ctx context . Context ) ( * ListOutput , error ) {
return doRequest [ ListOutput ]( ctx , c , "GET" , "/todos" , nil )
}
func ( c * Client ) Get ( ctx context . Context , id string ) ( * Todo , error ) {
return doRequest [ Todo ]( ctx , c , "GET" , "/todos/" + id , nil )
}
func ( c * Client ) Delete ( ctx context . Context , id string ) error {
_ , err := doRequest [ struct {}]( ctx , c , "DELETE" , "/todos/" + id , nil )
return err
}
// Helper
func doRequest [ T any ]( ctx context . Context , c * Client , method , path string , body any ) ( * T , error ) {
var bodyReader * bytes . Reader
if body != nil {
data , _ := json . Marshal ( body )
bodyReader = bytes . NewReader ( data )
}
req , err := http . NewRequestWithContext ( ctx , method , c . baseURL + path , bodyReader )
if err != nil {
return nil , err
}
req . Header . Set ( "Content-Type" , "application/json" )
resp , err := c . httpClient . Do ( req )
if err != nil {
return nil , err
}
defer resp . Body . Close ()
if resp . StatusCode >= 400 {
return nil , fmt . Errorf ( "HTTP %d " , resp . StatusCode )
}
var result T
if err := json . NewDecoder ( resp . Body ). Decode ( & result ); err != nil {
return nil , err
}
return & result , nil
}
Usage:
client := todoclient . New ( "http://localhost:8080" )
todo , err := client . Create ( ctx , & todoclient . CreateInput { Title : "Test" })
if err != nil {
log . Fatal ( err )
}
fmt . Println ( todo . ID )
Python Client
from dataclasses import dataclass
from typing import List, Optional
import requests
@dataclass
class Todo :
id : str
title: str
done: bool
@dataclass
class ListOutput :
items: List[Todo]
count: int
class TodoClient :
def __init__ ( self , base_url : str ):
self .base_url = base_url.rstrip( '/' )
def create ( self , title : str ) -> Todo:
response = requests.post(
f " { self .base_url } /todos" ,
json = { "title" : title}
)
response.raise_for_status()
data = response.json()
return Todo( ** data)
def list ( self ) -> ListOutput:
response = requests.get( f " { self .base_url } /todos" )
response.raise_for_status()
data = response.json()
return ListOutput(
items = [Todo( ** item) for item in data[ "items" ]],
count = data[ "count" ]
)
def get ( self , id : str ) -> Optional[Todo]:
response = requests.get( f " { self .base_url } /todos/ { id } " )
if response.status_code == 404 :
return None
response.raise_for_status()
return Todo( ** response.json())
def delete ( self , id : str ) -> None :
response = requests.delete( f " { self .base_url } /todos/ { id } " )
response.raise_for_status()
Usage:
client = TodoClient( "http://localhost:8080" )
# Create
todo = client.create( "Buy milk" )
print ( f "Created: { todo.id } " )
# List
result = client.list()
print ( f "Total: { result.count } " )
# Get
todo = client.get( "1" )
if todo:
print ( f "Found: { todo.title } " )
JSON-RPC Client
For JSON-RPC transport, clients need to handle the JSON-RPC envelope:
TypeScript
interface JsonRpcRequest {
jsonrpc : '2.0' ;
id : number ;
method : string ;
params ?: any ;
}
interface JsonRpcResponse < T > {
jsonrpc : '2.0' ;
id : number ;
result ?: T ;
error ?: { code : number ; message : string };
}
class JsonRpcClient {
private nextId = 1 ;
constructor ( private baseUrl : string ) {}
async call < T >( method : string , params ?: any ) : Promise < T > {
const request : JsonRpcRequest = {
jsonrpc: '2.0' ,
id: this . nextId ++ ,
method ,
params ,
};
const response = await fetch ( this . baseUrl , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ( request ),
});
const rpcResponse : JsonRpcResponse < T > = await response . json ();
if ( rpcResponse . error ) {
throw new Error ( ` ${ rpcResponse . error . code } : ${ rpcResponse . error . message } ` );
}
return rpcResponse . result ! ;
}
// Batch multiple calls in one request
async batch < T extends any []>( calls : { method : string ; params ?: any }[]) : Promise < T > {
const requests = calls . map (( call , i ) => ({
jsonrpc: '2.0' as const ,
id: this . nextId ++ ,
method: call . method ,
params: call . params ,
}));
const response = await fetch ( this . baseUrl , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ( requests ),
});
const responses : JsonRpcResponse < any >[] = await response . json ();
return responses . map (( r ) => r . result ) as T ;
}
}
// Typed wrapper
class TodoJsonRpcClient {
private rpc : JsonRpcClient ;
constructor ( baseUrl : string ) {
this . rpc = new JsonRpcClient ( baseUrl + '/rpc' );
}
create ( input : CreateInput ) : Promise < Todo > {
return this . rpc . call ( 'Create' , input );
}
list () : Promise < ListOutput > {
return this . rpc . call ( 'List' );
}
// Batch create multiple todos in one request
async createMany ( inputs : CreateInput []) : Promise < Todo []> {
return this . rpc . batch ( inputs . map ( input => ({ method: 'Create' , params: input })));
}
}
Error Handling in Clients
Always handle errors properly in your clients:
TypeScript
class ApiError extends Error {
constructor (
message : string ,
public code : string ,
public status : number
) {
super ( message );
this . name = 'ApiError' ;
}
}
async function handleResponse < T >( response : Response ) : Promise < T > {
if ( ! response . ok ) {
const body = await response . text ();
throw new ApiError ( body , 'HTTP_ERROR' , response . status );
}
const data = await response . json ();
return data ;
}
// In client methods
async create ( input : CreateInput ): Promise < Todo > {
try {
const response = await fetch ( ` ${ this . baseUrl } /todos` , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ( input ),
});
return handleResponse < Todo >( response );
} catch (error) {
if ( error instanceof ApiError ) {
// Handle specific error codes
switch ( error . status ) {
case 400 :
throw new Error ( 'Invalid input: ' + error . message );
case 404 :
throw new Error ( 'Not found' );
default :
throw error ;
}
}
throw error;
}
}
type APIError struct {
Code string
Message string
Status int
}
func ( e * APIError ) Error () string {
return fmt . Sprintf ( "[ %d ] %s : %s " , e . Status , e . Code , e . Message )
}
func doRequest [ T any ]( ctx context . Context , c * Client , method , path string , body any ) ( * T , error ) {
// ... (make request)
if resp . StatusCode >= 400 {
body , _ := io . ReadAll ( resp . Body )
return nil , & APIError {
Code : "HTTP_ERROR" ,
Message : string ( body ),
Status : resp . StatusCode ,
}
}
// ... (decode response)
}
// Usage
todo , err := client . Get ( ctx , "123" )
if err != nil {
var apiErr * APIError
if errors . As ( err , & apiErr ) {
if apiErr . Status == 404 {
// Handle not found
}
}
}
Automation with CI/CD
Automate client generation in your build:
GitHub Action
name : Generate Clients
on :
push :
paths :
- 'api/**' # When API changes
jobs :
generate :
runs-on : ubuntu-latest
steps :
- uses : actions/checkout@v3
- name : Start API server
run : go run main.go &
env :
PORT : 8080
- name : Wait for server
run : sleep 5
- name : Download OpenAPI spec
run : curl http://localhost:8080/openapi.json > openapi.json
- name : Generate TypeScript client
run : npx openapi-typescript openapi.json -o client/ts/types.ts
- name : Generate Python client
run : |
pip install openapi-python-client
openapi-python-client generate --path openapi.json --output-path client/python
- name : Commit generated clients
run : |
git add client/
git commit -m "Update generated clients" || true
git push
Best Practices
1. Version Your API
Include version in your service:
svc := contract . Register [ todo . API ]( impl ,
contract . WithName ( "Todo" ),
contract . WithDefaultResource ( "todos" ),
)
2. Keep Clients in Sync
Regenerate clients when your API changes. Consider:
Git submodules for shared clients
Package registry (npm, PyPI) for versioned clients
API versioning for breaking changes
Clients often need authentication:
class TodoClient {
constructor ( private baseUrl : string , private token : string ) {}
private async fetch ( path : string , options : RequestInit = {}) {
return fetch ( this . baseUrl + path , {
... options ,
headers: {
'Content-Type' : 'application/json' ,
'Authorization' : `Bearer ${ this . token } ` ,
... options . headers ,
},
});
}
}
4. Handle Rate Limits
async function withRetry < T >( fn : () => Promise < T >, maxRetries = 3 ) : Promise < T > {
for ( let i = 0 ; i < maxRetries ; i ++ ) {
try {
return await fn ();
} catch ( error ) {
if ( error instanceof ApiError && error . status === 429 ) {
await sleep ( Math . pow ( 2 , i ) * 1000 ); // Exponential backoff
continue ;
}
throw error ;
}
}
throw new Error ( 'Max retries exceeded' );
}
See Also