Skip to main content
The Go SDK generator creates native Go client libraries from your contract definitions. The generated code uses only the standard library, requires no external dependencies, and provides a resource-based API with full type safety.

Key Features

  • Zero Dependencies: Only uses Go’s standard library (net/http, encoding/json)
  • Single File Output: One self-contained Go file
  • Resource-Based API: Intuitive access (client.Todos.Create())
  • Full Type Safety: Complete Go types with proper JSON tags
  • SSE Streaming: Built-in EventStream[T] for server-sent events
  • Union Types: Proper marshal/unmarshal for discriminated unions

Quick Start

Step 1: Define Your Contract

# api.yaml
name: TodoAPI
description: Todo list API

defaults:
  base_url: http://localhost:8080

resources:
  - name: todos
    description: Manage todo items
    methods:
      - name: create
        input: CreateInput
        output: Todo
        http:
          method: POST
          path: /todos
      - name: list
        output: ListOutput
        http:
          method: GET
          path: /todos
      - name: get
        input: GetInput
        output: Todo
        http:
          method: GET
          path: /todos/{id}
      - name: delete
        input: DeleteInput
        http:
          method: DELETE
          path: /todos/{id}

types:
  - name: Todo
    kind: struct
    fields:
      - name: id
        type: string
      - name: title
        type: string
      - name: done
        type: bool
  - name: CreateInput
    kind: struct
    fields:
      - name: title
        type: string
  - name: GetInput
    kind: struct
    fields:
      - name: id
        type: string
  - name: DeleteInput
    kind: struct
    fields:
      - name: id
        type: string
  - name: ListOutput
    kind: struct
    fields:
      - name: items
        type: "[]Todo"
      - name: total
        type: int

Step 2: Generate the SDK

mizu contract gen api.yaml --client --lang go --output ./sdk/todoclient --package todoclient

Step 3: Use the Generated Client

package main

import (
    "context"
    "fmt"
    "log"

    "yourapp/sdk/todoclient"
)

func main() {
    // Create a client
    client := todoclient.NewClient("http://localhost:8080",
        todoclient.WithAuth("your-api-key"),
    )

    ctx := context.Background()

    // Create a todo
    todo, err := client.Todos.Create(ctx, &todoclient.CreateInput{
        Title: "Learn Mizu Go SDK",
    })
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Created: %s\n", todo.ID)

    // List all todos
    result, err := client.Todos.List(ctx)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Total: %d todos\n", result.Total)

    // Get a specific todo
    todo, err = client.Todos.Get(ctx, &todoclient.GetInput{ID: todo.ID})
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Got: %s - %s\n", todo.ID, todo.Title)
}

Generated Code Structure

The generated file contains everything in a single package:
// Code generated by mizu contract gen. DO NOT EDIT.

package todoclient

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "strings"
)

// === Types ===

type Todo struct { ... }
type CreateInput struct { ... }
type ListOutput struct { ... }

// === Client ===

type Client struct { ... }
func NewClient(baseURL string, opts ...Option) *Client { ... }

// === Options ===

type Option func(*Client)
func WithHTTPClient(h *http.Client) Option { ... }
func WithAuth(token string) Option { ... }
func WithHeaders(headers map[string]string) Option { ... }

// === Resources ===

type TodosResource struct { ... }
func (r *TodosResource) Create(ctx context.Context, in *CreateInput) (*Todo, error) { ... }
func (r *TodosResource) List(ctx context.Context) (*ListOutput, error) { ... }
func (r *TodosResource) Get(ctx context.Context, in *GetInput) (*Todo, error) { ... }
func (r *TodosResource) Delete(ctx context.Context, in *DeleteInput) error { ... }

// === Streaming (if applicable) ===

type EventStream[T any] struct { ... }
func (s *EventStream[T]) Next() bool { ... }
func (s *EventStream[T]) Event() T { ... }
func (s *EventStream[T]) Err() error { ... }
func (s *EventStream[T]) Close() error { ... }

// === Internal Helpers ===

func (c *Client) do(ctx context.Context, method, path string, in, out any) error { ... }

Client Configuration

Creating a Client

// Basic client with just base URL
client := todoclient.NewClient("http://localhost:8080")

// With options
client := todoclient.NewClient("http://localhost:8080",
    todoclient.WithAuth("your-api-key"),
    todoclient.WithHTTPClient(&http.Client{
        Timeout: 30 * time.Second,
    }),
    todoclient.WithHeaders(map[string]string{
        "X-Custom-Header": "value",
    }),
)

Available Options

OptionDescription
WithAuth(token string)Set Bearer authentication token
WithHTTPClient(client *http.Client)Use custom HTTP client
WithHeaders(headers map[string]string)Add default headers to all requests

Type System

Type Mapping Reference

Contract TypeGo Type
stringstring
boolbool
int, int32int32
int64int64
int8, int16int8, int16
uint, uint32uint32
uint64uint64
float32float32
float64float64
time.Timetime.Time
json.RawMessagejson.RawMessage
anyany
[]T[]T
map[string]Tmap[string]T

Struct Types

Contract struct types generate Go structs with JSON tags:
// Generated from contract type
type Todo struct {
    ID        string    `json:"id"`
    Title     string    `json:"title"`
    Done      bool      `json:"done"`
    CreatedAt time.Time `json:"created_at"`
}

Optional and Nullable Fields

Optional and nullable fields become pointers with omitempty:
Contract DefinitionGo TypeJSON Tag
Required fieldTjson:"name"
optional: true*Tjson:"name,omitempty"
nullable: true*Tjson:"name"
Both optional and nullable*Tjson:"name,omitempty"
Example:
// Contract definition
type UpdateInput struct {
    Title string `json:"title"`          // required
    Done  *bool  `json:"done,omitempty"` // optional
}

// Usage
client.Todos.Update(ctx, &UpdateInput{
    Title: "Updated title",
    Done:  ptr(true), // helper function: func ptr[T any](v T) *T { return &v }
})

Enum Fields

Enum fields generate as the base type with a comment:
// Contract: enum: [pending, active, completed]
// Generated:
// Status is one of: pending, active, completed
Status string `json:"status"`

Slice and Map Types

// Contract: kind: slice, elem: Todo
type TodoList []Todo

// Contract: kind: map, elem: string
type Metadata map[string]string

Union Types (Discriminated)

Union types generate a struct with pointer fields for each variant, plus marshal/unmarshal methods:
// ContentPart is a discriminated union (tag: "type").
// Variants: ContentPartText, ContentPartImage
type ContentPart struct {
    Text  *ContentPartText  `json:"-"`
    Image *ContentPartImage `json:"-"`
}

func (u *ContentPart) MarshalJSON() ([]byte, error) {
    if u.Text != nil {
        return json.Marshal(u.Text)
    }
    if u.Image != nil {
        return json.Marshal(u.Image)
    }
    return []byte("null"), nil
}

func (u *ContentPart) UnmarshalJSON(data []byte) error {
    var disc struct{ Type string `json:"type"` }
    if err := json.Unmarshal(data, &disc); err != nil {
        return err
    }
    switch disc.Type {
    case "text":
        u.Text = new(ContentPartText)
        return json.Unmarshal(data, u.Text)
    case "image":
        u.Image = new(ContentPartImage)
        return json.Unmarshal(data, u.Image)
    }
    return fmt.Errorf("unknown ContentPart type: %q", disc.Type)
}
Usage:
// Creating a union value
part := &ContentPart{
    Text: &ContentPartText{
        Type: "text",
        Content: "Hello",
    },
}

// Checking which variant
if part.Text != nil {
    fmt.Println("Text:", part.Text.Content)
} else if part.Image != nil {
    fmt.Println("Image:", part.Image.URL)
}

Resources and Methods

Resource Pattern

Each contract resource becomes a struct attached to the client:
type Client struct {
    Todos  *TodosResource
    Users  *UsersResource
    // ...
}
Access resources through the client:
client.Todos.Create(ctx, input)
client.Users.Get(ctx, &GetInput{ID: "user-123"})

Method Signatures

Generated method signatures follow these patterns:
Contract MethodGenerated Signature
No input, no outputMethod(ctx) error
No input, with outputMethod(ctx) (*Output, error)
With input, no outputMethod(ctx, *Input) error
With input, with outputMethod(ctx, *Input) (*Output, error)
All methods take context.Context as the first parameter for cancellation and timeout support.

Streaming (SSE)

For methods with streaming support, the generator creates an EventStream[T] type:

Stream Method Signature

func (r *ResponsesResource) Stream(ctx context.Context, in *StreamRequest) *EventStream[ResponseEvent]

EventStream API

type EventStream[T any] struct {
    // internal fields
}

// Next advances to the next event. Returns false when done or on error.
func (s *EventStream[T]) Next() bool

// Event returns the current event (call after Next returns true).
func (s *EventStream[T]) Event() T

// Err returns the error if Next returned false due to an error.
func (s *EventStream[T]) Err() error

// Close releases the underlying connection.
func (s *EventStream[T]) Close() error

Streaming Usage

stream := client.Responses.Stream(ctx, &StreamRequest{
    Model: "gpt-4",
    Input: "Hello!",
})
defer stream.Close()

for stream.Next() {
    event := stream.Event()

    // Handle different event types (for unions)
    switch {
    case event.TextDelta != nil:
        fmt.Print(event.TextDelta.Text)
    case event.Completed != nil:
        fmt.Println("\n--- Done ---")
    }
}

if err := stream.Err(); err != nil {
    log.Fatal("Stream error:", err)
}

Cancellation

Use context cancellation to stop a stream early:
ctx, cancel := context.WithCancel(context.Background())

stream := client.Responses.Stream(ctx, input)
defer stream.Close()

go func() {
    time.Sleep(5 * time.Second)
    cancel() // Stop the stream after 5 seconds
}()

for stream.Next() {
    fmt.Print(stream.Event().Text)
}

Error Handling

Error Type

Generated code includes an Error type for API errors:
type Error struct {
    StatusCode int    `json:"-"`
    Code       string `json:"code,omitempty"`
    Message    string `json:"message"`
}

func (e *Error) Error() string {
    if e.Code != "" {
        return fmt.Sprintf("%s: %s", e.Code, e.Message)
    }
    return e.Message
}

Checking Errors

todo, err := client.Todos.Get(ctx, &GetInput{ID: "nonexistent"})
if err != nil {
    var apiErr *todoclient.Error
    if errors.As(err, &apiErr) {
        switch apiErr.StatusCode {
        case 404:
            fmt.Println("Todo not found")
        case 401:
            fmt.Println("Unauthorized")
        case 400:
            fmt.Printf("Bad request: %s\n", apiErr.Message)
        default:
            fmt.Printf("API error %d: %s\n", apiErr.StatusCode, apiErr.Message)
        }
        return
    }
    // Network or other error
    log.Fatal(err)
}

Advanced Usage

Custom HTTP Client

// With timeout
client := todoclient.NewClient(baseURL,
    todoclient.WithHTTPClient(&http.Client{
        Timeout: 30 * time.Second,
    }),
)

// With transport for proxies, TLS config, etc.
transport := &http.Transport{
    Proxy: http.ProxyFromEnvironment,
    TLSClientConfig: &tls.Config{
        MinVersion: tls.VersionTLS12,
    },
}
client := todoclient.NewClient(baseURL,
    todoclient.WithHTTPClient(&http.Client{
        Transport: transport,
        Timeout:   30 * time.Second,
    }),
)

Context Usage

Always pass context for proper cancellation and timeout:
// With timeout
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

todo, err := client.Todos.Create(ctx, input)

// With values (e.g., request ID for logging)
ctx = context.WithValue(ctx, "requestID", "abc-123")

Concurrent Requests

var wg sync.WaitGroup
results := make([]*Todo, 3)
errors := make([]error, 3)

ids := []string{"1", "2", "3"}

for i, id := range ids {
    wg.Add(1)
    go func(idx int, todoID string) {
        defer wg.Done()
        results[idx], errors[idx] = client.Todos.Get(ctx, &GetInput{ID: todoID})
    }(i, id)
}

wg.Wait()

Complete Example

Server

// main.go
package main

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"
)

func main() {
    impl := todo.NewService()
    svc := contract.Register[todo.API](impl,
        contract.WithDefaultResource("todos"),
        contract.WithDefaults(&contract.Defaults{
            BaseURL: "http://localhost:8080",
        }),
    )

    app := mizu.New()
    rest.Mount(app.Router, svc)
    app.Listen(":8080")
}

Client Usage

// example/main.go
package main

import (
    "context"
    "fmt"
    "log"

    "yourapp/sdk/todoclient"
)

func main() {
    client := todoclient.NewClient("http://localhost:8080")
    ctx := context.Background()

    // Create
    todo, err := client.Todos.Create(ctx, &todoclient.CreateInput{
        Title: "Buy groceries",
    })
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Created: %s\n", todo.ID)

    // List
    result, err := client.Todos.List(ctx)
    if err != nil {
        log.Fatal(err)
    }
    for _, t := range result.Items {
        status := " "
        if t.Done {
            status = "x"
        }
        fmt.Printf("[%s] %s: %s\n", status, t.ID, t.Title)
    }

    // Delete
    if err := client.Todos.Delete(ctx, &todoclient.DeleteInput{ID: todo.ID}); err != nil {
        log.Fatal(err)
    }
    fmt.Println("Deleted successfully")
}

See Also