# Overview Source: https://docs.go-mizu.dev/cli/api/overview Production-ready JSON API with feature-based organization The API template creates a well-structured JSON API service. An API (Application Programming Interface) is a way for programs to talk to each other - when you build an API, you create endpoints (URLs) that other programs can call to get or send data, usually in JSON format. This template is designed for production use with a clean architecture that scales as your project grows. ## When to Use This Template Use the API template when: * **Building a REST API** - JSON endpoints for web or mobile clients * **Multiple developers** - Clear structure everyone can follow * **Production service** - Ready for deployment * **Need organization** - More than a few routes ## What You Get A complete project structure with: * **Feature-based organization** - Code grouped by domain * **Configuration management** - Environment-based config * **App lifecycle** - Clean startup and shutdown * **Example endpoints** - Health check, echo, users ## Quick Start ```bash theme={null} # Create the project mizu new myapi --template api # Enter the project cd myapi # Install dependencies go mod tidy # Run the server mizu dev ``` Test it: ```bash theme={null} curl http://localhost:8080/health curl http://localhost:8080/hello curl http://localhost:8080/api/users ``` ## Project Structure ``` myapi/ ├── cmd/ │ └── api/ │ └── main.go # Entry point ├── app/ │ └── api/ │ ├── app.go # Application setup │ ├── config.go # Configuration │ └── routes.go # Route registration ├── feature/ │ ├── echo/ │ │ └── http.go # Echo handler │ ├── health/ │ │ └── http.go # Health check │ ├── hello/ │ │ └── http.go # Hello handler │ └── users/ │ └── http.go # Users API ├── go.mod └── .gitignore ``` ## Key Features ### Feature-Based Organization Each feature (domain/module) gets its own folder: ``` feature/ ├── users/ # User management ├── products/ # Product catalog ├── orders/ # Order processing └── auth/ # Authentication ``` This keeps related code together and makes it easy to find things. ### Configuration Management Configuration is loaded from environment variables: ```go theme={null} type Config struct { Addr string // Server address Dev bool // Development mode } func LoadConfig() Config { return Config{ Addr: getEnv("ADDR", ":8080"), Dev: getEnv("DEV", "true") == "true", } } ``` ### Application Structure The `App` struct manages lifecycle: ```go theme={null} type App struct { cfg Config app *mizu.App } func New(cfg Config) *App { a := &App{cfg: cfg} a.app = mizu.New() a.routes() return a } ``` ## Example Routes ### Health Check ```bash theme={null} curl http://localhost:8080/health ``` ```json theme={null} {"status":"ok"} ``` ### Hello Endpoint ```bash theme={null} curl http://localhost:8080/hello ``` ``` Hello from myapi ``` ### Users API ```bash theme={null} curl http://localhost:8080/api/users ``` ```json theme={null} [{"id":"1","name":"Alice"},{"id":"2","name":"Bob"}] ``` ### Echo Endpoint ```bash theme={null} curl -X POST http://localhost:8080/echo -d '{"message":"hi"}' -H "Content-Type: application/json" ``` ```json theme={null} {"message":"hi"} ``` ## Why This Structure? ### Scalability As your API grows, add new features without cluttering existing code: ```bash theme={null} # Add a new feature mkdir -p feature/products touch feature/products/http.go ``` ### Testability Each feature can be tested independently: ```bash theme={null} go test ./feature/users/... go test ./feature/products/... ``` ### Team Collaboration Clear boundaries mean less merge conflicts: * Developer A works on `feature/users/` * Developer B works on `feature/products/` ### Maintainability Find code easily: * "Where's the user creation logic?" → `feature/users/` * "Where are routes defined?" → `app/api/routes.go` * "Where's the config?" → `app/api/config.go` ## Comparison with Minimal | Aspect | minimal | api | | ------------ | -------------------- | --------------------- | | Files | 1 | 10+ | | Structure | None | Feature-based | | Config | Hardcoded | Environment variables | | Suitable for | Learning, prototypes | Production | | Scalability | Poor | Good | ## Next Steps Detailed file-by-file explanation Build a complete API step by step # Structure Source: https://docs.go-mizu.dev/cli/api/structure Understanding every file in the API template The API template uses a feature-based architecture. "Feature-based" means your code is organized by what it does (users, products, orders) rather than by type (models, controllers, views). This makes it easier to find related code and work on one feature without affecting others. ## Directory Layout ``` myapi/ ├── cmd/ │ └── api/ │ └── main.go # Entry point ├── app/ │ └── api/ │ ├── app.go # Application struct and setup │ ├── config.go # Configuration loading │ └── routes.go # Route registration ├── feature/ │ ├── echo/ │ │ └── http.go # Echo endpoint │ ├── health/ │ │ └── http.go # Health check │ ├── hello/ │ │ └── http.go # Hello endpoint │ └── users/ │ └── http.go # Users API ├── go.mod ├── .gitignore └── README.md ``` ## The cmd/ Directory Contains the entry point(s) for your application. ### cmd/api/main.go ```go theme={null} package main import ( "log" "example.com/myapi/app/api" ) func main() { cfg := api.LoadConfig() app := api.New(cfg) log.Printf("listening on %s", cfg.Addr) if err := app.Listen(cfg.Addr); err != nil { log.Fatal(err) } } ``` **What it does:** 1. Loads configuration 2. Creates the application 3. Starts the HTTP server **Why separate from app/?** * Keeps `main()` minimal * Makes it easy to add more commands (workers, migrations) * Application logic is testable without `main()` ## The app/ Directory Contains application setup and configuration. ### app/api/app.go ```go theme={null} package api import "github.com/go-mizu/mizu" // App is the API application. type App struct { cfg Config app *mizu.App } // New creates a new App. func New(cfg Config) *App { a := &App{cfg: cfg} a.app = mizu.New() a.routes() return a } // Listen starts the HTTP server. func (a *App) Listen(addr string) error { return a.app.Listen(addr) } ``` **What it does:** * Defines the `App` struct that holds configuration and router * Provides a `New()` constructor that sets everything up * Exposes a `Listen()` method to start the server **Key concepts:** | Part | Purpose | | ------------ | ---------------------------------------- | | `App` struct | Groups related state together | | `New()` | Constructor pattern for setup | | `routes()` | Called in constructor to register routes | | `Listen()` | Thin wrapper around mizu's Listen | ### app/api/config.go ```go theme={null} package api import "os" // Config holds application configuration. type Config struct { Addr string Dev bool } // LoadConfig loads configuration from environment. func LoadConfig() Config { return Config{ Addr: getEnv("ADDR", ":8080"), Dev: getEnv("DEV", "true") == "true", } } func getEnv(key, defaultValue string) string { if value := os.Getenv(key); value != "" { return value } return defaultValue } ``` **What it does:** * Defines configuration structure * Loads values from environment variables * Provides sensible defaults **Adding more config:** ```go theme={null} type Config struct { Addr string Dev bool DatabaseURL string // Add new field } func LoadConfig() Config { return Config{ Addr: getEnv("ADDR", ":8080"), Dev: getEnv("DEV", "true") == "true", DatabaseURL: getEnv("DATABASE_URL", ""), // Load it } } ``` ### app/api/routes.go ```go theme={null} package api import ( "example.com/myapi/feature/echo" "example.com/myapi/feature/health" "example.com/myapi/feature/hello" "example.com/myapi/feature/users" ) func (a *App) routes() { a.app.Get("/health", health.Handler()) a.app.Get("/hello", hello.Handler()) a.app.Get("/api/users", users.List()) a.app.Post("/echo", echo.Handler()) } ``` **What it does:** * Registers all routes in one place * Imports handlers from feature packages * Makes route structure visible at a glance **Adding a new route:** ```go theme={null} import "example.com/myapi/feature/products" func (a *App) routes() { // Existing routes... // Add new route a.app.Get("/api/products", products.List()) a.app.Get("/api/products/:id", products.Get()) a.app.Post("/api/products", products.Create()) } ``` ## The feature/ Directory Contains your business logic, organized by domain. ### feature/health/http.go ```go theme={null} package health import "github.com/go-mizu/mizu" // Handler returns a health check handler. func Handler() mizu.Handler { return func(c *mizu.Ctx) error { return c.JSON(200, map[string]string{ "status": "ok", }) } } ``` **Pattern:** Factory function that returns a handler. **Why this pattern?** * Handlers can accept dependencies (database, logger) * Clean separation between setup and execution * Easy to test ### feature/hello/http.go ```go theme={null} package hello import "github.com/go-mizu/mizu" // Handler returns the hello handler. func Handler() mizu.Handler { return func(c *mizu.Ctx) error { return c.Text(200, "Hello from myapi\n") } } ``` Simple text response handler. ### feature/echo/http.go ```go theme={null} package echo import "github.com/go-mizu/mizu" // Handler returns the echo handler. func Handler() mizu.Handler { return func(c *mizu.Ctx) error { var body map[string]any if err := c.Bind(&body); err != nil { return err } return c.JSON(200, body) } } ``` **What it does:** * Reads JSON from request body * Returns the same JSON back * Useful for testing ### feature/users/http.go ```go theme={null} package users import "github.com/go-mizu/mizu" // User represents a user. type User struct { ID string `json:"id"` Name string `json:"name"` } // List returns a handler that lists users. func List() mizu.Handler { // In a real app, this would query a database users := []User{ {ID: "1", Name: "Alice"}, {ID: "2", Name: "Bob"}, } return func(c *mizu.Ctx) error { return c.JSON(200, users) } } ``` **What it shows:** * Type definitions live with their handlers * Mock data for the example (replace with database) * Clean API boundary ## Adding a New Feature Let's add a `products` feature: ### 1. Create the Package ```bash theme={null} mkdir -p feature/products ``` ### 2. Create the Handler Create `feature/products/http.go`: ```go theme={null} package products import "github.com/go-mizu/mizu" type Product struct { ID string `json:"id"` Name string `json:"name"` Price float64 `json:"price"` } func List() mizu.Handler { products := []Product{ {ID: "1", Name: "Widget", Price: 9.99}, {ID: "2", Name: "Gadget", Price: 19.99}, } return func(c *mizu.Ctx) error { return c.JSON(200, products) } } func Get() mizu.Handler { return func(c *mizu.Ctx) error { id := c.Param("id") // In real app: query database return c.JSON(200, Product{ ID: id, Name: "Widget", Price: 9.99, }) } } ``` ### 3. Register Routes Edit `app/api/routes.go`: ```go theme={null} import "example.com/myapi/feature/products" func (a *App) routes() { // ... existing routes ... a.app.Get("/api/products", products.List()) a.app.Get("/api/products/:id", products.Get()) } ``` ### 4. Test It ```bash theme={null} mizu dev curl http://localhost:8080/api/products curl http://localhost:8080/api/products/1 ``` ## File Naming Conventions | File | Purpose | | ------------ | ------------------------------- | | `http.go` | HTTP handlers | | `service.go` | Business logic (if needed) | | `store.go` | Database operations (if needed) | | `types.go` | Type definitions (if many) | ## Best Practices ### Keep Handlers Thin ```go theme={null} // Good: Handler calls service func Create(svc *ProductService) mizu.Handler { return func(c *mizu.Ctx) error { var input CreateProductInput if err := c.Bind(&input); err != nil { return err } product, err := svc.Create(c.Context(), input) if err != nil { return err } return c.JSON(201, product) } } ``` ### Use Dependency Injection ```go theme={null} // Handler factory accepts dependencies func List(db *sql.DB) mizu.Handler { return func(c *mizu.Ctx) error { // Use db here } } // In routes.go a.app.Get("/api/users", users.List(a.db)) ``` ### Group Related Routes ```go theme={null} func (a *App) routes() { // Public routes a.app.Get("/health", health.Handler()) // API routes a.app.Get("/api/users", users.List()) a.app.Post("/api/users", users.Create()) // Admin routes a.app.Get("/admin/stats", admin.Stats()) } ``` ## Next Steps Build a complete API from scratch See an even more structured approach # Tutorial Source: https://docs.go-mizu.dev/cli/api/tutorial Build a complete REST API from scratch In this tutorial, you'll build a todo list API with full CRUD operations. CRUD stands for Create, Read, Update, Delete - the four basic operations for managing data. This is the foundation of almost every web application, and you'll learn the patterns used in production Mizu APIs. ## What We're Building A REST API with these endpoints: | Method | Path | Description | | ------ | -------------- | ---------------- | | GET | /api/todos | List all todos | | POST | /api/todos | Create a todo | | GET | /api/todos/:id | Get a todo by ID | | PUT | /api/todos/:id | Update a todo | | DELETE | /api/todos/:id | Delete a todo | ## Step 1: Create the Project ```bash theme={null} mizu new todoapi --template api cd todoapi go mod tidy ``` Verify it works: ```bash theme={null} mizu dev # In another terminal: curl http://localhost:8080/health ``` You should see `{"status":"ok"}`. ## Step 2: Create the Todo Feature Create the feature directory: ```bash theme={null} mkdir -p feature/todos ``` Create `feature/todos/http.go`: ```go theme={null} package todos import ( "sync" "github.com/go-mizu/mizu" ) // Todo represents a todo item. type Todo struct { ID string `json:"id"` Title string `json:"title"` Completed bool `json:"completed"` } // Store is an in-memory todo store. type Store struct { mu sync.RWMutex todos map[string]*Todo nextID int } // NewStore creates a new todo store. func NewStore() *Store { return &Store{ todos: make(map[string]*Todo), } } ``` This sets up our data types and a simple in-memory store. ## Step 3: Implement List Handler Add to `feature/todos/http.go`: ```go theme={null} // List returns a handler that lists all todos. func List(store *Store) mizu.Handler { return func(c *mizu.Ctx) error { store.mu.RLock() defer store.mu.RUnlock() todos := make([]*Todo, 0, len(store.todos)) for _, t := range store.todos { todos = append(todos, t) } return c.JSON(200, todos) } } ``` ## Step 4: Implement Create Handler Add to `feature/todos/http.go`: ```go theme={null} import ( "fmt" // ... other imports ) // CreateInput is the input for creating a todo. type CreateInput struct { Title string `json:"title"` } // Create returns a handler that creates a todo. func Create(store *Store) mizu.Handler { return func(c *mizu.Ctx) error { var input CreateInput if err := c.Bind(&input); err != nil { return err } if input.Title == "" { return c.JSON(400, map[string]string{ "error": "title is required", }) } store.mu.Lock() defer store.mu.Unlock() store.nextID++ todo := &Todo{ ID: fmt.Sprintf("%d", store.nextID), Title: input.Title, Completed: false, } store.todos[todo.ID] = todo return c.JSON(201, todo) } } ``` ## Step 5: Implement Get Handler Add to `feature/todos/http.go`: ```go theme={null} // Get returns a handler that gets a todo by ID. func Get(store *Store) mizu.Handler { return func(c *mizu.Ctx) error { id := c.Param("id") store.mu.RLock() defer store.mu.RUnlock() todo, ok := store.todos[id] if !ok { return c.JSON(404, map[string]string{ "error": "todo not found", }) } return c.JSON(200, todo) } } ``` ## Step 6: Implement Update Handler Add to `feature/todos/http.go`: ```go theme={null} // UpdateInput is the input for updating a todo. type UpdateInput struct { Title *string `json:"title"` Completed *bool `json:"completed"` } // Update returns a handler that updates a todo. func Update(store *Store) mizu.Handler { return func(c *mizu.Ctx) error { id := c.Param("id") var input UpdateInput if err := c.Bind(&input); err != nil { return err } store.mu.Lock() defer store.mu.Unlock() todo, ok := store.todos[id] if !ok { return c.JSON(404, map[string]string{ "error": "todo not found", }) } if input.Title != nil { todo.Title = *input.Title } if input.Completed != nil { todo.Completed = *input.Completed } return c.JSON(200, todo) } } ``` ## Step 7: Implement Delete Handler Add to `feature/todos/http.go`: ```go theme={null} // Delete returns a handler that deletes a todo. func Delete(store *Store) mizu.Handler { return func(c *mizu.Ctx) error { id := c.Param("id") store.mu.Lock() defer store.mu.Unlock() if _, ok := store.todos[id]; !ok { return c.JSON(404, map[string]string{ "error": "todo not found", }) } delete(store.todos, id) return c.JSON(200, map[string]string{ "message": "deleted", }) } } ``` ## Step 8: Register Routes Update `app/api/app.go` to include the store: ```go theme={null} package api import ( "github.com/go-mizu/mizu" "example.com/todoapi/feature/todos" ) type App struct { cfg Config app *mizu.App todoStore *todos.Store } func New(cfg Config) *App { a := &App{ cfg: cfg, todoStore: todos.NewStore(), } a.app = mizu.New() a.routes() return a } func (a *App) Listen(addr string) error { return a.app.Listen(addr) } ``` Update `app/api/routes.go`: ```go theme={null} package api import ( "example.com/todoapi/feature/health" "example.com/todoapi/feature/todos" ) func (a *App) routes() { // Health check a.app.Get("/health", health.Handler()) // Todo routes a.app.Get("/api/todos", todos.List(a.todoStore)) a.app.Post("/api/todos", todos.Create(a.todoStore)) a.app.Get("/api/todos/:id", todos.Get(a.todoStore)) a.app.Put("/api/todos/:id", todos.Update(a.todoStore)) a.app.Delete("/api/todos/:id", todos.Delete(a.todoStore)) } ``` ## Step 9: Test the API Start the server: ```bash theme={null} mizu dev ``` In another terminal, test each endpoint: ### List (empty) ```bash theme={null} curl http://localhost:8080/api/todos ``` Output: `[]` ### Create ```bash theme={null} curl -X POST http://localhost:8080/api/todos \ -H "Content-Type: application/json" \ -d '{"title":"Buy groceries"}' ``` Output: ```json theme={null} {"id":"1","title":"Buy groceries","completed":false} ``` ### Create Another ```bash theme={null} curl -X POST http://localhost:8080/api/todos \ -H "Content-Type: application/json" \ -d '{"title":"Walk the dog"}' ``` ### List All ```bash theme={null} curl http://localhost:8080/api/todos ``` Output: ```json theme={null} [{"id":"1","title":"Buy groceries","completed":false},{"id":"2","title":"Walk the dog","completed":false}] ``` ### Get One ```bash theme={null} curl http://localhost:8080/api/todos/1 ``` ### Update ```bash theme={null} curl -X PUT http://localhost:8080/api/todos/1 \ -H "Content-Type: application/json" \ -d '{"completed":true}' ``` Output: ```json theme={null} {"id":"1","title":"Buy groceries","completed":true} ``` ### Delete ```bash theme={null} curl -X DELETE http://localhost:8080/api/todos/2 ``` Output: ```json theme={null} {"message":"deleted"} ``` ## Complete Code Here's the complete `feature/todos/http.go`: ```go theme={null} package todos import ( "fmt" "sync" "github.com/go-mizu/mizu" ) // Todo represents a todo item. type Todo struct { ID string `json:"id"` Title string `json:"title"` Completed bool `json:"completed"` } // Store is an in-memory todo store. type Store struct { mu sync.RWMutex todos map[string]*Todo nextID int } // NewStore creates a new todo store. func NewStore() *Store { return &Store{ todos: make(map[string]*Todo), } } // CreateInput is the input for creating a todo. type CreateInput struct { Title string `json:"title"` } // UpdateInput is the input for updating a todo. type UpdateInput struct { Title *string `json:"title"` Completed *bool `json:"completed"` } // List returns a handler that lists all todos. func List(store *Store) mizu.Handler { return func(c *mizu.Ctx) error { store.mu.RLock() defer store.mu.RUnlock() todos := make([]*Todo, 0, len(store.todos)) for _, t := range store.todos { todos = append(todos, t) } return c.JSON(200, todos) } } // Create returns a handler that creates a todo. func Create(store *Store) mizu.Handler { return func(c *mizu.Ctx) error { var input CreateInput if err := c.Bind(&input); err != nil { return err } if input.Title == "" { return c.JSON(400, map[string]string{"error": "title is required"}) } store.mu.Lock() defer store.mu.Unlock() store.nextID++ todo := &Todo{ ID: fmt.Sprintf("%d", store.nextID), Title: input.Title, Completed: false, } store.todos[todo.ID] = todo return c.JSON(201, todo) } } // Get returns a handler that gets a todo by ID. func Get(store *Store) mizu.Handler { return func(c *mizu.Ctx) error { id := c.Param("id") store.mu.RLock() defer store.mu.RUnlock() todo, ok := store.todos[id] if !ok { return c.JSON(404, map[string]string{"error": "todo not found"}) } return c.JSON(200, todo) } } // Update returns a handler that updates a todo. func Update(store *Store) mizu.Handler { return func(c *mizu.Ctx) error { id := c.Param("id") var input UpdateInput if err := c.Bind(&input); err != nil { return err } store.mu.Lock() defer store.mu.Unlock() todo, ok := store.todos[id] if !ok { return c.JSON(404, map[string]string{"error": "todo not found"}) } if input.Title != nil { todo.Title = *input.Title } if input.Completed != nil { todo.Completed = *input.Completed } return c.JSON(200, todo) } } // Delete returns a handler that deletes a todo. func Delete(store *Store) mizu.Handler { return func(c *mizu.Ctx) error { id := c.Param("id") store.mu.Lock() defer store.mu.Unlock() if _, ok := store.todos[id]; !ok { return c.JSON(404, map[string]string{"error": "todo not found"}) } delete(store.todos, id) return c.JSON(200, map[string]string{"message": "deleted"}) } } ``` ## What You Learned 1. **Feature organization** - Grouping related code together 2. **Handler factories** - Functions that return handlers 3. **Dependency injection** - Passing the store to handlers 4. **CRUD operations** - Create, Read, Update, Delete patterns 5. **Input validation** - Checking required fields 6. **Error responses** - Returning appropriate status codes ## Next Steps Replace in-memory store with a real database Learn about multi-protocol APIs Add logging, auth, and more Improve error responses # Contract Source: https://docs.go-mizu.dev/cli/contract Work with service contracts - list methods, call APIs, and export specs The `mizu contract` command helps you work with service contracts. A contract defines what your API can do - the methods, their inputs, and their outputs. This command lets you discover, explore, call methods, view schemas, and export specifications like OpenAPI directly from the command line. ## What is a Service Contract? A service contract defines the interface of your API: * What methods are available * What data each method accepts * What data each method returns When you build APIs with the `contract` template, your services are automatically discoverable. The `mizu contract` command lets you explore and interact with these APIs from the command line. ## Quick Examples ```bash theme={null} # List all available methods mizu contract ls # Show details about a specific method mizu contract show todo.Create # Call a method with JSON data mizu contract call todo.Create '{"title":"Buy milk"}' # Export the OpenAPI specification mizu contract spec > openapi.json ``` ## Subcommands | Command | Description | | --------------------- | -------------------------------- | | `mizu contract ls` | List all services and methods | | `mizu contract show` | Show details about a method | | `mizu contract call` | Call a method | | `mizu contract spec` | Export API specification | | `mizu contract types` | List available types and schemas | ## Setting the Server URL By default, commands connect to `http://localhost:8080`. You can change this in several ways: ```bash theme={null} # Pass as argument mizu contract ls http://api.example.com # Use the --url flag mizu contract ls --url http://api.example.com # Use environment variable export MIZU_URL=http://api.example.com mizu contract ls ``` Priority order: command argument > `--url` flag > `MIZU_URL` environment variable > default. *** ## mizu contract ls Lists all services and their methods. ### Usage ```bash theme={null} mizu contract ls [url] [flags] ``` ### Flags | Flag | Description | | ---------------- | -------------------------- | | `--url ` | Server URL | | `--all` | Include deprecated methods | | `--json` | Output as JSON | ### Example ```bash theme={null} mizu contract ls ``` Output: ``` todo todo.Create POST /api/todo Create a new todo item todo.List GET /api/todo List all todo items todo.Get GET /api/todo/{id} Get a todo by ID todo.Update PUT /api/todo/{id} Update a todo item todo.Delete DELETE /api/todo/{id} Delete a todo item ``` ### JSON Output ```bash theme={null} mizu contract ls --json ``` ```json theme={null} { "services": [{ "name": "todo", "methods": [{ "name": "Create", "fullName": "todo.Create", "httpMethod": "POST", "httpPath": "/api/todo", "input": {"id": "CreateTodoInput", "name": "CreateTodoInput"}, "output": {"id": "Todo", "name": "Todo"} }] }], "types": [...] } ``` *** ## mizu contract show Shows detailed information about a specific method. ### Usage ```bash theme={null} mizu contract show [url] [flags] ``` ### Flags | Flag | Description | | ---------------- | --------------------- | | `--url ` | Server URL | | `--schema` | Show full JSON schema | | `--json` | Output as JSON | ### Example ```bash theme={null} mizu contract show todo.Create ``` Output: ``` todo.Create POST /api/todo Create a new todo item Input (CreateTodoInput): title string required The title of the todo item description string Optional description priority integer Priority level (1-5) Output (Todo): id string required Unique identifier title string required Todo title description string Description completed boolean required Completion status createdAt string required Creation timestamp ``` ### Show Full Schema ```bash theme={null} mizu contract show todo.Create --schema ``` Outputs the complete JSON Schema for input and output types. *** ## mizu contract call Calls a method with input data and returns the result. ### Usage ```bash theme={null} mizu contract call [input] [url] [flags] ``` ### Input Formats | Format | Example | Description | | ----------- | ------------------------ | --------------- | | JSON string | `'{"title":"Buy milk"}'` | Inline JSON | | File | `@input.json` | Read from file | | Stdin | `-` | Read from stdin | ### Flags | Flag | Description | | -------------------------- | ------------------------------- | | `--url ` | Server URL | | `--id ` | Path parameter ID (for REST) | | `--timeout ` | Request timeout (default: 30s) | | `-H, --header ` | Add request header (repeatable) | | `--raw` | Output raw response | ### Examples **Call with inline JSON:** ```bash theme={null} mizu contract call todo.Create '{"title":"Buy milk","priority":1}' ``` Output: ```json theme={null} { "id": "abc123", "title": "Buy milk", "priority": 1, "completed": false, "createdAt": "2024-01-15T10:30:00Z" } ``` **Call with file input:** ```bash theme={null} # Create input file echo '{"title":"Buy groceries"}' > input.json # Call with file mizu contract call todo.Create @input.json ``` **Call with stdin:** ```bash theme={null} echo '{"title":"Buy groceries"}' | mizu contract call todo.Create - ``` **Call a method that takes an ID:** ```bash theme={null} mizu contract call todo.Get --id abc123 ``` **Add authentication header:** ```bash theme={null} mizu contract call todo.Create '{"title":"Secret todo"}' \ -H "Authorization: Bearer mytoken" ``` ### Error Output If the call fails: ``` error: rpc error -32602: title is required ``` *** ## mizu contract spec Exports the API specification in OpenAPI or OpenRPC format. ### Usage ```bash theme={null} mizu contract spec [url] [flags] ``` ### Flags | Flag | Description | | --------------------- | -------------------------------- | | `--url ` | Server URL | | `--format ` | Output format (openapi, openrpc) | | `--service ` | Export specific service only | | `--pretty` | Pretty print JSON | | `-o, --output ` | Write to file | ### Examples **Export OpenAPI spec:** ```bash theme={null} mizu contract spec > openapi.json ``` **Export pretty-printed:** ```bash theme={null} mizu contract spec --pretty > openapi.json ``` **Export to file:** ```bash theme={null} mizu contract spec -o openapi.json ``` Output: ``` Wrote openapi.json ``` **Export OpenRPC format:** ```bash theme={null} mizu contract spec --format openrpc > openrpc.json ``` *** ## mizu contract types Lists all types and their schemas. ### Usage ```bash theme={null} mizu contract types [type] [url] [flags] ``` ### Flags | Flag | Description | | ---------------- | --------------------- | | `--url ` | Server URL | | `--schema` | Show full JSON schema | | `--json` | Output as JSON | ### List All Types ```bash theme={null} mizu contract types ``` Output: ``` CreateTodoInput object 3 fields Todo object 5 fields UpdateTodoInput object 4 fields Priority enum 5 values ``` ### Show Specific Type ```bash theme={null} mizu contract types Todo ``` Output: ``` Todo (object) id string required title string required description string completed boolean required createdAt string required ``` ### Show Full Schema ```bash theme={null} mizu contract types Todo --schema ``` ```json theme={null} { "type": "object", "required": ["id", "title", "completed", "createdAt"], "properties": { "id": {"type": "string", "description": "Unique identifier"}, "title": {"type": "string", "description": "Todo title"}, "description": {"type": "string"}, "completed": {"type": "boolean"}, "createdAt": {"type": "string", "format": "date-time"} } } ``` *** ## Discovery Methods The CLI tries multiple methods to discover your API: 1. **JSON-RPC Discovery** - Calls `rpc.discover` method 2. **OpenRPC Spec** - Fetches `/openrpc.json` 3. **OpenAPI Spec** - Fetches `/openapi.json` 4. **Contract Endpoint** - Fetches `/_contract` It uses the first method that succeeds. *** ## Practical Workflows ### Explore a New API ```bash theme={null} # Start by listing what's available mizu contract ls # Pick a method and see its details mizu contract show users.Create # Try calling it mizu contract call users.Create '{"name":"Alice","email":"alice@example.com"}' ``` ### Generate Client Documentation ```bash theme={null} # Export the spec mizu contract spec --pretty -o api.json # Use tools like Swagger UI or Redoc to visualize npx @redocly/cli preview-docs api.json ``` ### Test During Development ```bash theme={null} # In terminal 1: Run your server mizu dev # In terminal 2: Test your methods mizu contract call todo.Create '{"title":"Test item"}' mizu contract call todo.List ``` ### CI/CD Integration ```bash theme={null} #!/bin/bash # Verify API is working # Wait for server to start sleep 5 # Check discovery works if ! mizu contract ls --json > /dev/null; then echo "API discovery failed" exit 1 fi # Check a specific method RESULT=$(mizu contract call health.Check) if [ $? -ne 0 ]; then echo "Health check failed" exit 1 fi echo "API is healthy" ``` *** ## Error Messages ### Cannot Discover Contract ``` error: cannot discover contract at http://localhost:8080 ``` **Causes:** * Server is not running * Server doesn't expose discovery endpoints * Wrong URL **Fix:** 1. Make sure your server is running (`mizu dev`) 2. Verify the URL is correct 3. Check that you're using the `contract` template ### Method Not Found ``` error: method not found: todo.Unknown did you mean? todo.Create todo.Update ``` **Fix:** Check the method name. Use `mizu contract ls` to see available methods. ### Missing Required Input ``` error: missing required input the todo.Create method requires input hint: mizu contract call todo.Create '{...}' mizu contract show todo.Create # see input schema ``` **Fix:** Provide the required input data. *** ## Exit Codes | Code | Meaning | | ---- | -------------------------------- | | 0 | Success | | 1 | Error (API error, network error) | | 2 | Usage error (missing arguments) | *** ## Next Steps Learn how to build contract-based services Learn about OpenAPI spec generation # Overview Source: https://docs.go-mizu.dev/cli/contract-template/overview Multi-protocol service with REST, JSON-RPC, and OpenAPI The contract template creates a service based on transport-neutral contracts. A "contract" in this context means a formal definition of what your API can do - like a menu at a restaurant. You define the methods once as Go code, and the framework automatically creates multiple ways to call them (REST, JSON-RPC) plus generates documentation (OpenAPI). ## When to Use This Template Use the contract template when: * **Multiple protocols needed** - REST for web clients, JSON-RPC for internal services * **API-first design** - Define contracts before implementation * **Auto-generated docs** - OpenAPI specs from your code * **Clean architecture** - Business logic separate from transport ## What You Get A complete service with: * **Service contracts** - Plain Go structs with typed methods * **REST transport** - RESTful HTTP endpoints * **JSON-RPC transport** - JSON-RPC 2.0 endpoint * **OpenAPI spec** - Auto-generated at `/openapi.json` * **Discovery endpoint** - List available methods ## Quick Start ```bash theme={null} # Create the project mizu new myservice --template contract # Enter the project cd myservice # Install dependencies go mod tidy # Run the server mizu dev ``` Test the different protocols: ```bash theme={null} # REST curl http://localhost:8080/api/todo # JSON-RPC curl -X POST http://localhost:8080 \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"todo.List","id":1}' # OpenAPI spec curl http://localhost:8080/openapi.json # Discovery mizu contract ls ``` ## How Contracts Work ### 1. Define a Service ```go theme={null} // service/todo/todo.go type Service struct { // dependencies here } type Todo struct { ID string `json:"id"` Title string `json:"title"` Completed bool `json:"completed"` } type CreateInput struct { Title string `json:"title"` } func (s *Service) Create(ctx context.Context, in *CreateInput) (*Todo, error) { // Implementation } func (s *Service) List(ctx context.Context) ([]*Todo, error) { // Implementation } ``` ### 2. Register with Contract System ```go theme={null} // In app setup registry := contract.NewRegistry() registry.Register("todo", &todo.Service{}) ``` ### 3. Mount Transports ```go theme={null} // REST transport rest := contract.NewREST(registry) app.Mount("/api", rest.Handler()) // JSON-RPC transport rpc := contract.NewJSONRPC(registry) app.Post("/", rpc.Handler()) // OpenAPI openapi := contract.NewOpenAPI(registry) app.Get("/openapi.json", openapi.Handler()) ``` ## Benefits ### Write Once, Expose Multiple Ways Your business logic is in plain Go methods: ```go theme={null} func (s *Service) Create(ctx context.Context, in *CreateInput) (*Todo, error) { // This same code handles REST and JSON-RPC } ``` Both transports call the same method: ```bash theme={null} # REST: POST /api/todo # JSON-RPC: {"method": "todo.Create", ...} ``` ### Automatic Documentation OpenAPI spec is generated from your Go types: ```go theme={null} type CreateInput struct { Title string `json:"title" doc:"The title of the todo"` } ``` Generates: ```json theme={null} { "CreateInput": { "type": "object", "properties": { "title": { "type": "string", "description": "The title of the todo" } } } } ``` ### Type Safety Everything is strongly typed: ```go theme={null} // Input and output types are checked at compile time func (s *Service) Create(ctx context.Context, in *CreateInput) (*Todo, error) ``` ### Discoverable Clients can discover available methods: ```bash theme={null} mizu contract ls ``` ``` todo todo.Create POST /api/todo Create a new todo todo.List GET /api/todo List all todos todo.Get GET /api/todo/{id} Get a todo by ID ``` ## Project Structure ``` myservice/ ├── cmd/ │ └── api/ │ └── main.go # Entry point ├── app/ │ └── server/ │ ├── config.go # Configuration │ └── server.go # Server setup ├── service/ │ └── todo/ │ └── todo.go # Todo service contract ├── go.mod └── .gitignore ``` ## Available Transports | Transport | Endpoint | Use Case | | --------- | --------------- | ------------------------------ | | REST | `/api/*` | Web clients, mobile apps | | JSON-RPC | `/` (POST) | Internal services, RPC clients | | OpenAPI | `/openapi.json` | Documentation, code generation | | OpenRPC | `/openrpc.json` | JSON-RPC documentation | ## CLI Integration Use `mizu contract` to interact with your service: ```bash theme={null} # List all methods mizu contract ls # Show method details mizu contract show todo.Create # Call a method mizu contract call todo.Create '{"title":"Buy milk"}' # Export OpenAPI spec mizu contract spec > openapi.json ``` ## When to Use Contract vs API Template | Aspect | API Template | Contract Template | | ------------- | ------------- | ----------------------- | | Protocols | REST only | REST + JSON-RPC | | Documentation | Manual | Auto-generated | | Type safety | At boundaries | Throughout | | Complexity | Lower | Higher | | Best for | Simple APIs | Multi-protocol services | ## Next Steps Understand every file in detail Build a contract-based service # Structure Source: https://docs.go-mizu.dev/cli/contract-template/structure Understanding the contract template file organization The contract template separates your business logic from transport concerns. "Transport" means how data travels - HTTP, WebSocket, or JSON-RPC. By separating these, you write your core logic once and expose it through multiple protocols automatically. ## Directory Layout ``` myservice/ ├── cmd/ │ └── api/ │ └── main.go # Entry point ├── app/ │ └── server/ │ ├── config.go # Configuration │ └── server.go # Server setup with transports ├── service/ │ └── todo/ │ └── todo.go # Todo service (business logic) ├── go.mod ├── .gitignore └── README.md ``` ## The service/ Directory This is where your business logic lives. Services are plain Go structs. ### service/todo/todo.go ```go theme={null} package todo import ( "context" "fmt" "sync" ) // Service is the todo service. type Service struct { mu sync.RWMutex todos map[string]*Todo nextID int } // New creates a new todo service. func New() *Service { return &Service{ todos: make(map[string]*Todo), } } // Todo is a todo item. type Todo struct { ID string `json:"id"` Title string `json:"title"` Completed bool `json:"completed"` } // CreateInput is the input for creating a todo. type CreateInput struct { Title string `json:"title"` } // Create creates a new todo. func (s *Service) Create(ctx context.Context, in *CreateInput) (*Todo, error) { 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 } // List returns all todos. func (s *Service) List(ctx context.Context) ([]*Todo, error) { s.mu.RLock() defer s.mu.RUnlock() todos := make([]*Todo, 0, len(s.todos)) for _, t := range s.todos { todos = append(todos, t) } return todos, nil } ``` **Key points:** * Pure Go - no HTTP or transport concerns * Context as first parameter * Strongly typed inputs and outputs * Returns errors for proper handling ### Method Signatures The contract system recognizes these patterns: ```go theme={null} // With input and output func (s *Service) Method(ctx context.Context, in *Input) (*Output, error) // Output only (no input) func (s *Service) Method(ctx context.Context) (*Output, error) // Input only (no output) func (s *Service) Method(ctx context.Context, in *Input) error // No input or output func (s *Service) Method(ctx context.Context) error ``` ## The app/ Directory Sets up the server and mounts transports. ### app/server/server.go ```go theme={null} package server import ( "github.com/go-mizu/mizu" "github.com/go-mizu/mizu/contract" "example.com/myservice/service/todo" ) type Server struct { cfg Config app *mizu.App registry *contract.Registry } func New(cfg Config) *Server { s := &Server{cfg: cfg} s.app = mizu.New() s.registry = contract.NewRegistry() // Register services s.registry.Register("todo", todo.New()) // Setup transports s.setupTransports() return s } func (s *Server) setupTransports() { // REST transport at /api/* rest := contract.NewREST(s.registry) s.app.Mount("/api", rest.Handler()) // JSON-RPC at root rpc := contract.NewJSONRPC(s.registry) s.app.Post("/", rpc.Handler()) // OpenAPI spec openapi := contract.NewOpenAPI(s.registry, contract.OpenAPIOptions{ Title: "My Service", Version: "1.0.0", }) s.app.Get("/openapi.json", openapi.Handler()) } func (s *Server) Listen(addr string) error { return s.app.Listen(addr) } ``` **What happens:** 1. **Registry** - Collects all services 2. **Register** - Adds a service under a namespace ("todo") 3. **REST transport** - Maps methods to HTTP endpoints 4. **JSON-RPC transport** - Handles JSON-RPC calls 5. **OpenAPI** - Generates documentation ### app/server/config.go ```go theme={null} package server import "os" type Config struct { Addr string Dev bool } func LoadConfig() Config { return Config{ Addr: getEnv("ADDR", ":8080"), Dev: getEnv("DEV", "true") == "true", } } func getEnv(key, defaultValue string) string { if v := os.Getenv(key); v != "" { return v } return defaultValue } ``` ## The cmd/ Directory Entry point for your service. ### cmd/api/main.go ```go theme={null} package main import ( "log" "example.com/myservice/app/server" ) func main() { cfg := server.LoadConfig() srv := server.New(cfg) log.Printf("listening on %s", cfg.Addr) if err := srv.Listen(cfg.Addr); err != nil { log.Fatal(err) } } ``` ## How Transports Map Methods ### REST Mapping Methods are mapped to HTTP endpoints automatically: | Method | HTTP | Path | | ------------- | ------ | ---------- | | `todo.Create` | POST | /api/todo | | `todo.List` | GET | /api/todo | | `todo.Get` | GET | /api/todo/ | | `todo.Update` | PUT | /api/todo/ | | `todo.Delete` | DELETE | /api/todo/ | The mapping follows conventions: * `Create` → POST without ID * `List` → GET without ID * `Get` → GET with ID * `Update` → PUT with ID * `Delete` → DELETE with ID ### JSON-RPC Mapping All methods are available via JSON-RPC: ```json theme={null} { "jsonrpc": "2.0", "method": "todo.Create", "params": {"title": "Buy milk"}, "id": 1 } ``` Response: ```json theme={null} { "jsonrpc": "2.0", "result": {"id": "1", "title": "Buy milk", "completed": false}, "id": 1 } ``` ## Adding a New Service ### 1. Create the Service ```bash theme={null} mkdir -p service/users ``` Create `service/users/users.go`: ```go theme={null} package users import "context" type Service struct { // dependencies } func New() *Service { return &Service{} } type User struct { ID string `json:"id"` Name string `json:"name"` Email string `json:"email"` } type CreateInput struct { Name string `json:"name"` Email string `json:"email"` } func (s *Service) Create(ctx context.Context, in *CreateInput) (*User, error) { return &User{ ID: "1", Name: in.Name, Email: in.Email, }, nil } func (s *Service) List(ctx context.Context) ([]*User, error) { return []*User{}, nil } ``` ### 2. Register the Service Update `app/server/server.go`: ```go theme={null} import "example.com/myservice/service/users" func New(cfg Config) *Server { // ... s.registry.Register("todo", todo.New()) s.registry.Register("users", users.New()) // Add this // ... } ``` ### 3. Test It ```bash theme={null} mizu dev # REST curl http://localhost:8080/api/users # JSON-RPC curl -X POST http://localhost:8080 \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"users.List","id":1}' # CLI mizu contract ls mizu contract call users.Create '{"name":"Alice","email":"alice@example.com"}' ``` ## Type Documentation Add documentation with struct tags: ```go theme={null} type Todo struct { ID string `json:"id" doc:"Unique identifier"` Title string `json:"title" doc:"The todo title"` Completed bool `json:"completed" doc:"Whether the todo is done"` } ``` This appears in the generated OpenAPI spec. ## Next Steps Build a complete contract service Deep dive into contracts # Tutorial Source: https://docs.go-mizu.dev/cli/contract-template/tutorial Build a multi-protocol API service In this tutorial, you'll build a notes service that exposes both REST and JSON-RPC endpoints, with automatic OpenAPI documentation. The magic of contracts is that you write your business logic once, and it works through multiple protocols automatically - no duplicate code. ## What We're Building A notes API with: * Create, list, get, update, delete operations * REST endpoints at `/api/notes/*` * JSON-RPC endpoint at `/` * OpenAPI spec at `/openapi.json` ## Step 1: Create the Project ```bash theme={null} mizu new notes --template contract cd notes go mod tidy ``` Verify it works: ```bash theme={null} mizu dev ``` ```bash theme={null} # Test the default todo service curl http://localhost:8080/api/todo ``` ## Step 2: Create the Notes Service Create the service directory: ```bash theme={null} mkdir -p service/notes ``` Create `service/notes/notes.go`: ```go theme={null} package notes import ( "context" "fmt" "sync" "time" ) // Service is the notes service. type Service struct { mu sync.RWMutex notes map[string]*Note nextID int } // New creates a new notes service. func New() *Service { return &Service{ notes: make(map[string]*Note), } } // Note represents a note. type Note struct { ID string `json:"id"` Title string `json:"title"` Content string `json:"content"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } // CreateInput is the input for creating a note. type CreateInput struct { Title string `json:"title"` Content string `json:"content"` } // UpdateInput is the input for updating a note. type UpdateInput struct { ID string `json:"id"` Title *string `json:"title,omitempty"` Content *string `json:"content,omitempty"` } // GetInput is the input for getting a note. type GetInput struct { ID string `json:"id"` } // DeleteInput is the input for deleting a note. type DeleteInput struct { ID string `json:"id"` } // DeleteOutput is the output for deleting a note. type DeleteOutput struct { Success bool `json:"success"` } ``` ## Step 3: Implement the Methods Add the methods to `service/notes/notes.go`: ```go theme={null} // Create creates a new note. func (s *Service) Create(ctx context.Context, in *CreateInput) (*Note, error) { if in.Title == "" { return nil, fmt.Errorf("title is required") } s.mu.Lock() defer s.mu.Unlock() s.nextID++ now := time.Now() note := &Note{ ID: fmt.Sprintf("%d", s.nextID), Title: in.Title, Content: in.Content, CreatedAt: now, UpdatedAt: now, } s.notes[note.ID] = note return note, nil } // List returns all notes. func (s *Service) List(ctx context.Context) ([]*Note, error) { s.mu.RLock() defer s.mu.RUnlock() notes := make([]*Note, 0, len(s.notes)) for _, n := range s.notes { notes = append(notes, n) } return notes, nil } // Get returns a note by ID. func (s *Service) Get(ctx context.Context, in *GetInput) (*Note, error) { s.mu.RLock() defer s.mu.RUnlock() note, ok := s.notes[in.ID] if !ok { return nil, fmt.Errorf("note not found: %s", in.ID) } return note, nil } // Update updates a note. func (s *Service) Update(ctx context.Context, in *UpdateInput) (*Note, error) { s.mu.Lock() defer s.mu.Unlock() note, ok := s.notes[in.ID] if !ok { return nil, fmt.Errorf("note not found: %s", in.ID) } if in.Title != nil { note.Title = *in.Title } if in.Content != nil { note.Content = *in.Content } note.UpdatedAt = time.Now() return note, nil } // Delete deletes a note. func (s *Service) Delete(ctx context.Context, in *DeleteInput) (*DeleteOutput, error) { s.mu.Lock() defer s.mu.Unlock() if _, ok := s.notes[in.ID]; !ok { return nil, fmt.Errorf("note not found: %s", in.ID) } delete(s.notes, in.ID) return &DeleteOutput{Success: true}, nil } ``` ## Step 4: Register the Service Update `app/server/server.go`: ```go theme={null} package server import ( "github.com/go-mizu/mizu" "github.com/go-mizu/mizu/contract" "example.com/notes/service/notes" // Add this "example.com/notes/service/todo" ) type Server struct { cfg Config app *mizu.App registry *contract.Registry } func New(cfg Config) *Server { s := &Server{cfg: cfg} s.app = mizu.New() s.registry = contract.NewRegistry() // Register services s.registry.Register("todo", todo.New()) s.registry.Register("notes", notes.New()) // Add this s.setupTransports() return s } func (s *Server) setupTransports() { // REST transport rest := contract.NewREST(s.registry) s.app.Mount("/api", rest.Handler()) // JSON-RPC transport rpc := contract.NewJSONRPC(s.registry) s.app.Post("/", rpc.Handler()) // OpenAPI spec openapi := contract.NewOpenAPI(s.registry, contract.OpenAPIOptions{ Title: "Notes API", Description: "A simple notes service", Version: "1.0.0", }) s.app.Get("/openapi.json", openapi.Handler()) } func (s *Server) Listen(addr string) error { return s.app.Listen(addr) } ``` ## Step 5: Test the API Restart the server: ```bash theme={null} mizu dev ``` ### Test via REST ```bash theme={null} # Create a note curl -X POST http://localhost:8080/api/notes \ -H "Content-Type: application/json" \ -d '{"title":"My First Note","content":"Hello world!"}' ``` ```json theme={null} {"id":"1","title":"My First Note","content":"Hello world!","createdAt":"2024-01-15T10:30:00Z","updatedAt":"2024-01-15T10:30:00Z"} ``` ```bash theme={null} # List notes curl http://localhost:8080/api/notes ``` ```bash theme={null} # Get a note curl http://localhost:8080/api/notes/1 ``` ```bash theme={null} # Update a note curl -X PUT http://localhost:8080/api/notes/1 \ -H "Content-Type: application/json" \ -d '{"title":"Updated Title"}' ``` ```bash theme={null} # Delete a note curl -X DELETE http://localhost:8080/api/notes/1 ``` ### Test via JSON-RPC ```bash theme={null} # Create via JSON-RPC curl -X POST http://localhost:8080 \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "notes.Create", "params": {"title": "RPC Note", "content": "Created via JSON-RPC"}, "id": 1 }' ``` ```json theme={null} {"jsonrpc":"2.0","result":{"id":"2","title":"RPC Note","content":"Created via JSON-RPC","createdAt":"...","updatedAt":"..."},"id":1} ``` ```bash theme={null} # List via JSON-RPC curl -X POST http://localhost:8080 \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"notes.List","id":2}' ``` ### Test via CLI ```bash theme={null} # List available methods mizu contract ls # Show method details mizu contract show notes.Create # Call a method mizu contract call notes.Create '{"title":"CLI Note","content":"From CLI"}' # List all notes mizu contract call notes.List ``` ### View OpenAPI Spec ```bash theme={null} curl http://localhost:8080/openapi.json | jq . ``` Or open `http://localhost:8080/openapi.json` in your browser. ## Step 6: Add Search Method Let's add a search capability. Add to `service/notes/notes.go`: ```go theme={null} // SearchInput is the input for searching notes. type SearchInput struct { Query string `json:"query"` } // Search searches notes by title or content. func (s *Service) Search(ctx context.Context, in *SearchInput) ([]*Note, error) { s.mu.RLock() defer s.mu.RUnlock() if in.Query == "" { return []*Note{}, nil } query := strings.ToLower(in.Query) var results []*Note for _, n := range s.notes { if strings.Contains(strings.ToLower(n.Title), query) || strings.Contains(strings.ToLower(n.Content), query) { results = append(results, n) } } return results, nil } ``` Add the import at the top: ```go theme={null} import ( "context" "fmt" "strings" // Add this "sync" "time" ) ``` Test it: ```bash theme={null} # Via REST (custom method becomes POST) curl -X POST http://localhost:8080/api/notes/search \ -H "Content-Type: application/json" \ -d '{"query":"hello"}' # Via JSON-RPC curl -X POST http://localhost:8080 \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"notes.Search","params":{"query":"hello"},"id":1}' # Via CLI mizu contract call notes.Search '{"query":"hello"}' ``` ## What You Learned 1. **Service contracts** - Define business logic as Go methods 2. **Transport-neutral** - Same code works for REST and JSON-RPC 3. **Auto-discovery** - Methods are automatically exposed 4. **OpenAPI generation** - Documentation from your code 5. **CLI integration** - Test services from command line ## Key Takeaways * **Separation of concerns** - Business logic in services, transport in setup * **Type safety** - Strongly typed inputs and outputs * **Consistency** - Same API via multiple protocols * **Discoverability** - Clients can explore your API ## Next Steps Learn more about the contract system Build server-rendered web apps # Dev Source: https://docs.go-mizu.dev/cli/dev Run your Mizu project in development mode The `mizu dev` command runs your Mizu application in development mode. It automatically finds your main package, compiles your code, starts your server, and handles shutdown gracefully when you press Ctrl+C. This is the command you'll use most during development. ## Basic Usage ```bash theme={null} mizu dev ``` That's it! The command auto-detects your main package and runs it. ## What Happens When You Run `mizu dev` 1. **Finds your main package** - Looks in `cmd/` folder first, then checks the root directory 2. **Compiles your code** - Uses `go run` to build and run 3. **Starts your server** - Your application starts running 4. **Waits for shutdown** - Listens for Ctrl+C or system signals 5. **Shuts down gracefully** - Gives your app time to clean up before exiting Example output: ``` Starting ./cmd/api... ``` Your server is now running! Open your browser to `http://localhost:8080` to see it. ## How to Stop the Server Press `Ctrl+C` in your terminal. You'll see: ``` Received interrupt, shutting down... ``` The CLI sends a signal to your application, giving it time to: * Close database connections * Finish processing requests * Save any pending data If your app doesn't stop within 15 seconds, it's force-terminated. ## Flags Reference | Flag | Description | | -------------- | --------------------------------- | | `--cmd ` | Specify which main package to run | | `--json` | Output lifecycle events as JSON | | `-h, --help` | Show help | ## How Main Package Discovery Works The CLI finds your main package automatically. Here's the order it checks: ### 1. If You Specify `--cmd` ```bash theme={null} mizu dev --cmd ./cmd/server ``` Uses exactly what you specify. No auto-detection. ### 2. Look in `cmd/` Directory If you have a `cmd/` folder, the CLI looks for `main.go` files inside subdirectories: ``` myapp/ ├── cmd/ │ └── api/ │ └── main.go ← Found! Uses ./cmd/api └── go.mod ``` ### 3. Check Root Directory If there's no `cmd/` folder, it checks the current directory: ``` myapp/ ├── main.go ← Found! Uses . └── go.mod ``` ### When Discovery Fails If no main package is found: ``` error: no runnable main package found (try --cmd) ``` **Fix:** Either: * Create a `cmd//main.go` file * Create a `main.go` in your root directory * Use `--cmd` to point to your main package ## Working with Multiple Commands If your project has multiple entry points: ``` myapp/ ├── cmd/ │ ├── api/ │ │ └── main.go # The API server │ └── worker/ │ └── main.go # A background worker └── go.mod ``` You need to specify which one to run: ```bash theme={null} # Run the API server mizu dev --cmd ./cmd/api # Run the worker mizu dev --cmd ./cmd/worker ``` ## Passing Arguments to Your Application Use `--` to separate CLI flags from your application's arguments: ```bash theme={null} mizu dev -- --port 9000 --debug ``` Everything after `--` is passed to your application. Your app will receive `--port 9000 --debug` as arguments. ## JSON Output Mode For scripts and automation, use `--json` to get machine-readable output: ```bash theme={null} mizu dev --json ``` This outputs one JSON object per line (NDJSON format): ```json theme={null} {"event":"starting","timestamp":"2024-01-15T10:30:00Z","message":"running ./cmd/api"} {"event":"started","timestamp":"2024-01-15T10:30:01Z","message":"pid 12345"} ``` ### Lifecycle Events | Event | When it happens | | ---------- | ----------------------------------------- | | `starting` | About to start your application | | `started` | Application started (includes process ID) | | `signal` | Received a shutdown signal (Ctrl+C) | | `timeout` | Graceful shutdown took too long | | `stopped` | Application exited | | `error` | Something went wrong | ### Event Structure ```json theme={null} { "event": "started", "timestamp": "2024-01-15T10:30:01Z", "message": "pid 12345", "exit_code": 0 } ``` * `event` - Type of event * `timestamp` - When it happened (UTC, ISO 8601 format) * `message` - Human-readable description * `exit_code` - Only included when application exits ## Verbose Mode See more details about what's happening: ```bash theme={null} mizu dev -v ``` Output: ``` discovered main package: ./cmd/api Starting ./cmd/api... ``` ## Error Messages and Solutions ### No Main Package Found ``` error: no runnable main package found (try --cmd) ``` **Why:** The CLI couldn't find a `package main` in your project. **Fix:** 1. Make sure you're in the right directory 2. Check that your `main.go` file has `package main` at the top 3. Use `--cmd` to specify the path manually ### Path Does Not Exist ``` error: path does not exist: ./cmd/missing ``` **Why:** You specified a `--cmd` path that doesn't exist. **Fix:** Check the path and make sure it's correct. ### Build Errors If your Go code has errors, you'll see the compiler output: ``` Starting ./cmd/api... # github.com/example/myapp/cmd/api ./main.go:10:5: undefined: mizu ``` **Fix:** Fix the Go compilation errors shown. ## Exit Codes The exit code from your application passes through `mizu dev`: | Code | Meaning | | ----- | ------------------------------------ | | 0 | Success - your app exited cleanly | | 1 | Error during startup | | 2 | Invalid flags or arguments | | 3 | No main package found | | Other | Passed through from your application | If your app exits with code 5, `mizu dev` also exits with code 5. This is important for scripts that check exit codes. ## Development Workflow A typical development workflow: ```bash theme={null} # Terminal 1: Run the server cd myapp mizu dev # Terminal 2: Test your API curl http://localhost:8080/health # When you make changes: # 1. Press Ctrl+C in Terminal 1 # 2. Run: mizu dev again ``` `mizu dev` does not watch for file changes. When you modify your code, stop the server with Ctrl+C and restart it. ## Recommended Project Structure For the best experience with `mizu dev`, use this structure: ``` myapp/ ├── cmd/ │ └── api/ │ └── main.go # Entry point ├── app/ │ └── api/ │ ├── app.go # Application setup │ ├── config.go # Configuration │ └── routes.go # Route definitions ├── feature/ │ └── users/ │ └── http.go # Feature handlers ├── go.mod └── go.sum ``` With this structure: * `mizu dev` finds `./cmd/api` automatically * Code is organized by feature * Clear separation of concerns ## Using with Scripts Check if the server started successfully: ```bash theme={null} #!/bin/bash # Start server in background, capture output mizu dev --json & SERVER_PID=$! # Wait for started event sleep 2 # Check if process is still running if ps -p $SERVER_PID > /dev/null; then echo "Server started successfully" else echo "Server failed to start" exit 1 fi # Run your tests here... # Clean up kill $SERVER_PID ``` ## Common Questions ### Why doesn't `mizu dev` watch for file changes? File watching adds complexity and dependencies. For now, just restart manually. Many editors have built-in run commands that make this easy. ### Can I run multiple servers at once? Yes! Open multiple terminals: ```bash theme={null} # Terminal 1 mizu dev --cmd ./cmd/api # Terminal 2 mizu dev --cmd ./cmd/worker ``` ### How do I change the port? Configure the port in your application code or use arguments: ```bash theme={null} mizu dev -- --port 3000 ``` (Your application needs to handle the `--port` flag) ## Next Steps Start a new project with mizu new See what templates are available # Exit Codes Source: https://docs.go-mizu.dev/cli/exit-codes CLI exit codes for scripting and automation The Mizu CLI uses specific exit codes to indicate different outcomes. Exit codes are numbers returned when a command finishes - 0 means success, anything else means something went wrong. These codes are essential for shell scripts and CI/CD pipelines that need to know if a command succeeded. ## Exit Code Reference | Code | Name | Description | | ---- | ----------- | ------------------------------------------ | | 0 | `OK` | Success - operation completed successfully | | 1 | `Error` | General error - something went wrong | | 2 | `Usage` | Usage error - invalid flags or arguments | | 3 | `NoProject` | No project - couldn't find main package | ## Detailed Descriptions ### Exit Code 0: Success The operation completed successfully. ```bash theme={null} mizu new myapp --template minimal echo $? # 0 mizu version echo $? # 0 ``` ### Exit Code 1: General Error An error occurred during execution. Examples: * File write failed * Template rendering error * Process failed to start * Network error ```bash theme={null} # Template error (corrupted template) mizu new myapp --template broken echo $? # 1 # Write error (permission denied) mizu new /root/myapp --template minimal echo $? # 1 ``` ### Exit Code 2: Usage Error Invalid command usage. Examples: * Unknown command * Missing required flag * Invalid flag value * Invalid arguments ```bash theme={null} # Unknown command mizu unknown echo $? # 2 # Missing required template mizu new myapp echo $? # 2 # Unknown template mizu new myapp --template nonexistent echo $? # 2 ``` ### Exit Code 3: No Project Found Specific to `mizu dev` - couldn't find a runnable main package. ```bash theme={null} # In empty directory cd /tmp/empty mizu dev # error: no runnable main package found (try --cmd) echo $? # 3 ``` ## Command-Specific Exit Codes ### `mizu new` | Code | Situation | | ---- | ---------------------------------------------------- | | 0 | Project created successfully | | 1 | File write error, template render error | | 2 | Missing `--template`, unknown template, invalid path | ### `mizu dev` | Code | Situation | | ---- | ------------------------------------------------ | | 0 | Process exited cleanly | | 1 | Process failed to start, timeout during shutdown | | 2 | Invalid flags | | 3 | No main package found | | N | Exit code from your application | `mizu dev` propagates exit codes from your application. If your app exits with code 5, `mizu dev` also exits with code 5. ### `mizu version` | Code | Situation | | ---- | ------------------------------ | | 0 | Version displayed successfully | ## Using Exit Codes in Scripts ### Bash ```bash theme={null} #!/bin/bash set -e # Exit on any error # Create project (exits if fails) mizu new myapp --template api # Or handle specific codes mizu dev || { code=$? case $code in 1) echo "Error occurred" ;; 2) echo "Invalid usage" ;; 3) echo "No project found" ;; *) echo "App exited with code $code" ;; esac exit $code } ``` ### Check for Success ```bash theme={null} if mizu new myapp --template minimal; then echo "Project created!" cd myapp && mizu dev else echo "Failed to create project" exit 1 fi ``` ### CI/CD Pipeline ```yaml theme={null} # GitHub Actions steps: - name: Create project run: mizu new myapp --template api # Fails job if exit code != 0 - name: Validate project run: | cd myapp go mod tidy go build ./... ``` ### Makefile ```makefile theme={null} .PHONY: new dev new: mizu new $(NAME) --template $(TEMPLATE) || exit 1 dev: mizu dev || (echo "Dev server failed with code $$?"; exit 1) ``` ## Error Messages Exit codes are accompanied by error messages: ```bash theme={null} $ mizu unknown error: unknown command "unknown" Run 'mizu --help' for usage. $ echo $? 2 $ mizu new myapp error: template is required Run 'mizu new --list' to see available templates. $ echo $? 2 $ mizu dev # in empty directory error: no runnable main package found (try --cmd) $ echo $? 3 ``` ## JSON Error Output With `--json`, errors include structured information: ```bash theme={null} mizu new myapp --json 2>&1 ``` ```json theme={null} { "success": false, "error": { "code": "missing_template", "message": "template is required (use --template or --list)" } } ``` ## Best Practices ### Always Check Exit Codes ```bash theme={null} # Bad - ignores failures mizu new myapp --template api cd myapp # Good - fails fast mizu new myapp --template api && cd myapp ``` ### Use `set -e` in Scripts ```bash theme={null} #!/bin/bash set -e # Script exits on first error set -o pipefail # Pipeline fails on first error mizu new myapp --template api cd myapp go mod tidy mizu dev ``` ### Handle Specific Codes ```bash theme={null} mizu dev case $? in 0) echo "Clean shutdown" ;; 3) echo "Run from a Go project directory" ;; *) echo "Unexpected exit" ;; esac ``` ## See Also * [Global Flags](/cli/global-flags) - CLI flags reference * [JSON Output](/cli/json-output) - Machine-readable output format * [mizu dev](/cli/dev) - Development server # Flags Source: https://docs.go-mizu.dev/cli/global-flags Flags available on all Mizu CLI commands Global flags can be used with any Mizu CLI command. Flags are options you add after a command to change its behavior - like `--quiet` to reduce output or `--json` to get machine-readable output for scripts. Global flags can be placed either before or after the subcommand. ## Available Flags | Flag | Short | Type | Description | | ------------ | ----- | ---- | --------------------------------- | | `--help` | `-h` | bool | Show help for command | | `--quiet` | `-q` | bool | Reduce output (errors only) | | `--verbose` | `-v` | bool | Increase verbosity (repeatable) | | `--json` | | bool | Emit machine-readable JSON output | | `--no-color` | | bool | Disable color output | ## Flag Placement Global flags can appear before or after the command: ```bash theme={null} # Before command mizu --verbose dev # After command mizu dev --verbose # Mixed (both work) mizu --verbose dev --cmd ./cmd/api ``` ## Detailed Reference ### `--help`, `-h` Shows help information for the CLI or a specific command. ```bash theme={null} # Show general help mizu --help # Show command-specific help mizu new --help mizu dev --help ``` ### `--quiet`, `-q` Suppresses normal output, showing only errors. Useful for scripts where you only care about failures. ```bash theme={null} # Quiet mode - only errors shown mizu new myapp --template minimal --quiet # Normal mode mizu new myapp --template minimal # Created myapp from template minimal # 0 directories, 4 files ``` `--quiet` is ignored when `--json` is used, since JSON output mode has its own output format. ### `--verbose`, `-v` Increases output verbosity. Can be repeated for more detail. ```bash theme={null} # Standard verbosity mizu dev # Level 1 verbose mizu dev -v # discovered main package: ./cmd/api # Starting ./cmd/api... # Level 2 verbose (if available) mizu dev -vv ``` Verbose output is written to stderr, keeping stdout clean for normal output or piping. ### `--json` Enables machine-readable JSON output. All commands support this flag. ```bash theme={null} # JSON output mizu version --json ``` ```json theme={null} { "success": true, "data": { "version": "0.1.0", "go_version": "go1.24", "commit": "abc1234", "built_at": "2024-01-15T10:30:00Z" } } ``` See [JSON Output](/cli/json-output) for detailed format documentation. ### `--no-color` Disables colored output. Useful when: * Piping output to files * Running in environments without color support * Personal preference ```bash theme={null} # Disable colors mizu new --list --no-color # Colors are auto-disabled when not in a terminal mizu new --list > templates.txt # No colors ``` Colors are automatically disabled when stdout is not a terminal (e.g., when piping or redirecting). ## Flag Combinations ### Common Patterns ```bash theme={null} # Silent execution with JSON errors mizu new myapp --template api --quiet --json # Debug mode with verbose output mizu dev --verbose # CI/CD friendly mizu new myapp --template api --json --no-color # Script-friendly version check VERSION=$(mizu version --json | jq -r '.data.version') ``` ### Conflicting Flags Some flags override others: | Combination | Behavior | | ------------------- | ------------------------------ | | `--quiet --verbose` | `--quiet` takes precedence | | `--quiet --json` | JSON output is not suppressed | | `--json --no-color` | JSON output (no colors anyway) | ## Environment Variables The CLI also respects these environment variables: | Variable | Description | Equivalent Flag | | ----------- | ----------------------- | --------------- | | `NO_COLOR` | Disable colors when set | `--no-color` | | `TERM=dumb` | Disable colors | `--no-color` | ```bash theme={null} # Disable colors via environment NO_COLOR=1 mizu new --list ``` ## Examples ### Scripting ```bash theme={null} #!/bin/bash set -e # Create project quietly, fail on error mizu new myapp --template api --quiet # Get version for logging VERSION=$(mizu version --json | jq -r '.data.version') echo "Created with mizu $VERSION" ``` ### CI/CD Pipeline ```yaml theme={null} # GitHub Actions example steps: - name: Create project run: | mizu new myapp --template api --json --no-color > result.json cat result.json | jq '.success' ``` ### Debugging ```bash theme={null} # Maximum verbosity for troubleshooting mizu dev -vvv # Check what would happen mizu new myapp --template api --dry-run -v ``` ## See Also * [Exit Codes](/cli/exit-codes) - Return codes for scripting * [JSON Output](/cli/json-output) - Machine-readable output format * [mizu new](/cli/new) - Create new projects * [mizu dev](/cli/dev) - Run in development mode # Installation Source: https://docs.go-mizu.dev/cli/installation Step-by-step guide to install the Mizu CLI on macOS, Linux, and Windows This guide will help you install the Mizu CLI on your computer. We'll cover everything from checking prerequisites to verifying your installation works. Even if you've never installed a command-line tool before, you'll be able to follow along. ## Prerequisites Before installing the Mizu CLI, you need Go installed on your computer. ### What is Go? Go (also called Golang) is the programming language that Mizu is built with. The Mizu CLI is a Go program, so you need Go to install and run it. ### Check if Go is Installed Open your terminal and run: ```bash theme={null} go version ``` If Go is installed, you'll see something like: ``` go version go1.23.0 darwin/arm64 ``` If you see "command not found", you need to install Go first. ### Install Go **Option 1: Homebrew (Recommended)** ```bash theme={null} brew install go ``` **Option 2: Download from golang.org** 1. Go to [https://go.dev/dl/](https://go.dev/dl/) 2. Download the macOS installer (.pkg file) 3. Run the installer **Ubuntu/Debian:** ```bash theme={null} sudo apt update sudo apt install golang-go ``` **Fedora:** ```bash theme={null} sudo dnf install golang ``` **Or download from golang.org:** ```bash theme={null} # Download (check go.dev for latest version) wget https://go.dev/dl/go1.23.0.linux-amd64.tar.gz # Extract to /usr/local sudo tar -C /usr/local -xzf go1.23.0.linux-amd64.tar.gz # Add to PATH (add this to ~/.bashrc or ~/.zshrc) export PATH=$PATH:/usr/local/go/bin ``` 1. Go to [https://go.dev/dl/](https://go.dev/dl/) 2. Download the Windows installer (.msi file) 3. Run the installer 4. Restart your terminal/command prompt After installing Go, verify it works: ```bash theme={null} go version ``` **Go Version Requirements:** The Mizu CLI requires Go 1.22 or later. If you have an older version, please upgrade. ## Install the Mizu CLI Now you're ready to install the Mizu CLI. The easiest way is using `go install`: ```bash theme={null} go install github.com/go-mizu/mizu/cmd/mizu@latest ``` This command: 1. Downloads the Mizu CLI source code 2. Compiles it on your computer 3. Installs the `mizu` binary to your Go bin directory ### Where is the Binary Installed? The `mizu` binary is installed to `$GOPATH/bin`. You can find this location by running: ```bash theme={null} go env GOPATH ``` This will show something like: * **macOS/Linux:** `/Users/yourname/go` or `/home/yourname/go` * **Windows:** `C:\Users\yourname\go` The binary is in the `bin` folder inside that directory. ## Add Go Bin to Your PATH For the `mizu` command to work from anywhere, the Go bin directory must be in your PATH. Add this line to your `~/.bashrc` or `~/.bash_profile`: ```bash theme={null} export PATH="$PATH:$(go env GOPATH)/bin" ``` Then reload your shell: ```bash theme={null} source ~/.bashrc ``` Add this line to your `~/.zshrc`: ```bash theme={null} export PATH="$PATH:$(go env GOPATH)/bin" ``` Then reload your shell: ```bash theme={null} source ~/.zshrc ``` 1. Press `Win + R`, type `sysdm.cpl`, press Enter 2. Click "Advanced" tab 3. Click "Environment Variables" 4. Under "User variables", find "Path" and click "Edit" 5. Click "New" and add: `%USERPROFILE%\go\bin` 6. Click "OK" to save 7. Restart your terminal ## Verify Installation After installation, verify the CLI is working: ```bash theme={null} mizu version ``` You should see output like: ``` mizu version 0.1.0 go version: go1.23.0 commit: abc1234 built: 2024-01-15T10:30:00Z ``` If you see this, the installation was successful. ## Install from Source If you want to build from source (for development or to get the latest unreleased changes): ```bash theme={null} # Clone the repository git clone https://github.com/go-mizu/mizu.git cd mizu # Build the CLI go build -o mizu ./cmd/mizu # Move to a directory in your PATH # macOS/Linux: sudo mv mizu /usr/local/bin/ # Or move to Go bin: mv mizu $(go env GOPATH)/bin/ ``` ### Build with Version Information For production builds, you can embed version information: ```bash theme={null} go build -ldflags "\ -X github.com/go-mizu/mizu/cli.Version=1.0.0 \ -X github.com/go-mizu/mizu/cli.Commit=$(git rev-parse --short HEAD) \ -X github.com/go-mizu/mizu/cli.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ -o mizu ./cmd/mizu ``` ## Updating the CLI To update to the latest version: ```bash theme={null} go install github.com/go-mizu/mizu/cmd/mizu@latest ``` This downloads and installs the newest version, replacing the old one. ## Shell Completion You can set up tab completion for command names. This lets you press Tab to autocomplete commands. Add to your `~/.bashrc`: ```bash theme={null} complete -W "new dev contract version" mizu ``` Add to your `~/.zshrc`: ```zsh theme={null} compctl -k "(new dev contract version)" mizu ``` ## Troubleshooting ### "command not found: mizu" This means your terminal can't find the `mizu` binary. The most common cause is that Go's bin directory isn't in your PATH. **Fix:** 1. Check where Go installs binaries: ```bash theme={null} go env GOPATH ``` 2. Check if the binary exists: ```bash theme={null} ls $(go env GOPATH)/bin/mizu ``` 3. If the binary exists but the command doesn't work, add Go bin to your PATH (see above). 4. After updating PATH, restart your terminal or run: ```bash theme={null} source ~/.bashrc # or ~/.zshrc ``` ### "go: command not found" Go is not installed or not in your PATH. Follow the Go installation instructions above. ### "permission denied" If you get permission errors: **Option 1:** Use sudo (not recommended for go install) ```bash theme={null} sudo go install github.com/go-mizu/mizu/cmd/mizu@latest ``` **Option 2:** Set GOPATH to a directory you own (recommended) ```bash theme={null} export GOPATH=$HOME/go go install github.com/go-mizu/mizu/cmd/mizu@latest ``` ### Build Errors If you get compilation errors when installing: 1. Make sure you have Go 1.22 or later: ```bash theme={null} go version ``` 2. Clear Go's module cache and try again: ```bash theme={null} go clean -modcache go install github.com/go-mizu/mizu/cmd/mizu@latest ``` ### Verify Go Environment If you're having trouble, check your Go environment: ```bash theme={null} go env ``` Key variables to check: * `GOPATH`: Where Go stores packages and binaries * `GOROOT`: Where Go itself is installed * `GOBIN`: Where binaries are installed (defaults to `$GOPATH/bin`) ## Next Steps Now that you have the CLI installed, you're ready to create your first project: Use mizu new to scaffold your first application Learn about the different project templates # JSON Output Source: https://docs.go-mizu.dev/cli/json-output Machine-readable JSON output format for automation All Mizu CLI commands support JSON output via the `--json` flag. JSON (JavaScript Object Notation) is a structured data format that scripts and programs can easily parse. Use this when you want to process command output programmatically instead of reading it as plain text. ## Enabling JSON Output Add `--json` to any command: ```bash theme={null} mizu version --json mizu new myapp --template api --json mizu dev --json ``` ## Output Format ### Success Response ```json theme={null} { "success": true, "data": { // Command-specific data } } ``` ### Error Response ```json theme={null} { "success": false, "error": { "code": "error_code", "message": "Human-readable error message" } } ``` ## Command-Specific Output ### `mizu version --json` ```json theme={null} { "success": true, "data": { "version": "0.1.0", "go_version": "go1.24", "commit": "abc1234", "built_at": "2024-01-15T10:30:00Z" } } ``` ### `mizu new --list --json` ```json theme={null} { "success": true, "data": { "templates": [ { "name": "api", "description": "JSON API service with a recommended layout", "tags": ["go", "mizu", "api", "service"] }, { "name": "minimal", "description": "Smallest runnable Mizu project", "tags": ["go", "mizu", "minimal", "starter"] } ] } } ``` ### `mizu new --json` Success: ```json theme={null} { "success": true, "data": { "template": "api", "root": "/path/to/myapp", "ops": [ {"op": "mkdir", "path": "app"}, {"op": "mkdir", "path": "app/api"}, {"op": "mkdir", "path": "cmd"}, {"op": "mkdir", "path": "cmd/api"}, {"op": "write", "path": ".gitignore", "bytes": 156, "mode": "0644"}, {"op": "write", "path": "README.md", "bytes": 423, "mode": "0644"}, {"op": "write", "path": "go.mod", "bytes": 78, "mode": "0644"}, {"op": "write", "path": "cmd/api/main.go", "bytes": 512, "mode": "0644"} ], "summary": { "mkdir": 4, "write": 4, "overwrite": 0, "skip": 0 } } } ``` Error: ```json theme={null} { "success": false, "error": { "code": "missing_template", "message": "template is required (use --template or --list)" } } ``` ### `mizu new --dry-run --json` Same format as `mizu new --json`, but files are not written: ```json theme={null} { "success": true, "data": { "template": "minimal", "root": "/path/to/myapp", "ops": [ {"op": "write", "path": ".gitignore", "bytes": 156, "mode": "0644"}, {"op": "write", "path": "README.md", "bytes": 234, "mode": "0644"}, {"op": "write", "path": "go.mod", "bytes": 78, "mode": "0644"}, {"op": "write", "path": "main.go", "bytes": 245, "mode": "0644"} ], "summary": { "mkdir": 0, "write": 4, "overwrite": 0, "skip": 0 } } } ``` ### `mizu dev --json` The dev command emits NDJSON (newline-delimited JSON) with lifecycle events: ```json theme={null} {"event":"starting","timestamp":"2024-01-15T10:30:00Z","message":"running ./cmd/api"} {"event":"started","timestamp":"2024-01-15T10:30:01Z","message":"pid 12345"} {"event":"signal","timestamp":"2024-01-15T10:35:00Z","message":"interrupt"} {"event":"stopped","timestamp":"2024-01-15T10:35:02Z","message":"clean exit"} ``` #### Event Types | Event | Description | Fields | | ---------- | --------------------------- | ------------------------ | | `starting` | About to start process | `message` | | `started` | Process started | `message` (includes PID) | | `signal` | Received shutdown signal | `message` (signal name) | | `timeout` | Graceful shutdown timed out | `message` | | `stopped` | Process exited | `message`, `exit_code` | | `error` | Error occurred | `message`, `exit_code` | #### Event Schema ```typescript theme={null} interface DevEvent { event: "starting" | "started" | "signal" | "timeout" | "stopped" | "error"; timestamp: string; // ISO 8601 UTC message?: string; // Human-readable description exit_code?: number; // Present for stopped/error events } ``` ## Error Codes | Code | Description | | ------------------ | ---------------------------------------- | | `missing_template` | Template flag required but not provided | | `unknown_template` | Specified template doesn't exist | | `path_error` | Invalid path (traversal, absolute, etc.) | | `conflicts` | Files would be overwritten (use --force) | | `plan_error` | Error building execution plan | | `apply_error` | Error writing files | | `list_error` | Error listing templates | ## Parsing JSON Output ### jq Examples ```bash theme={null} # Get version number mizu version --json | jq -r '.data.version' # Check if operation succeeded mizu new myapp --template api --json | jq '.success' # List template names mizu new --list --json | jq -r '.data.templates[].name' # Count files created mizu new myapp --template api --json | jq '.data.summary.write' # Get error message mizu new myapp --json 2>&1 | jq -r '.error.message // empty' ``` ### Bash Script ```bash theme={null} #!/bin/bash # Create project and parse result result=$(mizu new myapp --template api --json 2>&1) if echo "$result" | jq -e '.success' > /dev/null; then files=$(echo "$result" | jq '.data.summary.write') echo "Created project with $files files" else error=$(echo "$result" | jq -r '.error.message') echo "Error: $error" exit 1 fi ``` ### Python ```python theme={null} import subprocess import json result = subprocess.run( ["mizu", "version", "--json"], capture_output=True, text=True ) data = json.loads(result.stdout) if data["success"]: print(f"Version: {data['data']['version']}") else: print(f"Error: {data['error']['message']}") ``` ### Go ```go theme={null} package main import ( "encoding/json" "os/exec" ) type VersionResult struct { Success bool `json:"success"` Data struct { Version string `json:"version"` GoVersion string `json:"go_version"` } `json:"data"` } func main() { out, _ := exec.Command("mizu", "version", "--json").Output() var result VersionResult json.Unmarshal(out, &result) if result.Success { println("Version:", result.Data.Version) } } ``` ## Streaming Events (mizu dev) For `mizu dev --json`, parse events line by line: ### Bash ```bash theme={null} mizu dev --json 2>&1 | while IFS= read -r line; do event=$(echo "$line" | jq -r '.event') case "$event" in started) pid=$(echo "$line" | jq -r '.message' | grep -oP 'pid \K\d+') echo "Process started with PID: $pid" ;; stopped) code=$(echo "$line" | jq -r '.exit_code // 0') echo "Process exited with code: $code" ;; esac done ``` ### Node.js ```javascript theme={null} const { spawn } = require('child_process'); const readline = require('readline'); const proc = spawn('mizu', ['dev', '--json']); const rl = readline.createInterface({ input: proc.stdout, crlfDelay: Infinity }); rl.on('line', (line) => { const event = JSON.parse(line); console.log(`Event: ${event.event} - ${event.message}`); }); ``` ## Best Practices ### Always Handle Both Success and Error ```bash theme={null} result=$(mizu new myapp --template api --json 2>&1) if echo "$result" | jq -e '.success' > /dev/null 2>&1; then # Handle success else # Handle error fi ``` ### Capture stderr for Errors ```bash theme={null} # Errors go to stderr mizu new myapp --json 2>&1 | jq '.' ``` ### Use jq for Safe Parsing ```bash theme={null} # Safe extraction with defaults mizu version --json | jq -r '.data.version // "unknown"' ``` ## See Also * [Global Flags](/cli/global-flags) - All CLI flags including `--json` * [Exit Codes](/cli/exit-codes) - Return codes for automation * [mizu dev](/cli/dev) - Development server with streaming events # Overview Source: https://docs.go-mizu.dev/cli/live/overview Real-time interactive applications with WebSockets The live template creates real-time applications with WebSocket support. Normal web pages only update when you refresh them. Real-time apps update automatically - like chat apps or live dashboards. WebSockets keep a connection open between your browser and the server, so the server can push updates to you instantly without you asking. ## When to Use This Template Use the live template when: * **Real-time updates** - Data changes pushed to clients instantly * **Interactive UI** - User actions trigger server-side updates * **Live dashboards** - Metrics, monitoring, feeds * **Collaborative features** - Chat, notifications, presence ## What You Get * **Live views** - Server-rendered UI that updates automatically * **WebSocket handling** - Built-in connection management * **Pub/sub messaging** - Broadcast to all or specific clients * **Session management** - Track connected users ## Quick Start ```bash theme={null} mizu new myapp --template live cd myapp go mod tidy mizu dev ``` Open `http://localhost:8080` to see a live counter example. ## How It Works ### 1. User Loads Page Server renders the initial HTML. ### 2. WebSocket Connects JavaScript connects to `/ws` endpoint. ### 3. User Interacts Click events send messages to server. ### 4. Server Updates Server processes message, sends HTML back. ### 5. UI Updates Client replaces DOM with new HTML. ## Project Structure ``` myapp/ ├── cmd/server/main.go # Entry point ├── app/server/ │ ├── app.go # Application setup │ └── routes.go # Routes ├── handler/ │ ├── home.go # Home page │ └── counter.go # Counter live view ├── assets/ # Static files ├── views/ # Templates └── go.mod ``` ## Example: Counter ```go theme={null} // handler/counter.go func Counter(live *live.Server) mizu.Handler { return func(c *mizu.Ctx) error { return c.Render("pages/counter", nil) } } // WebSocket handler func (h *CounterHandler) OnMessage(ctx context.Context, s *live.Session, topic string, data []byte) { switch topic { case "increment": h.count++ case "decrement": h.count-- } // Re-render and send to client html := h.render() s.Send("update", html) } ``` ## Key Features ### Live Updates Push HTML updates to connected clients: ```go theme={null} // Send update to one session session.Send("update", html) // Broadcast to all sessions server.Publish("counter", "update", html) ``` ### Session State Track per-connection state: ```go theme={null} func OnConnect(ctx context.Context, s *live.Session) error { s.Set("username", "anonymous") return nil } ``` ### Topics Subscribe to specific channels: ```go theme={null} session.Subscribe("chat:room1") server.Publish("chat:room1", "message", data) ``` ## Next Steps Understand every file Build a real-time app # Structure Source: https://docs.go-mizu.dev/cli/live/structure Understanding the live template organization The live template combines server-rendered views with WebSocket-powered real-time updates. This structure allows the server to push changes to the browser instantly, without the user needing to refresh. It's like having a constant conversation between your server and the browser. ## Directory Layout ``` myapp/ ├── cmd/server/main.go # Entry point ├── app/server/ │ ├── app.go # Application + live server setup │ ├── config.go # Configuration │ └── routes.go # HTTP + WebSocket routes ├── handler/ │ ├── home.go # Static home page │ └── counter.go # Live counter handler ├── assets/ │ ├── embed.go # Asset embedding │ ├── css/style.css # Styles │ └── js/live.js # WebSocket client ├── views/ │ ├── layouts/main.html # Main layout │ ├── pages/ │ │ ├── home.html # Home page │ │ └── counter.html # Counter page │ └── partials/ # Shared partials ├── go.mod └── .gitignore ``` ## Core Files ### app/server/app.go ```go theme={null} package server import ( "github.com/go-mizu/mizu" "github.com/go-mizu/mizu/live" "github.com/go-mizu/mizu/view" ) type App struct { cfg Config app *mizu.App engine *view.Engine liveServer *live.Server } func New(cfg Config) *App { a := &App{cfg: cfg} a.app = mizu.New() // View engine for templates a.engine = view.New(view.Options{ Root: "views", Layout: "layouts/main", }) a.app.Use(a.engine.Middleware()) // Live server for WebSocket a.liveServer = live.NewServer(live.Options{ OnConnect: a.onConnect, OnMessage: a.onMessage, }) a.routes() return a } func (a *App) onConnect(ctx context.Context, s *live.Session) error { // Initialize session state return nil } func (a *App) onMessage(ctx context.Context, s *live.Session, topic string, data []byte) { // Handle incoming messages } ``` ### app/server/routes.go ```go theme={null} package server import "example.com/myapp/handler" func (a *App) routes() { // Static files a.app.Mount("/static/", staticHandler(a.cfg.Dev)) // Pages a.app.Get("/", handler.Home()) a.app.Get("/counter", handler.Counter(a.engine)) // WebSocket endpoint a.app.Mount("/ws", a.liveServer.Handler()) } ``` ### handler/counter.go ```go theme={null} package handler import ( "github.com/go-mizu/mizu" "github.com/go-mizu/mizu/view" ) func Counter(engine *view.Engine) mizu.Handler { return func(c *mizu.Ctx) error { return c.Render("pages/counter", map[string]any{ "Title": "Counter", "Count": 0, }) } } ``` ### assets/js/live.js ```javascript theme={null} // WebSocket client for live updates class LiveSocket { constructor(url) { this.url = url; this.connect(); } connect() { this.ws = new WebSocket(this.url); this.ws.onmessage = (e) => this.handleMessage(e); this.ws.onclose = () => setTimeout(() => this.connect(), 1000); } send(topic, data) { this.ws.send(JSON.stringify({ topic, data })); } handleMessage(event) { const { topic, data } = JSON.parse(event.data); if (topic === 'update') { document.getElementById('live-content').innerHTML = data; } } } // Initialize when DOM ready document.addEventListener('DOMContentLoaded', () => { window.live = new LiveSocket('ws://' + location.host + '/ws'); }); ``` ### views/pages/counter.html ```html theme={null} {{define "content"}}

Counter: {{.Count}}

{{end}} ``` ## Message Flow 1. **Page Load** - Server renders initial HTML 2. **WebSocket Connect** - Client connects to `/ws` 3. **User Action** - Button click calls `live.send()` 4. **Server Receives** - `OnMessage` handler processes 5. **Server Sends** - New HTML sent back 6. **DOM Update** - Client replaces content ## Next Steps Build a real-time chat Deep dive into live views # Tutorial Source: https://docs.go-mizu.dev/cli/live/tutorial Build a real-time counter application In this tutorial, you'll build an interactive counter that updates in real-time across all connected browsers. When one user clicks a button, everyone sees the change instantly. This is the foundation for chat apps, live dashboards, collaborative tools, and any feature that needs instant updates. ## Step 1: Create the Project ```bash theme={null} mizu new livecounter --template live cd livecounter go mod tidy mizu dev ``` Open `http://localhost:8080` to see the default app. ## Step 2: Understand the Counter The template includes a counter example. Let's examine how it works. ### The Counter Handler Look at `handler/counter.go`: ```go theme={null} package handler import "github.com/go-mizu/mizu" func Counter() mizu.Handler { return func(c *mizu.Ctx) error { return c.Render("pages/counter", map[string]any{ "Title": "Live Counter", "Count": 0, }) } } ``` ### The Counter View Look at `views/pages/counter.html`: ```html theme={null} {{define "content"}}

{{.Count}}

{{end}} ``` ### The WebSocket Handler In `app/server/app.go`, messages are handled: ```go theme={null} func (a *App) onMessage(ctx context.Context, s *live.Session, topic string, data []byte) { switch topic { case "increment": a.count++ case "decrement": a.count-- } // Re-render and broadcast to all clients html := a.renderCounter() a.liveServer.Publish("counter", "update", []byte(html)) } func (a *App) renderCounter() string { return fmt.Sprintf(`

%d

`, a.count) } ``` ## Step 3: Test Real-Time Updates 1. Open `http://localhost:8080/counter` in one browser 2. Open it in another browser window 3. Click + or - in one window 4. Watch both windows update! ## Step 4: Add a Reset Button Update the `renderCounter` function: ```go theme={null} func (a *App) renderCounter() string { return fmt.Sprintf(`

%d

`, a.count) } ``` Handle the reset message: ```go theme={null} func (a *App) onMessage(ctx context.Context, s *live.Session, topic string, data []byte) { switch topic { case "increment": a.count++ case "decrement": a.count-- case "reset": a.count = 0 } html := a.renderCounter() a.liveServer.Publish("counter", "update", []byte(html)) } ``` ## Step 5: Add Per-User Counters Each user can have their own counter: ```go theme={null} func (a *App) onConnect(ctx context.Context, s *live.Session) error { // Initialize per-session counter s.Set("count", 0) return nil } func (a *App) onMessage(ctx context.Context, s *live.Session, topic string, data []byte) { count := s.Get("count").(int) switch topic { case "increment": count++ case "decrement": count-- case "reset": count = 0 } s.Set("count", count) // Send update to this session only html := renderCounter(count) s.Send("update", []byte(html)) } ``` ## How Live Updates Work ``` Browser Server | | | --- WebSocket Connect -------> | | | (creates Session) | | | --- "increment" ------------> | | | (updates count) | <-- "update" (new HTML) ------ | | | | (replaces DOM) | ``` ## What You Learned * WebSocket connection handling * Message-based communication * Broadcasting updates to all clients * Per-session state management * Server-side rendering for updates ## Next Steps Add offline support and data sync Deep dive into live features # Overview Source: https://docs.go-mizu.dev/cli/minimal/overview The simplest Mizu project - perfect for learning The minimal template creates the smallest possible runnable Mizu application. If you're new to web development or Go, this is the best place to start. It contains just one file with about 20 lines of code, making it easy to understand exactly what happens when you run a web server. ## When to Use This Template Use the minimal template when: * **Learning Mizu** - Understand the basics without distractions * **Quick experiments** - Test an idea in minutes * **Custom structure** - You want to build your own project layout * **Embedding examples** - Need a simple example to share ## What You Get A single-file application with: * One Go file (`main.go`) * A basic route that returns "Hello" * A running server on port 8080 That's it. No extra folders, no configuration files, just the essentials. ## Quick Start ```bash theme={null} # Create the project mizu new hello --template minimal # Enter the project cd hello # Install dependencies go mod tidy # Run the server mizu dev ``` Open `http://localhost:8080` in your browser. You'll see: ``` Hello from hello ``` ## What the Code Looks Like The entire application fits in one file: ```go theme={null} package main import ( "log" "github.com/go-mizu/mizu" ) func main() { app := mizu.New() app.Get("/", func(c *mizu.Ctx) error { return c.Text(200, "Hello from minimal\n") }) log.Println("listening on :8080") if err := app.Listen(":8080"); err != nil { log.Fatal(err) } } ``` Let's break this down: 1. **Create the app** - `mizu.New()` creates a new Mizu application 2. **Add a route** - `app.Get("/", ...)` handles GET requests to the root path 3. **Return a response** - `c.Text(200, "...")` sends a plain text response 4. **Start the server** - `app.Listen(":8080")` starts the HTTP server ## Why Start Minimal? ### Easy to Understand With just 20 lines of code, you can see exactly what Mizu does: * Creates an HTTP server * Routes requests to handlers * Sends responses ### Easy to Modify Want to try something? Just edit the one file: ```go theme={null} // Add a JSON endpoint app.Get("/api/user", func(c *mizu.Ctx) error { return c.JSON(200, map[string]string{ "name": "Alice", "role": "admin", }) }) ``` ### Easy to Grow When you're ready for more structure, you can: 1. Add new files to the same directory 2. Migrate to the `api` template 3. Build your own custom structure ## Comparison with Other Templates | Aspect | minimal | api | | ---------------- | ------- | ----- | | Files | 1 | 10+ | | Folders | 0 | 5+ | | Lines of code | \~20 | \~200 | | Learning curve | Minutes | Hours | | Production ready | No | Yes | ## When to Upgrade Consider switching to the `api` template when: * You have more than 3-4 routes * You need configuration management * Multiple developers are working on the project * You want feature-based organization ## Next Steps See exactly what files are created Build your first Mizu app # Structure Source: https://docs.go-mizu.dev/cli/minimal/structure Understanding the files in a minimal Mizu project The minimal template creates the simplest possible project structure. This page explains every file so you understand exactly what you're working with. Don't worry if some concepts are new - we'll explain everything. ## Directory Layout ``` myapp/ ├── go.mod # Go module definition ├── main.go # Your application code ├── .gitignore # Git ignore rules └── README.md # Project documentation ``` That's only 4 files! ## File Details ### main.go This is your entire application: ```go theme={null} package main import ( "log" "github.com/go-mizu/mizu" ) func main() { app := mizu.New() app.Get("/", func(c *mizu.Ctx) error { return c.Text(200, "Hello from myapp\n") }) log.Println("listening on :8080") if err := app.Listen(":8080"); err != nil { log.Fatal(err) } } ``` **Line by line explanation:** | Line | Code | What it does | | ---- | --------------------------- | ----------------------------------------- | | 1 | `package main` | Marks this as an executable program | | 4 | `"log"` | Import Go's logging package | | 5 | `"github.com/go-mizu/mizu"` | Import the Mizu framework | | 8 | `func main()` | Entry point - runs when you start the app | | 9 | `app := mizu.New()` | Create a new Mizu application | | 11 | `app.Get("/", ...)` | Register a handler for GET / | | 12 | `func(c *mizu.Ctx) error` | Handler function signature | | 13 | `return c.Text(200, ...)` | Send HTTP 200 with text | | 16 | `log.Println(...)` | Log that we're starting | | 17 | `app.Listen(":8080")` | Start server on port 8080 | | 18 | `log.Fatal(err)` | Exit if server fails to start | ### go.mod The Go module file defines your project: ```go theme={null} module example.com/myapp go 1.22 require github.com/go-mizu/mizu v0.0.0 ``` **What each line means:** | Line | Meaning | | --------------------------------- | --------------------------- | | `module example.com/myapp` | Your project's import path | | `go 1.22` | Minimum Go version required | | `require github.com/go-mizu/mizu` | Dependency on Mizu | The version `v0.0.0` is a placeholder. When you run `go mod tidy`, it's replaced with the actual latest version. ### .gitignore Tells Git which files to ignore: ```gitignore theme={null} # Binaries *.exe *.exe~ *.dll *.so *.dylib # Test binary *.test # Output *.out # Go workspace go.work # IDE .idea/ .vscode/ *.swp *.swo ``` This prevents compiled binaries and editor files from being committed. ### README.md Basic project documentation: ````markdown theme={null} # myapp A Mizu application. ## Getting Started ```bash go mod tidy mizu dev ```` Open [http://localhost:8080](http://localhost:8080) ```` ## Understanding the Code ### The Handler ```go app.Get("/", func(c *mizu.Ctx) error { return c.Text(200, "Hello from myapp\n") }) ```` This is a **handler** - a function that processes HTTP requests. * `app.Get` - Registers this handler for GET requests * `"/"` - The URL path to match * `func(c *mizu.Ctx) error` - The handler function * `c` - The context, contains request info and response methods * `c.Text(200, ...)` - Send a text response with status 200 ### The Context The `*mizu.Ctx` parameter gives you access to: ```go theme={null} // Request data c.Param("id") // URL parameters like /users/:id c.Query("page") // Query string like ?page=1 c.Body() // Request body // Response methods c.Text(200, "...") // Plain text c.JSON(200, data) // JSON response c.HTML(200, "...") // HTML response ``` ### Starting the Server ```go theme={null} if err := app.Listen(":8080"); err != nil { log.Fatal(err) } ``` * `":8080"` means "listen on port 8080 on all interfaces" * `Listen` blocks until the server is stopped * If it fails (port in use, etc.), we log the error and exit ## Common Modifications ### Change the Port ```go theme={null} log.Println("listening on :3000") if err := app.Listen(":3000"); err != nil { log.Fatal(err) } ``` ### Add More Routes ```go theme={null} app.Get("/", func(c *mizu.Ctx) error { return c.Text(200, "Home page") }) app.Get("/about", func(c *mizu.Ctx) error { return c.Text(200, "About page") }) app.Get("/api/users", func(c *mizu.Ctx) error { return c.JSON(200, []string{"Alice", "Bob"}) }) ``` ### Add URL Parameters ```go theme={null} app.Get("/users/:id", func(c *mizu.Ctx) error { id := c.Param("id") return c.Text(200, "User ID: "+id) }) ``` ### Handle POST Requests ```go theme={null} app.Post("/users", func(c *mizu.Ctx) error { // Read JSON from request body var user struct { Name string `json:"name"` } if err := c.Bind(&user); err != nil { return err } return c.JSON(201, user) }) ``` ## When to Add More Structure Signs you've outgrown the minimal template: 1. **main.go is over 100 lines** - Time to split into multiple files 2. **You have related routes** - Group them into packages 3. **You need configuration** - Add a config file/struct 4. **Multiple developers** - Need clearer organization At this point, consider using the `api` template or building your own structure. ## Next Steps Build your first app step by step See a more structured approach # Tutorial Source: https://docs.go-mizu.dev/cli/minimal/tutorial Build your first Mizu application from scratch In this tutorial, you'll create a simple web application that responds to HTTP requests. We'll go step by step, explaining each concept as we encounter it. By the end, you'll understand how web servers work and the core concepts of Mizu. ## Prerequisites Before starting, make sure you have: * Go installed (version 1.22 or later) * Mizu CLI installed * A text editor Not sure? Run these commands: ```bash theme={null} go version # Should show go1.22 or higher mizu version # Should show mizu version info ``` ## Step 1: Create the Project Open your terminal and run: ```bash theme={null} mizu new hello --template minimal ``` You should see: ``` Created hello from template minimal 0 directories, 4 files Next steps: cd hello go mod tidy mizu dev ``` ## Step 2: Enter the Project ```bash theme={null} cd hello ``` Look at what was created: ```bash theme={null} ls -la ``` Output: ``` .gitignore README.md go.mod main.go ``` ## Step 3: Install Dependencies ```bash theme={null} go mod tidy ``` This downloads the Mizu framework and updates `go.mod` with the correct version. ## Step 4: Run the Server ```bash theme={null} mizu dev ``` Output: ``` Starting . listening on :8080 ``` Your server is running! ## Step 5: Test It Open another terminal (keep the server running) and test with curl: ```bash theme={null} curl http://localhost:8080 ``` Output: ``` Hello from hello ``` Or open `http://localhost:8080` in your browser. ## Step 6: Stop the Server Go back to the terminal running `mizu dev` and press `Ctrl+C`: ``` ^C Received interrupt, shutting down... ``` ## Understanding the Code Now let's look at the code. Open `main.go` in your editor: ```go theme={null} package main import ( "log" "github.com/go-mizu/mizu" ) func main() { app := mizu.New() app.Get("/", func(c *mizu.Ctx) error { return c.Text(200, "Hello from hello\n") }) log.Println("listening on :8080") if err := app.Listen(":8080"); err != nil { log.Fatal(err) } } ``` ### Key Concepts **1. Creating the App** ```go theme={null} app := mizu.New() ``` This creates a new Mizu application instance. It sets up the HTTP router and prepares everything for handling requests. **2. Adding a Route** ```go theme={null} app.Get("/", func(c *mizu.Ctx) error { return c.Text(200, "Hello from hello\n") }) ``` * `app.Get` means "handle GET requests" * `"/"` is the path (root URL) * The function is your handler - it runs when someone visits `/` * `c` is the context - it has request info and response methods * `c.Text(200, ...)` sends a plain text response with status code 200 **3. Starting the Server** ```go theme={null} if err := app.Listen(":8080"); err != nil { log.Fatal(err) } ``` This starts the HTTP server on port 8080 and waits for requests. ## Exercise 1: Add a New Route Let's add an `/about` page. Edit `main.go`: ```go theme={null} package main import ( "log" "github.com/go-mizu/mizu" ) func main() { app := mizu.New() app.Get("/", func(c *mizu.Ctx) error { return c.Text(200, "Hello from hello\n") }) // NEW: Add about route app.Get("/about", func(c *mizu.Ctx) error { return c.Text(200, "This is the about page\n") }) log.Println("listening on :8080") if err := app.Listen(":8080"); err != nil { log.Fatal(err) } } ``` Restart the server: ```bash theme={null} # Press Ctrl+C to stop, then: mizu dev ``` Test the new route: ```bash theme={null} curl http://localhost:8080/about ``` Output: ``` This is the about page ``` ## Exercise 2: Return JSON APIs usually return JSON. Let's add a JSON endpoint. Edit `main.go`: ```go theme={null} package main import ( "log" "github.com/go-mizu/mizu" ) func main() { app := mizu.New() app.Get("/", func(c *mizu.Ctx) error { return c.Text(200, "Hello from hello\n") }) app.Get("/about", func(c *mizu.Ctx) error { return c.Text(200, "This is the about page\n") }) // NEW: Add JSON endpoint app.Get("/api/status", func(c *mizu.Ctx) error { return c.JSON(200, map[string]string{ "status": "ok", "app": "hello", }) }) log.Println("listening on :8080") if err := app.Listen(":8080"); err != nil { log.Fatal(err) } } ``` Restart and test: ```bash theme={null} curl http://localhost:8080/api/status ``` Output: ```json theme={null} {"app":"hello","status":"ok"} ``` ## Exercise 3: URL Parameters Let's create a route that uses URL parameters. Edit `main.go`: ```go theme={null} package main import ( "log" "github.com/go-mizu/mizu" ) func main() { app := mizu.New() app.Get("/", func(c *mizu.Ctx) error { return c.Text(200, "Hello from hello\n") }) // NEW: Route with parameter app.Get("/greet/:name", func(c *mizu.Ctx) error { name := c.Param("name") return c.Text(200, "Hello, "+name+"!\n") }) log.Println("listening on :8080") if err := app.Listen(":8080"); err != nil { log.Fatal(err) } } ``` Restart and test: ```bash theme={null} curl http://localhost:8080/greet/Alice curl http://localhost:8080/greet/Bob ``` Output: ``` Hello, Alice! Hello, Bob! ``` The `:name` in the route is a **parameter**. Whatever value is in that position gets captured and is available via `c.Param("name")`. ## Exercise 4: Query Parameters Query parameters are the `?key=value` part of URLs. Edit `main.go`: ```go theme={null} package main import ( "log" "github.com/go-mizu/mizu" ) func main() { app := mizu.New() // NEW: Route with query parameter app.Get("/search", func(c *mizu.Ctx) error { query := c.Query("q") if query == "" { query = "(no query)" } return c.Text(200, "Searching for: "+query+"\n") }) log.Println("listening on :8080") if err := app.Listen(":8080"); err != nil { log.Fatal(err) } } ``` Restart and test: ```bash theme={null} curl "http://localhost:8080/search?q=mizu" curl "http://localhost:8080/search" ``` Output: ``` Searching for: mizu Searching for: (no query) ``` ## What You've Learned In this tutorial, you learned: 1. **Create a project** with `mizu new` 2. **Run a server** with `mizu dev` 3. **Add routes** with `app.Get()` 4. **Return text** with `c.Text()` 5. **Return JSON** with `c.JSON()` 6. **Use URL parameters** with `:name` and `c.Param()` 7. **Use query parameters** with `c.Query()` ## Next Steps You've mastered the basics. Here are some options: Learn a production-ready project structure Add logging, auth, and more to your routes Learn all the context methods See more complete examples # New Source: https://docs.go-mizu.dev/cli/new Create a new Mizu project from a template The `mizu new` command creates a new Mizu project by generating files from a template. Think of templates as starter kits - each one sets up a complete project structure so you can start coding right away instead of creating files manually. ## Basic Usage ```bash theme={null} mizu new [path] --template ``` **Arguments:** * `[path]` - Where to create the project (optional, defaults to current directory) * `--template` - Which template to use (required) ## Quick Examples ```bash theme={null} # Create a minimal project in current directory mizu new . --template minimal # Create an API project in a new folder mizu new myapi --template api # Create a web app with a custom module name mizu new webapp --template web --module github.com/myname/webapp # Preview what will be created (without creating anything) mizu new myapp --template api --dry-run ``` ## Understanding the Command When you run `mizu new myapp --template api`, here's what happens: 1. **Creates the directory** - A new folder called `myapp` is created 2. **Generates files** - Template files are copied with your project name filled in 3. **Sets up Go module** - The `go.mod` file is created with the correct module path 4. **Reports success** - Shows you what was created Example output: ``` Created myapp from template api 5 directories, 12 files Next steps: cd myapp go mod tidy mizu dev ``` ## Available Templates To see all available templates: ```bash theme={null} mizu new --list ``` Output: ``` Template Purpose api JSON API service with a recommended layout contract Transport-neutral service contract with REST, JSON-RPC, and OpenAPI live Real-time interactive app with live views and instant updates minimal Smallest runnable Mizu project sync Offline-first app with sync, live updates, and reactive state web Full-stack web app with views, components, and static assets ``` ## Flags Reference ### Required Flags | Flag | Short | Description | | ------------------- | ----- | ------------------- | | `--template ` | `-t` | The template to use | ### Optional Flags | Flag | Description | Default | | ------------------- | ---------------------------------------------------- | -------------------- | | `--list` | List available templates | - | | `--dry-run` | Show what would be created without creating anything | - | | `--force` | Overwrite existing files | - | | `--name ` | Project name | Derived from path | | `--module ` | Go module path | `example.com/` | | `--license ` | License identifier | `MIT` | | `--var key=value` | Custom template variable (repeatable) | - | ## Detailed Flag Explanations ### --template, -t (Required) Specifies which template to use. This is the only required flag. ```bash theme={null} # Using long form mizu new myapp --template api # Using short form mizu new myapp -t api ``` If you forget to specify a template: ``` error: template is required Run 'mizu new --list' to see available templates. ``` ### --list Lists all available templates without creating a project. ```bash theme={null} mizu new --list ``` This is helpful when you forget the template names. ### --dry-run Shows exactly what files would be created without actually creating them. This is useful for: * Previewing the project structure * Checking for conflicts before overwriting * Understanding what a template includes ```bash theme={null} mizu new myapp --template api --dry-run ``` Output: ``` Plan for template: api Target: /Users/you/projects/myapp Operations: mkdir cmd mkdir cmd/api mkdir app mkdir app/api mkdir feature mkdir feature/hello write go.mod write main.go write cmd/api/main.go write app/api/app.go write app/api/config.go write app/api/routes.go write feature/hello/http.go ... ``` ### --force Overwrites existing files without asking. Use this carefully! ```bash theme={null} # This will overwrite any existing files mizu new myapp --template api --force ``` Without `--force`, if files already exist: ``` error: files already exist: go.mod main.go Use --force to overwrite. ``` ### --name Sets the project name explicitly. By default, the name is derived from the path. ```bash theme={null} # Name will be "myapp" mizu new myapp --template api # Override the name mizu new myapp --template api --name my-cool-app ``` The name is used in: * Package names * Log messages * Config files ### --module Sets the Go module path. This is important for imports within your project. ```bash theme={null} # Default: example.com/myapp mizu new myapp --template api # Custom module path mizu new myapp --template api --module github.com/myname/myapp ``` **What is a module path?** A Go module path is a unique identifier for your project. It's used when: * Other packages import your code * You run `go get` to download your project * Go resolves dependencies Common patterns: * `github.com/username/project` - For GitHub projects * `example.com/project` - For local development * `company.com/team/project` - For internal projects ### --license Sets the license identifier. Default is `MIT`. ```bash theme={null} mizu new myapp --template api --license Apache-2.0 ``` This affects the LICENSE file content in templates that include one. ### --var (Custom Variables) Pass custom variables to templates. This flag can be used multiple times. ```bash theme={null} mizu new myapp --template api --var port=3000 --var db=postgres ``` Custom variables are available in templates as `.Vars.key`. Template authors can document which variables their templates support. ## Working with Paths ### Current Directory Create files in the current directory: ```bash theme={null} # Using "." mizu new . --template minimal # Or omit the path entirely (same effect) cd myproject mizu new --template minimal ``` ### New Directory Create a new directory for the project: ```bash theme={null} mizu new myapp --template api ``` ### Nested Path Create a project in a nested path: ```bash theme={null} mizu new projects/backend/api --template api ``` All necessary parent directories are created automatically. ### Absolute Path You can also use absolute paths: ```bash theme={null} mizu new /Users/me/projects/myapp --template api ``` ## JSON Output For scripting and automation, use `--json`: ```bash theme={null} mizu new myapp --template api --json ``` Output: ```json theme={null} { "template": "api", "path": "/Users/you/projects/myapp", "operations": [ {"type": "mkdir", "path": "cmd"}, {"type": "mkdir", "path": "cmd/api"}, {"type": "write", "path": "go.mod", "size": 156}, {"type": "write", "path": "cmd/api/main.go", "size": 423} ], "summary": { "directories": 5, "files": 12 } } ``` ### JSON with Dry Run Combine `--json` with `--dry-run` to get a machine-readable plan: ```bash theme={null} mizu new myapp --template api --dry-run --json ``` ### JSON Error Output Errors are also returned as JSON: ```json theme={null} { "error": "unknown_template", "message": "unknown template: badname" } ``` ## Common Workflows ### Start a New Project The most common workflow: ```bash theme={null} # 1. Create the project mizu new myapp --template api # 2. Enter the project directory cd myapp # 3. Download dependencies go mod tidy # 4. Start development mizu dev ``` ### Preview Before Creating If you're unsure what a template contains: ```bash theme={null} # See what will be created mizu new myapp --template api --dry-run # If it looks good, create it mizu new myapp --template api ``` ### Start Fresh If you want to reset a project to the template defaults: ```bash theme={null} # This overwrites all files mizu new . --template api --force ``` Using `--force` will overwrite your existing code! Make sure you have a backup or have committed your changes to git first. ### Scripted Project Creation For automation (CI/CD, scripts): ```bash theme={null} #!/bin/bash mizu new myapp --template api --module "github.com/company/myapp" --json > result.json if [ $? -eq 0 ]; then echo "Project created successfully" cd myapp && go mod tidy else echo "Failed to create project" cat result.json exit 1 fi ``` ## Template Variables Templates have access to these built-in variables: | Variable | Description | Example | | --------- | ------------------ | --------------------- | | `Name` | Project name | `myapp` | | `Module` | Go module path | `github.com/me/myapp` | | `License` | License identifier | `MIT` | | `Year` | Current year | `2024` | Plus any custom variables you pass with `--var`. ## Error Messages ### Unknown Template ``` error: unknown template "badname" Run 'mizu new --list' to see available templates. ``` **Fix:** Use `mizu new --list` to see valid template names. ### Files Already Exist ``` error: files already exist: go.mod main.go Use --force to overwrite. ``` **Fix:** Either use `--force` to overwrite, or choose a different path. ### Invalid Path ``` error: path "/root/project" is not writable ``` **Fix:** Choose a path where you have write permissions. ## Exit Codes | Code | Meaning | | ---- | ---------------------------------------------- | | 0 | Success | | 1 | Error (template error, file system error) | | 2 | Usage error (missing flags, invalid arguments) | ## Next Steps After creating a project: Start the development server Learn what each template includes # Overview Source: https://docs.go-mizu.dev/cli/overview Learn what the Mizu CLI is and how it helps you build web applications faster The Mizu CLI (Command Line Interface) is a tool that helps you create and run Mizu web applications. If you've never used a CLI before, don't worry - this guide will walk you through everything step by step. A CLI is a program you interact with by typing commands in your terminal (also called command prompt, shell, or console). Instead of clicking buttons in a graphical interface, you type text commands and press Enter. The terminal is the black (or white) window where you type commands - every computer has one built in. ## What Does the Mizu CLI Do? The Mizu CLI helps you with two main tasks: 1. **Creating new projects** - Instead of creating files and folders manually, the CLI generates a complete project structure for you 2. **Running your application** - The CLI can start your web server with a single command Think of it like a helpful assistant that handles the boring setup work so you can focus on building your application. ## Quick Start Example Let's create your first Mizu application. Open your terminal and run these commands: ```bash theme={null} # Step 1: Create a new project mizu new myapp --template minimal # Step 2: Go into the project folder cd myapp # Step 3: Download dependencies go mod tidy # Step 4: Start the server mizu dev ``` You should see output like this: ``` Starting ./cmd/server... ``` Now open your browser and go to `http://localhost:8080`. You'll see your application running. Press `Ctrl+C` in the terminal to stop the server. ## Commands The Mizu CLI has five commands. Each command does one specific thing, making it easy to remember what to type. | Command | What it does | | ----------------- | ----------------------------------------------- | | `mizu new` | Creates a new project from a template | | `mizu dev` | Runs your project in development mode | | `mizu contract` | Works with service contracts (for API projects) | | `mizu middleware` | Explores available middlewares | | `mizu version` | Shows the CLI version | ### Getting Help You can add `--help` to any command to see what options are available: ```bash theme={null} # See all commands mizu --help # See options for creating a new project mizu new --help # See options for the dev command mizu dev --help ``` ## Choosing a Template When you create a new project with `mizu new`, you choose a template. Each template gives you a different starting point: | Template | Best for | Difficulty | | ---------- | ------------------------------------- | ------------ | | `minimal` | Learning Mizu, quick experiments | Beginner | | `api` | JSON APIs, backend services | Beginner | | `contract` | Multi-protocol APIs (REST + JSON-RPC) | Intermediate | | `web` | Server-rendered websites with HTML | Intermediate | | `live` | Real-time apps (chat, dashboards) | Intermediate | | `sync` | Offline-first apps with data sync | Advanced | **Not sure which to pick?** Start with `minimal` to learn the basics, then try `api` when you want to build a real project. To see all available templates: ```bash theme={null} mizu new --list ``` Output: ``` Template Purpose api JSON API service with a recommended layout contract Transport-neutral service contract with REST, JSON-RPC, and OpenAPI live Real-time interactive app with live views and instant updates minimal Smallest runnable Mizu project sync Offline-first app with sync, live updates, and reactive state web Full-stack web app with views, components, and static assets ``` ## How the CLI Works When you run `mizu new myapp --template api`, here's what happens: 1. The CLI reads the template files 2. It creates the `myapp` folder 3. It generates all the project files with your project name filled in 4. It sets up the correct Go module path When you run `mizu dev`, here's what happens: 1. The CLI looks for your main package (usually in `cmd/` folder) 2. It compiles and runs your Go code 3. It watches for `Ctrl+C` to shut down gracefully ## Design Philosophy The Mizu CLI follows these principles: * **Convention over Configuration** - It works with sensible defaults, so you don't need to configure everything * **Explicit when needed** - You can override any default with flags * **Machine-friendly** - All commands support `--json` output for scripts and automation * **Clear errors** - When something goes wrong, you get helpful messages explaining how to fix it ## Next Steps Get the Mizu CLI installed on your computer Learn all the options for creating projects See what each template includes Explore 100+ available middlewares # Overview Source: https://docs.go-mizu.dev/cli/sync/overview Offline-first application with data synchronization The sync template creates offline-first applications where data syncs automatically between client and server. "Offline-first" means your app works even without internet - changes are saved locally and sent to the server when you're back online. This is great for mobile apps, field work, or anywhere with unreliable connections. The sync engine handles the complex work of merging changes from multiple devices. ## When to Use This Template Use the sync template when: * **Offline support** - App works without network * **Collaborative editing** - Multiple users edit same data * **Optimistic updates** - UI updates before server confirms * **Conflict resolution** - Automatic merge of concurrent changes ## What You Get * **Sync engine** - Handles data synchronization * **Operation log** - Tracks changes for replay * **Deduplication** - Prevents duplicate operations * **Live updates** - Real-time broadcast of changes * **Snapshot support** - Fast initial load ## Quick Start ```bash theme={null} mizu new myapp --template sync cd myapp go mod tidy mizu dev ``` Open `http://localhost:8080` to see a syncing todo list. ## How It Works ### 1. Client Makes Change User adds a todo item (works offline). ### 2. Operation Logged Change is recorded as an operation. ### 3. Sync to Server When online, operation is sent to server. ### 4. Server Applies Server validates and applies the operation. ### 5. Broadcast Change is broadcast to all connected clients. ### 6. UI Updates All clients see the new todo. ## Project Structure ``` myapp/ ├── cmd/server/main.go # Entry point ├── app/server/ │ ├── app.go # App + sync setup │ ├── config.go # Configuration │ └── routes.go # Routes ├── handler/ │ └── home.go # Home page ├── service/ │ └── todo/ │ └── mutator.go # Todo operations ├── assets/ # Static files ├── views/ # Templates └── go.mod ``` ## Key Concepts ### Operations Changes are expressed as operations: ```go theme={null} type Operation struct { ID string `json:"id"` Type string `json:"type"` // "create", "update", "delete" Args json.RawMessage `json:"args"` } ``` ### Apply Function Server applies operations to state: ```go theme={null} func (s *Store) Apply(ctx context.Context, m sync.Mutation) ([]sync.Change, error) { switch m.Type { case "create": // Create todo case "update": // Update todo case "delete": // Delete todo } return changes, nil } ``` ### Snapshots Fast initial load with current state: ```go theme={null} func (s *Store) Snapshot(ctx context.Context, scope string) (json.RawMessage, uint64, error) { todos := s.GetAll(scope) data, _ := json.Marshal(todos) return data, s.version, nil } ``` ## Example: Todo App The template includes a todo list that syncs: ```go theme={null} // Create a todo mutation := sync.Mutation{ Type: "todo.create", Args: json.RawMessage(`{"title":"Buy milk"}`), } // Apply and broadcast changes, err := store.Apply(ctx, mutation) syncEngine.Broadcast(scope, changes) ``` ## Benefits ### Works Offline Operations queue locally, sync when online. ### Optimistic UI Changes appear immediately, confirmed later. ### Conflict-Free Operations merge automatically using timestamps. ### Efficient Sync Only operations are sent, not full state. ## Next Steps Understand every file Build a syncing app # Structure Source: https://docs.go-mizu.dev/cli/sync/structure Understanding the sync template organization The sync template combines view rendering, live updates, and data synchronization. The key component is the "sync engine" which tracks changes as operations, replays them to rebuild state, and broadcasts updates to all connected clients. This is how collaborative apps like Google Docs work. ## Directory Layout ``` myapp/ ├── cmd/server/main.go # Entry point ├── app/server/ │ ├── app.go # App + sync engine setup │ ├── config.go # Configuration │ └── routes.go # HTTP + sync routes ├── handler/ │ └── home.go # Home page handler ├── service/ │ └── todo/ │ └── mutator.go # Todo store and apply logic ├── assets/ │ ├── embed.go # Asset embedding │ ├── css/style.css # Styles │ └── js/sync.js # Sync client ├── views/ │ ├── layouts/main.html # Main layout │ └── pages/home.html # Home page ├── go.mod └── .gitignore ``` ## Core Files ### app/server/app.go ```go theme={null} package server import ( "github.com/go-mizu/mizu" "github.com/go-mizu/mizu/live" "github.com/go-mizu/mizu/sync" synchttp "github.com/go-mizu/mizu/sync/http" "github.com/go-mizu/mizu/sync/memory" "github.com/go-mizu/mizu/view" "example.com/myapp/service/todo" ) type App struct { cfg Config app *mizu.App engine *view.Engine syncEngine *sync.Engine syncTransport *synchttp.Transport liveServer *live.Server store *todo.Store } func New(cfg Config) *App { a := &App{cfg: cfg} a.app = mizu.New() a.store = todo.NewStore() a.setupView() a.setupSync() a.setupLive() a.routes() return a } func (a *App) setupView() { a.engine = view.New(view.Options{ Root: "views", Layout: "layouts/main", }) a.app.Use(a.engine.Middleware()) } func (a *App) setupSync() { log := memory.NewLog() dedupe := memory.NewDedupe() a.syncEngine = sync.New(sync.Options{ Log: log, Apply: a.store.Apply, Snapshot: a.store.Snapshot, Dedupe: dedupe, }) a.syncTransport = synchttp.New(synchttp.Options{ Engine: a.syncEngine, }) } func (a *App) setupLive() { a.liveServer = live.NewServer(live.Options{}) } ``` ### service/todo/mutator.go ```go theme={null} package todo import ( "context" "encoding/json" "fmt" gosync "sync" "github.com/go-mizu/mizu/sync" ) type Store struct { mu gosync.RWMutex todos map[string]map[string]*Todo // scope -> id -> todo } type Todo struct { ID string `json:"id"` Title string `json:"title"` Completed bool `json:"completed"` } func NewStore() *Store { return &Store{ todos: make(map[string]map[string]*Todo), } } // Apply handles mutations (sync.ApplyFunc) func (s *Store) Apply(ctx context.Context, m sync.Mutation) ([]sync.Change, error) { s.mu.Lock() defer s.mu.Unlock() scope := m.Scope if s.todos[scope] == nil { s.todos[scope] = make(map[string]*Todo) } switch m.Type { case "todo.create": return s.createTodo(scope, m.Args) case "todo.toggle": return s.toggleTodo(scope, m.Args) case "todo.delete": return s.deleteTodo(scope, m.Args) default: return nil, fmt.Errorf("unknown mutation: %s", m.Type) } } func (s *Store) createTodo(scope string, args json.RawMessage) ([]sync.Change, error) { var input struct { ID string `json:"id"` Title string `json:"title"` } if err := json.Unmarshal(args, &input); err != nil { return nil, err } todo := &Todo{ ID: input.ID, Title: input.Title, Completed: false, } s.todos[scope][todo.ID] = todo data, _ := json.Marshal(s.todos[scope]) return []sync.Change{{Scope: scope, Data: data}}, nil } // Snapshot returns current state (sync.SnapshotFunc) func (s *Store) Snapshot(ctx context.Context, scope string) (json.RawMessage, uint64, error) { s.mu.RLock() defer s.mu.RUnlock() todos := s.GetAll(scope) data, _ := json.Marshal(todos) return data, 0, nil } // GetAll returns all todos for a scope func (s *Store) GetAll(scope string) []*Todo { s.mu.RLock() defer s.mu.RUnlock() if s.todos[scope] == nil { return []*Todo{} } todos := make([]*Todo, 0, len(s.todos[scope])) for _, t := range s.todos[scope] { todos = append(todos, t) } return todos } ``` ### app/server/routes.go ```go theme={null} package server import "example.com/myapp/handler" func (a *App) routes() { a.app.Mount("/static/", staticHandler(a.cfg.Dev)) a.app.Get("/", handler.Home(a.store)) a.syncTransport.Mount(a.app) // Mounts at /_sync/* a.app.Mount("/ws", a.liveServer.Handler()) } ``` ### assets/js/sync.js ```javascript theme={null} class SyncClient { constructor(url) { this.url = url; this.pending = []; } async mutate(type, args) { const mutation = { id: crypto.randomUUID(), type, args, }; // Optimistic: apply locally this.applyLocal(mutation); // Queue for sync this.pending.push(mutation); await this.sync(); } async sync() { if (this.pending.length === 0) return; try { const res = await fetch(this.url + '/_sync/push', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mutations: this.pending }), }); if (res.ok) { this.pending = []; } } catch (e) { // Will retry later } } } ``` ## Sync Flow 1. **Client Mutation** - User action creates mutation 2. **Local Apply** - UI updates optimistically 3. **Push to Server** - Mutation sent to `/_sync/push` 4. **Server Apply** - Store.Apply processes mutation 5. **Broadcast** - Changes sent to all clients 6. **UI Sync** - All clients update ## Next Steps Build a syncing todo app Deep dive into sync # Tutorial Source: https://docs.go-mizu.dev/cli/sync/tutorial Build an offline-first todo app with data synchronization In this tutorial, you'll build a todo application that works offline and syncs automatically when reconnected. Offline-first apps are essential for mobile users and anyone with unreliable connections. Your app stays responsive even without internet, and changes from multiple users merge seamlessly when connectivity returns. ## Step 1: Create the Project ```bash theme={null} mizu new synctodo --template sync cd synctodo go mod tidy mizu dev ``` Open `http://localhost:8080` to see the default syncing todo app. ## Step 2: Understand the Default App The template includes a working todo list. Let's trace how data flows. ### The Store Look at `service/todo/mutator.go`: ```go theme={null} type Store struct { mu gosync.RWMutex todos map[string]map[string]*Todo // scope -> id -> todo } type Todo struct { ID string `json:"id"` Title string `json:"title"` Completed bool `json:"completed"` } ``` The store uses a mutex for thread safety and organizes todos by scope (like a namespace). ### The Apply Function This is the heart of sync - it processes mutations: ```go theme={null} func (s *Store) Apply(ctx context.Context, m sync.Mutation) ([]sync.Change, error) { switch m.Type { case "todo.create": return s.createTodo(scope, m.Args) case "todo.toggle": return s.toggleTodo(scope, m.Args) case "todo.delete": return s.deleteTodo(scope, m.Args) } return nil, fmt.Errorf("unknown mutation: %s", m.Type) } ``` Each mutation type maps to a specific operation. ### The Snapshot Function Returns current state for new clients: ```go theme={null} func (s *Store) Snapshot(ctx context.Context, scope string) (json.RawMessage, uint64, error) { todos := s.GetAll(scope) data, _ := json.Marshal(todos) return data, 0, nil } ``` ## Step 3: Test Offline Behavior 1. Open the app in your browser 2. Add some todos 3. Open DevTools → Network → check "Offline" 4. Add more todos (they appear immediately!) 5. Uncheck "Offline" 6. Watch todos sync to server The key insight: the UI updates **before** the server confirms. ## Step 4: Add Priority Feature Let's add priority levels to todos. ### Update the Model Edit `service/todo/mutator.go`: ```go theme={null} type Todo struct { ID string `json:"id"` Title string `json:"title"` Completed bool `json:"completed"` Priority string `json:"priority"` // "low", "medium", "high" } ``` ### Update Create Function ```go theme={null} func (s *Store) createTodo(scope string, args json.RawMessage) ([]sync.Change, error) { var input struct { ID string `json:"id"` Title string `json:"title"` Priority string `json:"priority"` } if err := json.Unmarshal(args, &input); err != nil { return nil, err } // Default priority priority := input.Priority if priority == "" { priority = "medium" } todo := &Todo{ ID: input.ID, Title: input.Title, Completed: false, Priority: priority, } s.todos[scope][todo.ID] = todo data, _ := json.Marshal(s.todos[scope]) return []sync.Change{{Scope: scope, Data: data}}, nil } ``` ### Add Set Priority Mutation ```go theme={null} func (s *Store) Apply(ctx context.Context, m sync.Mutation) ([]sync.Change, error) { s.mu.Lock() defer s.mu.Unlock() scope := m.Scope if s.todos[scope] == nil { s.todos[scope] = make(map[string]*Todo) } switch m.Type { case "todo.create": return s.createTodo(scope, m.Args) case "todo.toggle": return s.toggleTodo(scope, m.Args) case "todo.delete": return s.deleteTodo(scope, m.Args) case "todo.setPriority": return s.setPriority(scope, m.Args) default: return nil, fmt.Errorf("unknown mutation: %s", m.Type) } } func (s *Store) setPriority(scope string, args json.RawMessage) ([]sync.Change, error) { var input struct { ID string `json:"id"` Priority string `json:"priority"` } if err := json.Unmarshal(args, &input); err != nil { return nil, err } todo := s.todos[scope][input.ID] if todo == nil { return nil, fmt.Errorf("todo not found: %s", input.ID) } todo.Priority = input.Priority data, _ := json.Marshal(s.todos[scope]) return []sync.Change{{Scope: scope, Data: data}}, nil } ``` ### Update the JavaScript Client Edit `assets/js/sync.js` to handle priority: ```javascript theme={null} class SyncClient { constructor(url, scope) { this.url = url; this.scope = scope; this.pending = []; this.state = []; } async createTodo(title, priority = 'medium') { await this.mutate('todo.create', { id: crypto.randomUUID(), title, priority, }); } async setPriority(id, priority) { await this.mutate('todo.setPriority', { id, priority }); } async mutate(type, args) { const mutation = { id: crypto.randomUUID(), type, args, scope: this.scope, }; // Optimistic update this.applyLocal(mutation); this.render(); // Queue and sync this.pending.push(mutation); await this.sync(); } applyLocal(mutation) { switch (mutation.type) { case 'todo.create': this.state.push({ id: mutation.args.id, title: mutation.args.title, completed: false, priority: mutation.args.priority || 'medium', }); break; case 'todo.setPriority': const todo = this.state.find(t => t.id === mutation.args.id); if (todo) todo.priority = mutation.args.priority; break; } } render() { const container = document.getElementById('todos'); container.innerHTML = this.state .sort((a, b) => this.priorityOrder(a) - this.priorityOrder(b)) .map(todo => `
${todo.title}
`).join(''); } priorityOrder(todo) { return { high: 0, medium: 1, low: 2 }[todo.priority]; } async sync() { if (this.pending.length === 0) return; try { const res = await fetch(this.url + '/_sync/push', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ scope: this.scope, mutations: this.pending, }), }); if (res.ok) { const { changes } = await res.json(); if (changes && changes.length > 0) { this.state = JSON.parse(changes[0].data); this.render(); } this.pending = []; } } catch (e) { console.log('Sync failed, will retry:', e); } } } // Initialize const sync = new SyncClient('', 'default'); ``` ## Step 5: Add Conflict Resolution When two users edit the same todo, we need to handle conflicts. ### Last-Write-Wins Strategy The simplest approach - later mutations win: ```go theme={null} func (s *Store) setPriority(scope string, args json.RawMessage) ([]sync.Change, error) { var input struct { ID string `json:"id"` Priority string `json:"priority"` Timestamp int64 `json:"timestamp"` } if err := json.Unmarshal(args, &input); err != nil { return nil, err } todo := s.todos[scope][input.ID] if todo == nil { return nil, fmt.Errorf("todo not found: %s", input.ID) } // Only apply if newer (last-write-wins) if input.Timestamp > todo.UpdatedAt { todo.Priority = input.Priority todo.UpdatedAt = input.Timestamp } data, _ := json.Marshal(s.todos[scope]) return []sync.Change{{Scope: scope, Data: data}}, nil } ``` Update the Todo struct: ```go theme={null} type Todo struct { ID string `json:"id"` Title string `json:"title"` Completed bool `json:"completed"` Priority string `json:"priority"` UpdatedAt int64 `json:"updatedAt"` } ``` ## Step 6: Add Multi-User Support Open the app in two browser windows to test collaboration. ### Add User Tracking ```go theme={null} // In app/server/app.go func (a *App) setupSync() { log := memory.NewLog() dedupe := memory.NewDedupe() a.syncEngine = sync.New(sync.Options{ Log: log, Apply: a.store.Apply, Snapshot: a.store.Snapshot, Dedupe: dedupe, OnChange: func(scope string, changes []sync.Change) { // Broadcast to all connected clients via live server for _, change := range changes { a.liveServer.Publish(scope, "sync", change.Data) } }, }) } ``` ### Client Listens for Updates ```javascript theme={null} class SyncClient { constructor(url, scope) { this.url = url; this.scope = scope; this.pending = []; this.state = []; this.connectWebSocket(); } connectWebSocket() { this.ws = new WebSocket(`ws://${location.host}/ws`); this.ws.onmessage = (e) => { const { topic, data } = JSON.parse(e.data); if (topic === 'sync') { this.state = JSON.parse(data); this.render(); } }; this.ws.onopen = () => { // Subscribe to our scope this.ws.send(JSON.stringify({ topic: 'subscribe', data: this.scope, })); }; this.ws.onclose = () => { setTimeout(() => this.connectWebSocket(), 1000); }; } } ``` ## How Sync Works ``` User A (offline) Server User B (online) | | | | --- Create todo -------- | (queued locally) | | | | | (goes online) | | | | | | --- Push mutations ----> | | | | --- Apply -----------> | | | --- Broadcast -------> | | | | (UI updates) | <-- Confirm ------------ | | | | | ``` ## What You Learned * Mutation-based data changes * Optimistic UI updates * Offline queueing and sync * Conflict resolution strategies * Multi-user broadcasting ## Next Steps Deep dive into sync internals Add real-time features without full sync # Templates Source: https://docs.go-mizu.dev/cli/templates Choose the right template for your Mizu project When you create a new project with `mizu new`, you choose a template. Templates are starter projects that give you a solid foundation with all the boilerplate already set up. Each template is designed for a specific use case, from simple learning projects to complex real-time applications. ## Available Templates The Mizu CLI includes 6 built-in templates: | Template | Description | Best For | | ------------ | ------------------------- | ----------------------------------- | | **minimal** | Smallest runnable project | Learning, quick experiments | | **api** | JSON API service | REST APIs, backend services | | **contract** | Multi-protocol service | APIs with REST + JSON-RPC | | **web** | Full-stack web app | Server-rendered websites | | **live** | Real-time app | Chat, dashboards, live updates | | **sync** | Offline-first app | Collaborative apps, offline support | ## List Templates See all available templates: ```bash theme={null} mizu new --list ``` Output: ``` Template Purpose api JSON API service with a recommended layout contract Transport-neutral service contract with REST, JSON-RPC, and OpenAPI live Real-time interactive app with live views and instant updates minimal Smallest runnable Mizu project sync Offline-first app with sync, live updates, and reactive state web Full-stack web app with views, components, and static assets ``` ## Which Template Should I Use? Use this decision guide to pick the right template: ### Are you learning Mizu? **Use `minimal`** - It has the least amount of code, making it easy to understand how Mizu works. ```bash theme={null} mizu new learn-mizu --template minimal ``` ### Are you building a REST API? **Use `api`** - It includes a recommended project structure with feature-based organization. ```bash theme={null} mizu new my-api --template api ``` ### Do you need multiple protocols (REST + JSON-RPC)? **Use `contract`** - Define your API once, expose it via multiple transports. ```bash theme={null} mizu new my-service --template contract ``` ### Are you building a website with HTML pages? **Use `web`** - Includes view templates, layouts, and static asset handling. ```bash theme={null} mizu new my-website --template web ``` ### Do you need real-time updates (like chat)? **Use `live`** - Includes WebSocket support and live view rendering. ```bash theme={null} mizu new my-app --template live ``` ### Do you need offline support with data sync? **Use `sync`** - The most advanced template with offline-first architecture. ```bash theme={null} mizu new my-app --template sync ``` ## Template Comparison ### Complexity Level ``` minimal ─────────────────────────────────────► sync | | | | | simple api web/live contract advanced ``` ### Features Included | Feature | minimal | api | contract | web | live | sync | | ----------------------- | :-----: | :-: | :------: | :-: | :--: | :--: | | Single file | Yes | - | - | - | - | - | | Feature-based structure | - | Yes | Yes | Yes | Yes | Yes | | JSON responses | Yes | Yes | Yes | Yes | Yes | Yes | | HTML templates | - | - | - | Yes | Yes | Yes | | Static assets | - | - | - | Yes | Yes | Yes | | JSON-RPC | - | - | Yes | - | - | - | | OpenAPI generation | - | - | Yes | - | - | - | | WebSocket | - | - | - | - | Yes | Yes | | Live updates | - | - | - | - | Yes | Yes | | Offline support | - | - | - | - | - | Yes | | Data sync | - | - | - | - | - | Yes | ## Common Patterns ### Starting Simple, Then Growing Start with `minimal` to learn, then create a new project with `api`: ```bash theme={null} # Day 1: Learn Mizu mizu new learn --template minimal cd learn go mod tidy mizu dev # Day 2: Build a real project cd .. mizu new my-api --template api cd my-api go mod tidy mizu dev ``` ### Prototyping to Production Start with `minimal` for a quick prototype, then migrate to `api` for production: 1. Build your prototype with `minimal` 2. Validate your idea works 3. Create a new `api` project 4. Copy your business logic over 5. Enjoy the better structure ### Frontend + Backend For apps with both a frontend and backend: * **API only (frontend in React/Vue):** Use `api` * **Server-rendered HTML:** Use `web` * **Real-time features:** Use `live` or `sync` ## Template Architecture All templates share these principles: ### Embedded in CLI Templates are built into the CLI binary. This means: * No network required to create projects * Templates always match CLI version * Works offline ### Go Module Ready Every template creates a valid `go.mod` file: ```go theme={null} module {{.Module}} go 1.22 require github.com/go-mizu/mizu v0.x.x ``` ### Common Files All templates include: | File | Purpose | | ------------ | --------------------- | | `go.mod` | Go module definition | | `.gitignore` | Git ignore rules | | `README.md` | Project documentation | ## Creating Projects ### Basic Creation ```bash theme={null} mizu new myapp --template api cd myapp go mod tidy mizu dev ``` ### With Custom Module Path ```bash theme={null} mizu new myapp --template api --module github.com/myname/myapp ``` ### Preview Before Creating ```bash theme={null} mizu new myapp --template api --dry-run ``` ## Explore Each Template Single-file starting point for learning JSON API with feature-based structure Multi-protocol service with OpenAPI Full-stack web app with views Real-time app with WebSockets Offline-first app with data sync ## Next Steps * Pick a template and explore its detailed documentation * Run `mizu new --template --dry-run` to preview any template * Check out the tutorials for hands-on guides # Version Source: https://docs.go-mizu.dev/cli/version Display version and build information about the Mizu CLI The `mizu version` command shows information about the installed Mizu CLI, including the version number, Go version, and build details. Use this command to verify your installation works or to include version info in bug reports. ## Basic Usage ```bash theme={null} mizu version ``` Output: ``` mizu version 0.1.0 go version: go1.23.0 commit: abc1234 built: 2024-01-15T10:30:00Z ``` ## What Each Field Means | Field | Description | Example | | ------------ | --------------------------------- | ---------------------- | | `version` | The semantic version of the CLI | `0.1.0` | | `go version` | The Go version used to compile | `go1.23.0` | | `commit` | Short Git commit hash | `abc1234` | | `built` | When this version was built (UTC) | `2024-01-15T10:30:00Z` | ## Flags | Flag | Description | | ------------ | -------------- | | `--json` | Output as JSON | | `-h, --help` | Show help | ## JSON Output For scripts and automation, use `--json`: ```bash theme={null} mizu version --json ``` Output: ```json theme={null} { "version": "0.1.0", "go_version": "go1.23.0", "commit": "abc1234", "built_at": "2024-01-15T10:30:00Z" } ``` ## When to Use This Command ### Verify Installation After installing the CLI, confirm it's working: ```bash theme={null} mizu version ``` If you see version information, the CLI is installed correctly. ### Check for Updates Compare your version with the latest release: ```bash theme={null} # Check your version mizu version # Update to latest go install github.com/go-mizu/mizu/cmd/mizu@latest # Verify update mizu version ``` ### Include in Bug Reports When reporting issues, include your version information: ```bash theme={null} mizu version --json > version.json ``` Share this file with your bug report so maintainers know your exact setup. ### CI/CD Version Checks Ensure a minimum version in build scripts: ```bash theme={null} #!/bin/bash # Get the version VERSION=$(mizu version --json | jq -r '.version') # Compare versions (requires semver comparison) if [[ "$VERSION" == "dev" ]]; then echo "Warning: Using development build" fi echo "Using mizu $VERSION" ``` ## Development Builds If you built from source without setting version flags, you'll see: ``` mizu version dev go version: go1.23.0 commit: unknown built: unknown ``` This is normal for local development builds. ## Setting Build Information When building the CLI, you can embed version information using Go's `-ldflags`: ```bash theme={null} go build -ldflags "\ -X github.com/go-mizu/mizu/cli.Version=1.0.0 \ -X github.com/go-mizu/mizu/cli.Commit=$(git rev-parse --short HEAD) \ -X github.com/go-mizu/mizu/cli.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ -o mizu ./cmd/mizu ``` This is useful for: * Release builds * CI/CD pipelines * Custom distribution ### Variables | Variable | Default | | --------------- | --------- | | `cli.Version` | `dev` | | `cli.Commit` | `unknown` | | `cli.BuildTime` | `unknown` | ## Exit Codes | Code | Description | | ---- | ---------------- | | 0 | Success (always) | The version command always succeeds if the CLI is running. ## Troubleshooting ### "command not found: mizu" The CLI isn't installed or isn't in your PATH. See [Installation](/cli/installation). ### Version Shows "dev" You're running a development build. This is fine for testing but you may want to install a release version: ```bash theme={null} go install github.com/go-mizu/mizu/cmd/mizu@latest ``` ## Next Steps Learn how to install or update the CLI Start building with Mizu # Overview Source: https://docs.go-mizu.dev/cli/web/overview Full-stack web application with views and static assets The web template creates a full-stack web application with server-rendered HTML. Unlike single-page apps (SPAs) where JavaScript renders the page in the browser, server-rendered apps generate complete HTML on the server. This is great for SEO, fast initial loads, and simpler architecture. You write templates that combine HTML with your data, and the server sends the finished page to the browser. ## When to Use This Template Use the web template when: * **Building a website** - Server-rendered HTML pages * **Need layouts** - Shared headers, footers, navigation * **Static assets** - CSS, JavaScript, images * **SEO matters** - Search engine friendly HTML ## What You Get * **View engine** - Template rendering with layouts * **Static file serving** - Embedded or file-based assets * **Page handlers** - Clean separation of routes and views * **Component system** - Reusable UI pieces ## Quick Start ```bash theme={null} mizu new mysite --template web cd mysite go mod tidy mizu dev ``` Open `http://localhost:8080` in your browser. ## Project Structure ``` mysite/ ├── cmd/ │ └── web/ │ └── main.go # Entry point ├── app/ │ └── web/ │ ├── app.go # Application setup │ ├── config.go # Configuration │ └── routes.go # Route registration ├── handler/ │ ├── home.go # Home page handler │ └── about.go # About page handler ├── assets/ │ ├── embed.go # Asset embedding │ ├── css/ │ │ └── style.css # Stylesheets │ └── js/ │ └── app.js # JavaScript ├── views/ │ ├── layouts/ │ │ └── main.html # Main layout │ ├── pages/ │ │ ├── home.html # Home page │ │ └── about.html # About page │ └── partials/ │ └── nav.html # Navigation partial ├── go.mod └── .gitignore ``` ## Key Features ### View Engine Renders HTML templates with data: ```go theme={null} func Home() mizu.Handler { return func(c *mizu.Ctx) error { return c.Render("pages/home", map[string]any{ "Title": "Welcome", "Items": []string{"One", "Two", "Three"}, }) } } ``` ### Layouts Wrap pages in a consistent layout: ```html theme={null} {{.Title}} {{template "partials/nav" .}}
{{template "content" .}}
``` ### Static Assets Serve CSS, JS, and images: ```go theme={null} app.Mount("/static/", staticHandler()) ``` Assets are embedded in the binary for easy deployment. ## Handlers ```go theme={null} // handler/home.go package handler import "github.com/go-mizu/mizu" func Home() mizu.Handler { return func(c *mizu.Ctx) error { return c.Render("pages/home", map[string]any{ "Title": "Home", "Message": "Welcome to my site!", }) } } ``` ## Views ```html theme={null} {{define "content"}}

{{.Message}}

This is the home page.

{{end}} ``` ## Next Steps Understand every file Build a website step by step # Structure Source: https://docs.go-mizu.dev/cli/web/structure Understanding the web template files The web template provides a complete full-stack structure for server-rendered websites. "Full-stack" means it includes everything: server code (Go), templates (HTML), styles (CSS), and client-side code (JavaScript). This page explains how all the pieces fit together. ## Directory Layout ``` mysite/ ├── cmd/web/main.go # Entry point ├── app/web/ │ ├── app.go # Application setup │ ├── config.go # Configuration │ └── routes.go # Route registration ├── handler/ │ ├── home.go # Home page │ └── about.go # About page ├── assets/ │ ├── embed.go # Asset embedding │ ├── css/style.css # Styles │ └── js/app.js # JavaScript ├── views/ │ ├── layouts/main.html # Main layout │ ├── pages/ │ │ ├── home.html # Home page │ │ └── about.html # About page │ └── partials/nav.html # Navigation ├── go.mod └── .gitignore ``` ## Core Files ### app/web/app.go ```go theme={null} package web import ( "github.com/go-mizu/mizu" "github.com/go-mizu/mizu/view" ) type App struct { cfg Config app *mizu.App engine *view.Engine } func New(cfg Config) *App { a := &App{cfg: cfg} a.app = mizu.New() a.engine = view.New(view.Options{ Root: "views", Layout: "layouts/main", }) a.app.Use(a.engine.Middleware()) a.routes() return a } ``` ### app/web/routes.go ```go theme={null} package web import "example.com/mysite/handler" func (a *App) routes() { a.app.Mount("/static/", staticHandler(a.cfg.Dev)) a.app.Get("/", handler.Home()) a.app.Get("/about", handler.About()) } ``` ## Handler Files ### handler/home.go ```go theme={null} package handler import "github.com/go-mizu/mizu" func Home() mizu.Handler { return func(c *mizu.Ctx) error { return c.Render("pages/home", map[string]any{ "Title": "Home", "Message": "Welcome to my website!", }) } } ``` ## View Files ### views/layouts/main.html ```html theme={null} {{.Title}} | My Site {{template "partials/nav" .}}
{{template "content" .}}
``` ### views/pages/home.html ```html theme={null} {{define "content"}}

{{.Message}}

This is the home page content.

{{end}} ``` ### views/partials/nav.html ```html theme={null} ``` ## Asset Files ### assets/embed.go ```go theme={null} package assets import "embed" //go:embed css js var FS embed.FS ``` This embeds CSS and JS files into the binary. ### assets/css/style.css ```css theme={null} * { box-sizing: border-box; } body { font-family: system-ui, sans-serif; line-height: 1.6; margin: 0; padding: 0; } .container { max-width: 800px; margin: 0 auto; padding: 2rem; } nav { background: #333; padding: 1rem; } nav a { color: white; margin-right: 1rem; } ``` ## Adding a New Page 1. Create handler: `handler/contact.go` 2. Create view: `views/pages/contact.html` 3. Add route: `app/web/routes.go` ```go theme={null} // handler/contact.go package handler import "github.com/go-mizu/mizu" func Contact() mizu.Handler { return func(c *mizu.Ctx) error { return c.Render("pages/contact", map[string]any{ "Title": "Contact", }) } } ``` ```html theme={null} {{define "content"}}

Contact Us

{{end}} ``` ```go theme={null} // routes.go a.app.Get("/contact", handler.Contact()) ``` ## Next Steps Build a complete website Learn more about templates # Tutorial Source: https://docs.go-mizu.dev/cli/web/tutorial Build a server-rendered website In this tutorial, you'll build a simple blog with pages, a layout, and styling. Server-rendered websites are great for content sites, blogs, and applications where SEO matters. The server generates complete HTML pages, which makes your site fast to load and easy for search engines to index. ## Step 1: Create the Project ```bash theme={null} mizu new myblog --template web cd myblog go mod tidy mizu dev ``` Open `http://localhost:8080` to see the default page. ## Step 2: Update the Layout Edit `views/layouts/main.html`: ```html theme={null} {{.Title}} | My Blog
{{template "content" .}}

© 2024 My Blog

``` ## Step 3: Create a Blog Post Handler Create `handler/posts.go`: ```go theme={null} package handler import "github.com/go-mizu/mizu" type Post struct { ID string Title string Summary string Content string Date string } var posts = []Post{ { ID: "1", Title: "Getting Started with Mizu", Summary: "Learn how to build web apps with Mizu", Content: "Mizu is a lightweight web framework for Go...", Date: "January 15, 2024", }, { ID: "2", Title: "Building APIs", Summary: "Create REST APIs quickly", Content: "Building APIs with Mizu is straightforward...", Date: "January 10, 2024", }, } func PostList() mizu.Handler { return func(c *mizu.Ctx) error { return c.Render("pages/posts", map[string]any{ "Title": "Blog Posts", "Posts": posts, }) } } func PostShow() mizu.Handler { return func(c *mizu.Ctx) error { id := c.Param("id") for _, p := range posts { if p.ID == id { return c.Render("pages/post", map[string]any{ "Title": p.Title, "Post": p, }) } } return c.Text(404, "Post not found") } } ``` ## Step 4: Create Post Views Create `views/pages/posts.html`: ```html theme={null} {{define "content"}}

{{.Title}}

{{range .Posts}} {{end}}
{{end}} ``` Create `views/pages/post.html`: ```html theme={null} {{define "content"}} {{end}} ``` ## Step 5: Add Routes Update `app/web/routes.go`: ```go theme={null} package web import "example.com/myblog/handler" func (a *App) routes() { a.app.Mount("/static/", staticHandler(a.cfg.Dev)) // Pages a.app.Get("/", handler.PostList()) a.app.Get("/posts/:id", handler.PostShow()) a.app.Get("/about", handler.About()) } ``` ## Step 6: Add Styling Update `assets/css/style.css`: ```css theme={null} * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; } header { background: #1a1a2e; padding: 1rem 2rem; } nav { display: flex; justify-content: space-between; align-items: center; max-width: 800px; margin: 0 auto; } .logo { color: white; font-weight: bold; font-size: 1.5rem; text-decoration: none; } .nav-links a { color: #ccc; text-decoration: none; margin-left: 1.5rem; } .nav-links a:hover { color: white; } main { max-width: 800px; margin: 2rem auto; padding: 0 1rem; } .post-card { background: #f9f9f9; padding: 1.5rem; margin-bottom: 1.5rem; border-radius: 8px; } .post-card h2 a { color: #1a1a2e; text-decoration: none; } .date { color: #666; font-size: 0.9rem; margin: 0.5rem 0; } .read-more { color: #4a90d9; text-decoration: none; } .post header { margin-bottom: 2rem; padding-bottom: 1rem; border-bottom: 1px solid #eee; } footer { margin-top: 3rem; padding: 1rem 0; text-align: center; color: #666; border-top: 1px solid #eee; } ``` ## Step 7: Test It Restart the server and browse: * `http://localhost:8080/` - Post list * `http://localhost:8080/posts/1` - Single post * `http://localhost:8080/about` - About page ## What You Learned * Creating page handlers * Using templates with data * Layouts and partials * URL parameters in routes * Styling with CSS ## Next Steps Add real-time features Learn more about templates # Reference Source: https://docs.go-mizu.dev/contract/api-reference Complete reference for all Contract v2 functions, types, and options # API Reference This page documents all public functions, types, and options in the Contract v2 package. ## Main Package Import: `github.com/go-mizu/mizu/contract/v2` ### Functions #### Register Registers a Go interface implementation as a Contract service. ```go theme={null} func Register[T any](impl T, opts ...Option) *RegisteredService ``` | Parameter | Type | Description | | --------- | -------------- | ------------------------------------------------ | | `T` | type parameter | The interface type that defines your API | | `impl` | `T` | Your implementation that satisfies the interface | | `opts` | `...Option` | Optional configuration | **Returns:** `*RegisteredService` for mounting on transports. **Example:** ```go theme={null} // todo/api.go - Your interface is defined in a package // type API interface { // Create(ctx context.Context, in *CreateInput) (*Todo, error) // List(ctx context.Context) (*ListOutput, error) // } impl := todo.NewService() svc := contract.Register[todo.API](impl, contract.WithName("Todo"), contract.WithDefaultResource("todos"), ) ``` *** ### Types #### RegisteredService A registered service ready to be mounted on transports. ```go theme={null} type RegisteredService struct { // ... internal fields } ``` **Methods:** | Method | Returns | Description | | ------------------------------------ | -------------------- | -------------------------------- | | `Descriptor()` | `*ServiceDescriptor` | Get service metadata | | `Call(ctx, resource, method, input)` | `(any, error)` | Invoke a method programmatically | | `NewInput(resource, method)` | `(any, error)` | Create new input instance | *** #### ServiceDescriptor Metadata about a registered service. ```go theme={null} type ServiceDescriptor struct { Name string // Service name Description string // Optional description Resources []*Resource // All resources Types []*TypeInfo // All discovered types } ``` *** #### Resource A group of related methods (e.g., "todos"). ```go theme={null} type Resource struct { Name string // Resource name (e.g., "todos") Methods []*Method // Methods in this resource } ``` *** #### Method Metadata for a single method. ```go theme={null} type Method struct { Name string // Method name (e.g., "create") Description string // Optional description Input *TypeInfo // Input type (nil if no input) Output *TypeInfo // Output type (nil if no output) HTTP *HTTPBinding // HTTP method and path Stream StreamMode // Streaming mode if any } ``` *** #### TypeInfo Information about a type used in the API. ```go theme={null} type TypeInfo struct { Name string // Type name (e.g., "CreateInput") Kind string // Type kind (struct, slice, etc.) Schema map[string]any // JSON Schema } ``` *** #### HTTPBinding HTTP method and path for a method. ```go theme={null} type HTTPBinding struct { Method string // HTTP method (GET, POST, etc.) Path string // URL path pattern } ``` *** ### Options #### WithName Sets the service name: ```go theme={null} contract.WithName("Todo") ``` #### WithDescription Sets the service description: ```go theme={null} contract.WithDescription("Todo management API") ``` #### WithDefaultResource Groups all methods under a resource name: ```go theme={null} contract.WithDefaultResource("todos") ``` #### WithResource Groups specific methods under a resource: ```go theme={null} contract.WithResource("users", "CreateUser", "GetUser", "ListUsers") ``` #### WithMethodHTTP Override HTTP binding for a method: ```go theme={null} contract.WithMethodHTTP("Archive", "POST", "/todos/{id}/archive") ``` #### WithHTTP Set HTTP bindings for multiple methods: ```go theme={null} contract.WithHTTP(map[string]contract.HTTPBinding{ "Create": {Method: "POST", Path: "/v1/todos"}, "Get": {Method: "GET", Path: "/v1/todos/{id}"}, }) ``` #### WithDefaults Set global defaults: ```go theme={null} contract.WithDefaults(contract.Defaults{ BaseURL: "https://api.example.com", }) ``` #### WithStreaming Mark a method as streaming: ```go theme={null} contract.WithStreaming("Chat", contract.StreamSSE) ``` **Usage Example:** ```go theme={null} svc := contract.Register[TodoAPI](impl, contract.WithName("Todo"), contract.WithDescription("Manages todo items"), contract.WithDefaultResource("todos"), ) ``` *** ## REST Package Import: `github.com/go-mizu/mizu/contract/v2/transport/rest` ### Functions #### Mount Mounts REST endpoints on a router. ```go theme={null} func Mount(router *mizu.Router, svc *contract.RegisteredService, opts ...Option) ``` | Parameter | Type | Description | | --------- | -------------------- | ---------------------- | | `router` | `*mizu.Router` | Mizu router | | `svc` | `*RegisteredService` | Registered service | | `opts` | `...Option` | Optional configuration | **Endpoints created based on method names:** * `Create` -> `POST /{resource}` * `List` -> `GET /{resource}` * `Get` -> `GET /{resource}/{id}` * `Update` -> `PUT /{resource}/{id}` * `Delete` -> `DELETE /{resource}/{id}` **Example:** ```go theme={null} rest.Mount(app.Router, svc) // Creates: POST /todos, GET /todos, GET /todos/{id}, PUT /todos/{id}, DELETE /todos/{id} ``` *** #### NewHandler Creates an HTTP handler for the service. ```go theme={null} func NewHandler(svc *contract.RegisteredService, opts ...Option) http.Handler ``` *** ### Options ```go theme={null} // Custom invoker (for middleware) rest.WithInvoker(invoker) // Custom path prefix rest.WithPrefix("/api/v1") ``` *** ## JSON-RPC Package Import: `github.com/go-mizu/mizu/contract/v2/transport/jsonrpc` ### Functions #### Mount Mounts a JSON-RPC 2.0 endpoint. ```go theme={null} func Mount(router *mizu.Router, path string, svc *contract.RegisteredService, opts ...Option) ``` | Parameter | Type | Description | | --------- | -------------------- | ------------------------- | | `router` | `*mizu.Router` | Mizu router | | `path` | `string` | URL path for the endpoint | | `svc` | `*RegisteredService` | Registered service | | `opts` | `...Option` | Optional configuration | **Example:** ```go theme={null} jsonrpc.Mount(app.Router, "/rpc", svc) // Methods available as: todos.create, todos.list, todos.get, etc. ``` *** #### NewHandler Creates a JSON-RPC HTTP handler. ```go theme={null} func NewHandler(svc *contract.RegisteredService, opts ...Option) http.Handler ``` *** ### Options ```go theme={null} // Custom invoker (for middleware) jsonrpc.WithInvoker(invoker) ``` *** ### Types #### Request ```go theme={null} type Request struct { JSONRPC string `json:"jsonrpc"` ID any `json:"id,omitempty"` Method string `json:"method"` Params json.RawMessage `json:"params,omitempty"` } ``` #### Response ```go theme={null} type Response struct { JSONRPC string `json:"jsonrpc"` ID any `json:"id,omitempty"` Result any `json:"result,omitempty"` Error *Error `json:"error,omitempty"` } ``` #### Error ```go theme={null} type Error struct { Code int `json:"code"` Message string `json:"message"` Data any `json:"data,omitempty"` } ``` *** ## MCP Package Import: `github.com/go-mizu/mizu/contract/v2/transport/mcp` ### Functions #### Mount Mounts MCP endpoint. ```go theme={null} func Mount(router *mizu.Router, path string, svc *contract.RegisteredService, opts ...Option) ``` **Example:** ```go theme={null} mcp.Mount(app.Router, "/mcp", svc) ``` *** #### NewHandler Creates an MCP HTTP handler. ```go theme={null} func NewHandler(svc *contract.RegisteredService, opts ...Option) http.Handler ``` *** ### Options ```go theme={null} // Server information mcp.WithServerInfo(mcp.ServerInfo{ Name: "my-api", Version: "1.0.0", }) // Instructions for AI mcp.WithInstructions("Use these tools to manage todos.") // Allowed CORS origins mcp.WithAllowedOrigins("https://claude.ai") // Custom invoker (for middleware) mcp.WithInvoker(invoker) ``` *** ### Types #### ServerInfo ```go theme={null} type ServerInfo struct { Name string `json:"name"` Version string `json:"version"` } ``` *** ## OpenAPI Package Import: `github.com/go-mizu/mizu/contract/v2/transport/openapi` ### Functions #### Mount Mounts OpenAPI spec endpoint. ```go theme={null} func Mount(router *mizu.Router, path string, svc *contract.RegisteredService) ``` **Example:** ```go theme={null} openapi.Mount(app.Router, "/openapi.json", svc) ``` *** #### Generate Generates OpenAPI document from service. ```go theme={null} func Generate(svc *contract.RegisteredService) *Document ``` *** ## Error Types ### Error Contract error with code and message. ```go theme={null} type Error struct { Code ErrorCode `json:"code"` Message string `json:"message"` Details map[string]any `json:"details,omitempty"` } ``` **Methods:** | Method | Returns | Description | | ------------------------ | -------- | ---------------------------------- | | `Error()` | `string` | Error message (implements `error`) | | `HTTPStatus()` | `int` | HTTP status code | | `JSONRPCCode()` | `int` | JSON-RPC error code | | `Unwrap()` | `error` | Wrapped cause | | `WithDetail(key, value)` | `*Error` | Add detail | | `WithDetails(map)` | `*Error` | Add multiple details | | `WithCause(err)` | `*Error` | Wrap underlying error | *** ### Error Constructors Convenience functions for common errors: ```go theme={null} contract.ErrNotFound(message string) *Error // 404 contract.ErrInvalidArgument(message string) *Error // 400 contract.ErrPermissionDenied(message string) *Error // 403 contract.ErrUnauthenticated(message string) *Error // 401 contract.ErrInternal(message string) *Error // 500 contract.ErrAlreadyExists(message string) *Error // 409 contract.ErrResourceExhausted(message string) *Error // 429 contract.ErrUnimplemented(message string) *Error // 501 contract.ErrUnavailable(message string) *Error // 503 ``` General constructors: ```go theme={null} contract.NewError(code ErrorCode, message string) *Error contract.Errorf(code ErrorCode, format string, args ...any) *Error ``` *** ### ErrorCode All available error codes: ```go theme={null} type ErrorCode string const ( ErrCodeOK ErrorCode = "OK" ErrCodeCanceled ErrorCode = "CANCELED" ErrCodeUnknown ErrorCode = "UNKNOWN" ErrCodeInvalidArgument ErrorCode = "INVALID_ARGUMENT" ErrCodeDeadlineExceeded ErrorCode = "DEADLINE_EXCEEDED" ErrCodeNotFound ErrorCode = "NOT_FOUND" ErrCodeAlreadyExists ErrorCode = "ALREADY_EXISTS" ErrCodePermissionDenied ErrorCode = "PERMISSION_DENIED" ErrCodeResourceExhausted ErrorCode = "RESOURCE_EXHAUSTED" ErrCodeFailedPrecondition ErrorCode = "FAILED_PRECONDITION" ErrCodeAborted ErrorCode = "ABORTED" ErrCodeOutOfRange ErrorCode = "OUT_OF_RANGE" ErrCodeUnimplemented ErrorCode = "UNIMPLEMENTED" ErrCodeInternal ErrorCode = "INTERNAL" ErrCodeUnavailable ErrorCode = "UNAVAILABLE" ErrCodeDataLoss ErrorCode = "DATA_LOSS" ErrCodeUnauthenticated ErrorCode = "UNAUTHENTICATED" ) ``` *** ### Helper Functions ```go theme={null} // Convert error to Contract error contract.AsError(err error) *Error // Convert HTTP status to error code contract.HTTPStatusToErrorCode(status int) ErrorCode ``` *** ## Method Signatures Contract recognizes these method patterns: ### Pattern 1: Input and Output ```go theme={null} func (s *Service) Create(ctx context.Context, in *CreateInput) (*Todo, error) ``` ### Pattern 2: Output Only ```go theme={null} func (s *Service) List(ctx context.Context) (*ListOutput, error) ``` ### Pattern 3: Input Only ```go theme={null} func (s *Service) Delete(ctx context.Context, in *DeleteInput) error ``` ### Pattern 4: Neither ```go theme={null} func (s *Service) Health(ctx context.Context) error ``` **Rules:** * First parameter must be `context.Context` * Input (if any) must be a pointer to a struct * Last return value must be `error` * Output (if any) must be a pointer to a struct *** ## Complete Example This example shows a complete application using the recommended package-based organization: ```go theme={null} // todo/types.go package todo type Todo struct { ID string `json:"id"` Title string `json:"title"` } type CreateInput struct { Title string `json:"title"` } type GetInput struct { ID string `json:"id" path:"id"` } type DeleteInput struct { ID string `json:"id" path:"id"` } type ListOutput struct { Items []*Todo `json:"items"` } ``` ```go theme={null} // 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) Delete(ctx context.Context, in *DeleteInput) error } ``` ```go theme={null} // todo/service.go package todo import ( "context" contract "github.com/go-mizu/mizu/contract/v2" ) // Service implements todo.API type Service struct{} // Compile-time check var _ API = (*Service)(nil) func NewService() *Service { return &Service{} } func (s *Service) Create(ctx context.Context, in *CreateInput) (*Todo, error) { if in.Title == "" { return nil, contract.ErrInvalidArgument("title is required") } return &Todo{ID: "1", Title: in.Title}, nil } func (s *Service) List(ctx context.Context) (*ListOutput, error) { return &ListOutput{Items: []*Todo{}}, nil } func (s *Service) Get(ctx context.Context, in *GetInput) (*Todo, error) { return &Todo{ID: in.ID, Title: "Sample"}, nil } func (s *Service) Delete(ctx context.Context, in *DeleteInput) error { return nil } ``` ```go theme={null} // 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" "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/openapi" "yourapp/todo" ) func main() { impl := todo.NewService() // Register svc := contract.Register[todo.API](impl, contract.WithName("Todo"), contract.WithDefaultResource("todos"), ) // Create app app := mizu.New() // Mount transports rest.Mount(app.Router, svc) jsonrpc.Mount(app.Router, "/rpc", svc) mcp.Mount(app.Router, "/mcp", svc) openapi.Mount(app.Router, "/openapi.json", svc) app.Listen(":8080") } ``` ## See Also * [Quick Start](/contract/quick-start) - Get started quickly * [Services](/contract/service) - Method signatures in detail * [Errors](/contract/errors) - Error patterns * [Overview](/contract/transports-overview) - All transports # Architecture Source: https://docs.go-mizu.dev/contract/architecture Understand how Contract works under the hood - a beginner-friendly deep dive # Architecture This page explains how Contract works internally. Understanding the architecture helps you make better decisions when building your APIs and troubleshoot issues when they arise. ## The Big Picture At its core, Contract does one simple thing: it takes your plain Go code and makes it accessible via different network protocols. Here's how the pieces fit together: ``` ┌─────────────────────────────────────────────────────────────────────┐ │ YOUR CODE (Plain Go) │ │ ┌───────────────────────────────────────────────────────────────┐ │ │ │ package todo │ │ │ │ type Service struct { ... } │ │ │ │ func (s *Service) Create(...) (*Todo, error) │ │ │ │ func (s *Service) List(...) ([]*Todo, error) │ │ │ │ │ │ │ │ No HTTP. No JSON. No framework. Just business logic. │ │ │ └───────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────┘ │ │ contract.Register[todo.API](todo.NewService()) │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ CONTRACT (The Bridge) │ │ ┌───────────────────────────────────────────────────────────────┐ │ │ │ Service { │ │ │ │ Name: "todo" │ │ │ │ Methods: [Create, List, Get, Delete] │ │ │ │ Types: {Todo, CreateInput, ListOutput, ...} │ │ │ │ } │ │ │ │ │ │ │ │ Each method has an "Invoker" - a fast way to call it. │ │ │ │ Each type has a "Schema" - a description for documentation. │ │ │ └───────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────┘ │ ┌─────────────────────────┼─────────────────────────┐ │ │ │ ▼ ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ REST Handler │ │ JSON-RPC Handler│ │ MCP Handler │ │ │ │ │ │ │ │ POST /todos │ │ method: "Create"│ │ tool: "Create" │ │ GET /todos │ │ method: "List" │ │ tool: "List" │ │ GET /todos/{id} │ │ method: "Get" │ │ tool: "Get" │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │ │ └─────────────────────────┼─────────────────────────┘ │ ▼ ┌─────────────────┐ │ HTTP Requests │ │ from Clients │ └─────────────────┘ ``` ## Three Layers Explained ### Layer 1: Your Code (The Service) This is the Go code you write. It contains your business logic with no knowledge of HTTP, JSON, or any protocol: ```go theme={null} // todo/service.go package todo type Service struct { db *sql.DB // Your dependencies go here } func NewService(db *sql.DB) *Service { return &Service{db: db} } func (s *Service) Create(ctx context.Context, in *CreateInput) (*Todo, error) { // This is pure business logic // No http.Request, no JSON marshaling, no protocol concerns todo := &Todo{ ID: generateID(), Title: in.Title, } // Save to database _, err := s.db.ExecContext(ctx, "INSERT INTO todos...") if err != nil { return nil, err } return todo, nil } ``` **Why this matters**: Your code is easy to test (no HTTP mocking needed), easy to understand (just Go functions), and can be reused in different contexts (CLI tools, background jobs, etc.). ### Layer 2: The Contract (The Bridge) When you call `contract.Register()`, Contract inspects your service and creates a data structure that describes it: ```go theme={null} impl := todo.NewService(db) svc := contract.Register[todo.API](impl) ``` This registration process: 1. **Discovers methods** using Go's reflection 2. **Parses signatures** to understand inputs and outputs 3. **Generates JSON schemas** from your Go types 4. **Creates invokers** for fast method calling The result is a `Service` struct that knows everything about your API: ```go theme={null} svc.Name // "todo" svc.Methods // [Create, List, Get, Delete] svc.Types // TypeRegistry with JSON schemas ``` ### Layer 3: Transports (The Protocols) Transports are HTTP handlers that speak different protocols. They: 1. **Receive HTTP requests** in their specific format 2. **Find the right method** in the contract 3. **Call the method** using the invoker 4. **Return the response** in their specific format Each transport does this differently: | Transport | Request Format | Response Format | | --------- | ------------------------ | ------------------ | | REST | HTTP verbs + paths | JSON body | | JSON-RPC | JSON with method name | JSON-RPC envelope | | MCP | JSON-RPC with tool calls | MCP content blocks | ## How a Request Flows Through the System Let's trace a REST request from start to finish: ### Step 1: Client Sends Request ```bash theme={null} curl -X POST http://localhost:8080/todos \ -H "Content-Type: application/json" \ -d '{"title": "Buy milk"}' ``` ### Step 2: REST Handler Receives It The REST handler (mounted by `rest.Mount()`) receives the HTTP request: ``` Method: POST Path: /todos Body: {"title": "Buy milk"} ``` ### Step 3: Handler Determines the Method Based on the HTTP method (POST) and path (/todos), the handler knows to call `Create`: ```go theme={null} // Inside the REST handler: // POST /todos -> Create method // GET /todos -> List method // GET /todos/{id} -> Get method // DELETE /todos/{id} -> Delete method methodName := "Create" // Derived from HTTP method + path method := svc.Resolve(methodName) // Find the Method struct ``` ### Step 4: Invoker Calls Your Method The invoker unmarshals the JSON input and calls your method: ```go theme={null} // What the invoker does (simplified): input := &CreateInput{} json.Unmarshal(requestBody, input) // {"title": "Buy milk"} -> CreateInput result, err := yourService.Create(ctx, input) // Actually calls your code! ``` ### Step 5: Handler Sends Response The handler marshals your response back to JSON: ```go theme={null} // Your method returned: &Todo{ID: "todo_1", Title: "Buy milk"} // Handler converts it to JSON and sends: HTTP/1.1 200 OK Content-Type: application/json {"id":"todo_1","title":"Buy milk","completed":false} ``` ## The Complete Request Flow Here's the detailed flow for any transport: ``` ┌─────────────────────────────────────────────────────────────────┐ │ HTTP Request │ │ POST /todos │ │ Body: {"title": "Buy milk"} │ └───────────────────────────────┬─────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ Transport Handler │ │ │ │ 1. Parse protocol-specific request │ │ REST: Parse HTTP method + path + body │ │ JSON-RPC: Parse JSON-RPC envelope │ │ MCP: Parse tool call │ │ │ │ 2. Resolve method name │ │ resolver.Resolve("Create") → *Method │ │ │ │ 3. Get invoker and call method │ │ invoker.Invoke(ctx, method, inputBytes) │ │ │ │ │ ├─ json.Unmarshal(inputBytes, &CreateInput{}) │ │ ├─ method.Invoker.Call(ctx, input) │ │ └─ return result or error │ │ │ │ 4. Format protocol-specific response │ │ REST: JSON body with HTTP status │ │ JSON-RPC: JSON-RPC envelope with result/error │ │ MCP: Content block with isError flag │ └───────────────────────────────┬─────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ HTTP Response │ │ 200 OK │ │ {"id":"todo_1","title":"Buy milk","completed":false} │ └─────────────────────────────────────────────────────────────────┘ ``` ## Core Components Explained ### Service The `Service` struct is the central data structure that holds everything about your API: ```go theme={null} type Service struct { Name string // "todo" Description string // Optional description Version string // API version Methods []*Method // All discovered methods Types *TypeRegistry // All types and their schemas } ``` ### Method Each method on your service becomes a `Method` struct: ```go theme={null} type Method struct { Name string // "Create" Description string // From doc comments (if any) InputType reflect.Type // *CreateInput OutputType reflect.Type // *Todo Invoker MethodInvoker // Fast caller } ``` The `Invoker` is the key to performance - it's created once at registration and used for every request. ### TypeRegistry The TypeRegistry holds all your types and their JSON schemas: ```go theme={null} // Your Go type: type CreateInput struct { Title string `json:"title"` } // Becomes this JSON Schema: { "type": "object", "properties": { "title": {"type": "string"} }, "required": ["title"] } ``` Schemas are used for: * OpenAPI documentation * MCP tool definitions * Input validation (future) * Client code generation ### Invoker An invoker is a pre-compiled way to call a method. Without Contract, calling a method via reflection on every request would be slow: ```go theme={null} // Slow: reflection on every call method := reflect.ValueOf(service).MethodByName("Create") result := method.Call([]reflect.Value{ctxValue, inputValue}) ``` With Contract, the reflection happens once at registration, and subsequent calls are fast: ```go theme={null} // Fast: compiled invoker result, err := method.Invoker.Call(ctx, input) ``` ## Package Organization The contract v2 package is organized into: ``` contract/v2/ ├── contract.go # Core: Register, RegisteredService ├── types.go # Type discovery, JSON schema generation ├── errors.go # Error types and codes │ └── transport/ # Transport implementations ├── rest/ # REST transport │ └── rest.go # Mount, NewHandler │ ├── jsonrpc/ # JSON-RPC 2.0 │ └── jsonrpc.go # Mount, NewHandler, Request/Response types │ ├── mcp/ # Model Context Protocol │ └── mcp.go # Mount, NewHandler │ └── openapi/ # OpenAPI spec generation └── openapi.go # Mount, Generate ``` ## Design Principles ### 1. Reflection Only at Startup Contract uses reflection (Go's way of inspecting types at runtime) only once when you call `Register()`. After that, all method calls use pre-compiled invokers. This means: * **Startup**: Slightly slower (milliseconds) due to reflection * **Runtime**: Fast method calls with no reflection overhead ### 2. Protocol Agnostic Errors Errors use codes (like `NOT_FOUND`, `INVALID_ARGUMENT`) that map to appropriate representations in each protocol: ```go theme={null} return nil, contract.ErrNotFound("user not found") ``` | Protocol | Representation | | -------- | ------------------------------------- | | REST | HTTP 404 + message body | | JSON-RPC | Error code -32601 | | MCP | `{"isError": true, "content": [...]}` | ### 3. No Magic, Just Functions Contract doesn't use: * Code generation * Build-time processing * Special comments * Interface implementations Everything is standard Go: structs, methods, and function calls. If your code compiles, it works with Contract. ## Common Questions ### Why not use code generation instead of reflection? Code generation (like protobuf) requires extra build steps and generated files to maintain. Contract's reflection-based approach: * Works with any Go struct immediately * No extra build steps * No generated files to keep in sync * Faster iteration during development ### How does Contract know which HTTP verb to use? For REST, Contract uses naming conventions: * `Create*` → POST * `Get*` → GET * `List*` → GET * `Update*` → PUT * `Delete*` → DELETE * Other names → POST ### Can I use Contract with gRPC? Not directly yet, but the error codes are aligned with gRPC status codes for future compatibility. JSON-RPC provides similar RPC semantics over HTTP. ### What happens if I change my method signature? Re-register your service (which happens automatically on server restart). Contract will discover the new signature and update the schemas. ## See Also * [Service Definition](/contract/service) - How to write services * [Registration](/contract/register) - The registration process in detail * [Transports Overview](/contract/transports-overview) - Compare all transports * [Middleware](/contract/middleware) - Add cross-cutting concerns # Clients Source: https://docs.go-mizu.dev/contract/client-generation Generate typed API clients in any language from your Contract services # 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](/contract/sdk-overview) for details. ## Two Approaches to Client Generation | Approach | Best For | Pros | Cons | | ----------------------------------------- | ---------------------------------------- | ------------------------------------------------------- | ------------------------------- | | **[Native SDKs](/contract/sdk-overview)** | 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 | Built-in generators for Go, Python, and TypeScript with OpenAI-style developer experience 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. ``` ## Two Ways to Get API Metadata | Endpoint | Format | Best For | | ------------------- | ----------- | ---------------------------- | | `/openapi.json` | OpenAPI 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 ```go theme={null} 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 ```bash theme={null} 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): ```bash theme={null} npm install -D openapi-typescript npx openapi-typescript openapi.json -o ./src/api/types.ts ``` Using `openapi-typescript-codegen` (full client): ```bash theme={null} npm install -D openapi-typescript-codegen npx openapi-typescript-codegen --input openapi.json --output ./src/api ``` Using `openapi-generator`: ```bash theme={null} npm install -g @openapitools/openapi-generator-cli openapi-generator-cli generate -i openapi.json -g typescript-fetch -o ./api-client ``` #### Python Using `openapi-python-client`: ```bash theme={null} pip install openapi-python-client openapi-python-client generate --path openapi.json --output-path ./client ``` Using `openapi-generator`: ```bash theme={null} openapi-generator-cli generate -i openapi.json -g python -o ./python-client ``` #### Go Using `oapi-codegen`: ```bash theme={null} go install github.com/deepmap/oapi-codegen/v2/cmd/oapi-codegen@latest oapi-codegen -package client openapi.json > client/client.go ``` Using `openapi-generator`: ```bash theme={null} openapi-generator-cli generate -i openapi.json -g go -o ./go-client ``` #### Other Languages ```bash theme={null} # 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 ```go theme={null} 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: ```go theme={null} 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 ```python theme={null} 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: ```python theme={null} 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 ```typescript theme={null} interface JsonRpcRequest { jsonrpc: '2.0'; id: number; method: string; params?: any; } interface JsonRpcResponse { jsonrpc: '2.0'; id: number; result?: T; error?: { code: number; message: string }; } class JsonRpcClient { private nextId = 1; constructor(private baseUrl: string) {} async call(method: string, params?: any): Promise { 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 = 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(calls: { method: string; params?: any }[]): Promise { 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[] = 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 { return this.rpc.call('Create', input); } list(): Promise { return this.rpc.call('List'); } // Batch create multiple todos in one request async createMany(inputs: CreateInput[]): Promise { return this.rpc.batch(inputs.map(input => ({ method: 'Create', params: input }))); } } ``` ## Error Handling in Clients Always handle errors properly in your clients: ### TypeScript ```typescript theme={null} class ApiError extends Error { constructor( message: string, public code: string, public status: number ) { super(message); this.name = 'ApiError'; } } async function handleResponse(response: Response): Promise { 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 { try { const response = await fetch(`${this.baseUrl}/todos`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input), }); return handleResponse(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; } } ``` ### Go ```go theme={null} 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 ```yaml theme={null} 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: ```go theme={null} 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 ### 3. Add Custom Headers Clients often need authentication: ```typescript theme={null} 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 ```typescript theme={null} async function withRetry(fn: () => Promise, maxRetries = 3): Promise { 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 * [SDK Generation](/contract/sdk-overview) - Native SDK generators for Go, Python, TypeScript * [Go SDK](/contract/sdk-go) - Native Go client generation * [Python SDK](/contract/sdk-python) - Native Python client generation * [TypeScript SDK](/contract/sdk-typescript) - Native TypeScript client generation * [OpenAPI](/contract/openapi) - OpenAPI specification generation * [REST](/contract/rest) - REST endpoints * [JSON-RPC](/contract/jsonrpc) - JSON-RPC endpoints * [MCP](/contract/mcp) - MCP for AI assistants # Error Codes Source: https://docs.go-mizu.dev/contract/error-codes Complete guide to all Contract error codes and how they map to different protocols # Error Codes Reference Contract uses a standard set of error codes that work across all protocols. This page explains each code, when to use it, and how it maps to HTTP, JSON-RPC, and other protocols. ## Why Standard Error Codes? Different protocols handle errors differently: * **HTTP** uses status codes (404, 500, etc.) * **JSON-RPC** uses negative error codes (-32600, -32601, etc.) * **gRPC** uses its own codes (NOT\_FOUND, INTERNAL, etc.) Contract uses protocol-agnostic codes that automatically translate to the right format: ```go theme={null} // You write this once: return nil, contract.ErrNotFound("user not found") // And it becomes: // - HTTP: 404 Not Found // - JSON-RPC: {"error": {"code": -32601, "message": "user not found"}} // - MCP: {"isError": true, "content": [{"type": "text", "text": "user not found"}]} ``` ## Quick Reference Table | Error Code | HTTP | JSON-RPC | Use When | | --------------------- | ---- | -------- | -------------------------- | | `INVALID_ARGUMENT` | 400 | -32602 | Bad input from user | | `NOT_FOUND` | 404 | -32601 | Resource doesn't exist | | `ALREADY_EXISTS` | 409 | -32003 | Trying to create duplicate | | `PERMISSION_DENIED` | 403 | -32004 | User can't do this | | `UNAUTHENTICATED` | 401 | -32011 | User not logged in | | `RESOURCE_EXHAUSTED` | 429 | -32005 | Rate limited | | `FAILED_PRECONDITION` | 412 | -32006 | System not ready | | `INTERNAL` | 500 | -32603 | Server bug | | `UNAVAILABLE` | 503 | -32009 | Service down | | `UNIMPLEMENTED` | 501 | -32601 | Feature not built | ## Detailed Error Code Guide ### INVALID\_ARGUMENT **Use when:** The user sent bad input. ```go theme={null} func (s *Service) Create(ctx context.Context, in *CreateInput) (*Todo, error) { if in.Title == "" { return nil, contract.ErrInvalidArgument("title is required") } if len(in.Title) > 500 { return nil, contract.ErrInvalidArgument("title must be 500 characters or less") } if !isValidEmail(in.Email) { return nil, contract.ErrInvalidArgument("email format is invalid") } // ... } ``` **Common scenarios:** * Missing required field * Value too long or too short * Invalid format (email, URL, phone) * Number out of allowed range * Wrong data type **HTTP Status:** 400 Bad Request *** ### NOT\_FOUND **Use when:** The requested item doesn't exist. ```go theme={null} func (s *Service) Get(ctx context.Context, in *GetInput) (*Todo, error) { todo, ok := s.todos[in.ID] if !ok { return nil, contract.ErrNotFound("todo not found") } return todo, nil } ``` **Common scenarios:** * Getting item by ID that doesn't exist * Referencing deleted resource * Looking up user by email that's not registered **With extra details:** ```go theme={null} return nil, contract.ErrNotFound("todo not found").WithDetail("id", in.ID) ``` **HTTP Status:** 404 Not Found *** ### ALREADY\_EXISTS **Use when:** Trying to create something that already exists. ```go theme={null} func (s *Service) Create(ctx context.Context, in *CreateInput) (*User, error) { if s.emailExists(in.Email) { return nil, contract.ErrAlreadyExists("email already registered") } // ... } ``` **Common scenarios:** * Creating user with existing email * Creating resource with duplicate unique ID * Adding item that's already in a list **HTTP Status:** 409 Conflict *** ### PERMISSION\_DENIED **Use when:** User is logged in but not allowed to do this. ```go theme={null} func (s *Service) Delete(ctx context.Context, in *DeleteInput) error { user := UserFromContext(ctx) todo := s.todos[in.ID] // Only owner or admin can delete if todo.OwnerID != user.ID && user.Role != "admin" { return contract.ErrPermissionDenied("you can only delete your own todos") } // ... } ``` **Common scenarios:** * User trying to access another user's data * User without admin role trying admin action * User trying to modify read-only resource **HTTP Status:** 403 Forbidden **Note:** Use `UNAUTHENTICATED` if user isn't logged in at all. *** ### UNAUTHENTICATED **Use when:** User needs to log in first. ```go theme={null} func (s *Service) Create(ctx context.Context, in *CreateInput) (*Todo, error) { user := UserFromContext(ctx) if user == nil { return nil, contract.ErrUnauthenticated("please log in first") } // ... } ``` **Common scenarios:** * No authentication token provided * Token has expired * Token is invalid or malformed **HTTP Status:** 401 Unauthorized **vs PERMISSION\_DENIED:** * `UNAUTHENTICATED`: "Who are you?" (not logged in) * `PERMISSION_DENIED`: "I know who you are, but you can't do this" (logged in but not allowed) *** ### RESOURCE\_EXHAUSTED **Use when:** Some limit has been reached. ```go theme={null} func (s *Service) Create(ctx context.Context, in *CreateInput) (*Todo, error) { if s.rateLimiter.IsExceeded(ctx) { return nil, contract.ErrResourceExhausted("too many requests, try again later") } if s.storage.IsFull() { return nil, contract.ErrResourceExhausted("storage quota exceeded") } // ... } ``` **Common scenarios:** * Rate limit exceeded * Storage quota full * Too many items created * Concurrent request limit **HTTP Status:** 429 Too Many Requests *** ### FAILED\_PRECONDITION **Use when:** System isn't in the right state for this operation. ```go theme={null} func (s *Service) Publish(ctx context.Context, in *PublishInput) error { article := s.articles[in.ID] if article.Status != "draft" { return contract.ErrFailedPrecondition("can only publish draft articles") } if !article.HasContent { return contract.ErrFailedPrecondition("article has no content") } // ... } ``` **Common scenarios:** * Resource in wrong state for operation * Required setup not complete * Dependency not satisfied * Order of operations violated **HTTP Status:** 412 Precondition Failed *** ### INTERNAL **Use when:** Something unexpected went wrong on the server. ```go theme={null} func (s *Service) Create(ctx context.Context, in *CreateInput) (*Todo, error) { err := s.db.Save(todo) if err != nil { // Log the real error for debugging log.Printf("database error: %v", err) // Return generic message to user (don't leak internals!) return nil, contract.ErrInternal("failed to save todo") } // ... } ``` **With cause for logging:** ```go theme={null} return nil, contract.ErrInternal("failed to save").WithCause(dbErr) // The cause is logged but NOT exposed to the client ``` **Common scenarios:** * Database errors * Unexpected nil values * Bug in your code * External service returned unexpected response **HTTP Status:** 500 Internal Server Error **Important:** Never expose internal details to clients: ```go theme={null} // Bad: Leaks internal info contract.ErrInternal("postgres: connection refused to 10.0.0.1:5432") // Good: Safe message contract.ErrInternal("database temporarily unavailable") ``` *** ### UNAVAILABLE **Use when:** Service is temporarily unavailable. ```go theme={null} func (s *Service) Create(ctx context.Context, in *CreateInput) (*Todo, error) { if s.isInMaintenanceMode() { return nil, contract.ErrUnavailable("service is under maintenance") } if !s.db.IsConnected() { return nil, contract.ErrUnavailable("service temporarily unavailable") } // ... } ``` **Common scenarios:** * Planned maintenance * Database connection lost * Dependent service is down * Server overloaded **HTTP Status:** 503 Service Unavailable **vs INTERNAL:** * `UNAVAILABLE`: Temporary problem, try again later * `INTERNAL`: Something is broken, might need fixing *** ### UNIMPLEMENTED **Use when:** Feature doesn't exist yet. ```go theme={null} func (s *Service) Export(ctx context.Context, in *ExportInput) (*File, error) { if in.Format == "pdf" { return nil, contract.ErrUnimplemented("PDF export coming soon") } // ... } ``` **Common scenarios:** * Feature not built yet * Method stub that's not implemented * Format or option not supported **HTTP Status:** 501 Not Implemented *** ### Other Error Codes These are less commonly used but available: | Code | HTTP | Use When | | ------------------- | ---- | ---------------------------------- | | `CANCELED` | 499 | Request was canceled by client | | `UNKNOWN` | 500 | Error doesn't fit other categories | | `DEADLINE_EXCEEDED` | 504 | Operation timed out | | `ABORTED` | 409 | Concurrent modification conflict | | `OUT_OF_RANGE` | 400 | Pagination or index out of bounds | | `DATA_LOSS` | 500 | Unrecoverable data corruption | ## Creating Errors ### Convenience Functions The most common errors have shortcut functions: ```go theme={null} contract.ErrNotFound("user not found") contract.ErrInvalidArgument("title required") contract.ErrPermissionDenied("admin only") contract.ErrUnauthenticated("please log in") contract.ErrInternal("something went wrong") contract.ErrAlreadyExists("email taken") contract.ErrResourceExhausted("rate limit exceeded") contract.ErrUnimplemented("coming soon") contract.ErrUnavailable("maintenance mode") ``` ### General Constructor For other codes, use `NewError`: ```go theme={null} contract.NewError(contract.ErrCodeCanceled, "request was canceled") contract.NewError(contract.ErrCodeDeadlineExceeded, "operation timed out") contract.NewError(contract.ErrCodeAborted, "concurrent modification") ``` ### With Format String ```go theme={null} contract.Errorf(contract.ErrCodeNotFound, "user %s not found", userID) ``` ## Adding Context to Errors ### Add Details Include structured data for debugging: ```go theme={null} err := contract.ErrNotFound("todo not found"). WithDetail("id", todoID). WithDetail("searched_at", time.Now()) ``` ### Add Multiple Details ```go theme={null} err := contract.ErrInvalidArgument("validation failed"). WithDetails(map[string]any{ "field": "email", "value": email, "reason": "invalid format", }) ``` ### Add Cause Wrap the underlying error (useful for logging): ```go theme={null} result, err := s.db.Query(...) if err != nil { return nil, contract.ErrInternal("database query failed").WithCause(err) // The cause is available for logging but NOT sent to the client } ``` ## Checking Error Types ### In Your Code ```go theme={null} import "errors" _, err := svc.Get(ctx, &GetInput{ID: "123"}) if err != nil { var contractErr *contract.Error if errors.As(err, &contractErr) { switch contractErr.Code { case contract.ErrCodeNotFound: // Handle not found case contract.ErrCodePermissionDenied: // Handle forbidden default: // Handle other errors } } } ``` ### Get HTTP Status ```go theme={null} err := contract.ErrNotFound("not found") status := err.HTTPStatus() // Returns 404 ``` ### Convert HTTP Status to Error Code ```go theme={null} code := contract.HTTPStatusToErrorCode(404) // Returns ErrCodeNotFound ``` ## How Errors Look in Different Protocols ### REST ``` HTTP/1.1 404 Not Found Content-Type: text/plain user not found ``` ### JSON-RPC ```json theme={null} { "jsonrpc": "2.0", "id": 1, "error": { "code": -32601, "message": "user not found" } } ``` ### MCP ```json theme={null} { "jsonrpc": "2.0", "id": 1, "result": { "content": [ {"type": "text", "text": "user not found"} ], "isError": true } } ``` ## Decision Guide Use this flowchart to pick the right error code: ``` Is the user logged in? ├─ No → UNAUTHENTICATED └─ Yes (or not required) │ Is the input valid? ├─ No → INVALID_ARGUMENT └─ Yes │ Does the resource exist? ├─ No → NOT_FOUND └─ Yes │ Can this user access it? ├─ No → PERMISSION_DENIED └─ Yes │ Is the system ready? ├─ No, maintenance → UNAVAILABLE ├─ No, rate limit → RESOURCE_EXHAUSTED ├─ No, wrong state → FAILED_PRECONDITION └─ Yes │ Did something break? ├─ Yes → INTERNAL └─ No → (success!) ``` ## Best Practices ### 1. Be Specific But Safe ```go theme={null} // Good: Specific but safe contract.ErrNotFound("user not found") // Bad: Leaks information contract.ErrNotFound("user with email admin@company.com not found") ``` ### 2. Use the Right Code ```go theme={null} // Good: UNAUTHENTICATED for "not logged in" if user == nil { return nil, contract.ErrUnauthenticated("please log in") } // Bad: Using INTERNAL for auth failures if user == nil { return nil, contract.ErrInternal("no user") // Wrong code! } ``` ### 3. Log Internal Errors ```go theme={null} if err := s.db.Save(item); err != nil { // Log the real error log.Printf("database error: %v", err) // Return safe error to user return nil, contract.ErrInternal("failed to save") } ``` ### 4. Add Context When Helpful ```go theme={null} // Helpful for debugging contract.ErrNotFound("todo not found").WithDetail("id", todoID) // Not helpful (obvious from context) contract.ErrNotFound("not found").WithDetail("type", "todo") ``` ## See Also * [Error Handling](/contract/errors) - Error patterns and examples * [Transports Overview](/contract/transports-overview) - How errors travel * [API Reference](/contract/api-reference) - Complete error API # Errors Source: https://docs.go-mizu.dev/contract/errors Learn how to handle errors in Contract APIs so they work correctly across all protocols ## Why Error Handling Matters When building APIs, errors are inevitable. Users will request items that don't exist, provide invalid data, or lack permissions. How you handle these errors determines whether your API is frustrating or pleasant to use. Contract provides a portable error system that ensures your errors work correctly across REST, JSON-RPC, MCP, and other protocols - without you having to think about each one separately. ## The Problem with Simple Errors Consider this code in a todo service: ```go theme={null} package todo func (s *Service) Get(ctx context.Context, in *GetInput) (*Todo, error) { todo := s.findTodo(in.ID) if todo == nil { return nil, errors.New("not found") // Simple error } return todo, nil } ``` The problem? Different protocols expect errors in different formats: | Protocol | Expected Format | | -------- | ----------------------------------------------- | | REST | HTTP 404 status with error body | | JSON-RPC | `{"error": {"code": -32601, "message": "..."}}` | | MCP | `{"content": [...], "isError": true}` | With a simple `errors.New()`, Contract can't know which HTTP status to use, so it defaults to 500 Internal Server Error - even though "not found" should clearly be 404. ## The Solution: Contract Errors Contract provides error types that know how to translate themselves to each protocol: ```go theme={null} package todo import contract "github.com/go-mizu/mizu/contract/v2" func (s *Service) Get(ctx context.Context, in *GetInput) (*Todo, error) { todo := s.findTodo(in.ID) if todo == nil { return nil, contract.ErrNotFound("todo not found") // Contract error } return todo, nil } ``` This single line automatically becomes the right format for each protocol: | Protocol | Response | | -------- | ------------------------------------------------------------------------- | | REST | HTTP 404 with `{"error": "todo not found"}` | | JSON-RPC | `{"error": {"code": -32601, "message": "todo not found"}}` | | MCP | `{"content": [{"type":"text","text":"todo not found"}], "isError": true}` | You write one error, and it works everywhere. ## Creating Errors ### Method 1: Error Constructor Functions (Recommended) Contract provides convenient functions for common error types: ```go theme={null} import contract "github.com/go-mizu/mizu/contract/v2" // Resource not found (HTTP 404) contract.ErrNotFound("user not found") // Invalid input data (HTTP 400) contract.ErrInvalidArgument("email address is invalid") // User not logged in (HTTP 401) contract.ErrUnauthenticated("please log in") // User lacks permission (HTTP 403) contract.ErrPermissionDenied("admin access required") // Something went wrong on the server (HTTP 500) contract.ErrInternal("database unavailable") // Resource already exists (HTTP 409) contract.ErrAlreadyExists("username already taken") // Rate limiting (HTTP 429) contract.ErrResourceExhausted("too many requests") // Feature not implemented (HTTP 501) contract.ErrUnimplemented("export feature coming soon") // Service temporarily down (HTTP 503) contract.ErrUnavailable("maintenance in progress") ``` ### Method 2: NewError with Code For more control, use `NewError` with an explicit error code: ```go theme={null} err := contract.NewError(contract.ErrCodeNotFound, "user not found") ``` ### Method 3: Errorf with Formatting Use `Errorf` when you need to include variables in the message: ```go theme={null} err := contract.Errorf(contract.ErrCodeNotFound, "user %s not found", userID) err := contract.Errorf(contract.ErrCodeInvalidArgument, "field %s: %s", fieldName, reason) ``` ## Choosing the Right Error Code ### User's Fault (4xx HTTP Status) Use these when the client made a mistake or is requesting something they can't have: ```go theme={null} // Resource doesn't exist contract.ErrNotFound("order #12345 not found") // Bad input data - validation failed contract.ErrInvalidArgument("email must be a valid email address") // Need to log in first contract.ErrUnauthenticated("session expired, please log in again") // Logged in but can't do this action contract.ErrPermissionDenied("only admins can delete users") // Tried to create something that exists contract.ErrAlreadyExists("account with this email already exists") // Too many requests - rate limited contract.ErrResourceExhausted("rate limit exceeded, try again in 60 seconds") // Wrong state for operation contract.ErrFailedPrecondition("order already shipped, cannot cancel") ``` ### Your Fault (5xx HTTP Status) Use these when something went wrong on your end: ```go theme={null} // Unexpected error - bugs, panics caught contract.ErrInternal("an unexpected error occurred") // Database or external service down contract.ErrUnavailable("service temporarily unavailable") // Feature not built yet contract.ErrUnimplemented("bulk export coming soon") // Operation took too long contract.ErrDeadlineExceeded("request timed out") ``` ## Error Code Reference | Code | HTTP | When to Use | | --------------------------- | ---- | ----------------------------------- | | `ErrCodeInvalidArgument` | 400 | Bad input data, validation failures | | `ErrCodeUnauthenticated` | 401 | User not logged in, expired token | | `ErrCodePermissionDenied` | 403 | User logged in but lacks permission | | `ErrCodeNotFound` | 404 | Resource doesn't exist | | `ErrCodeAlreadyExists` | 409 | Resource already exists | | `ErrCodeFailedPrecondition` | 412 | Can't do action in current state | | `ErrCodeResourceExhausted` | 429 | Rate limiting, quota exceeded | | `ErrCodeCanceled` | 499 | Client canceled the request | | `ErrCodeInternal` | 500 | Bugs, unexpected errors | | `ErrCodeUnimplemented` | 501 | Feature not built yet | | `ErrCodeUnavailable` | 503 | Service down, maintenance | | `ErrCodeDeadlineExceeded` | 504 | Operation timed out | ## Adding Context to Errors ### Adding Details Details help clients understand and handle errors better. They appear in the error response but don't change the message: ```go theme={null} // Add multiple details at once err := contract.ErrInvalidArgument("validation failed"). WithDetails(map[string]any{ "field": "email", "reason": "must be a valid email address", "value": "not-an-email", }) // Add single detail err := contract.ErrNotFound("todo not found"). WithDetail("todoId", requestedID) ``` Details appear in the JSON response: ```json theme={null} { "code": "INVALID_ARGUMENT", "message": "validation failed", "details": { "field": "email", "reason": "must be a valid email address", "value": "not-an-email" } } ``` This is incredibly useful for: * **Frontend developers**: Can show "Please enter a valid email address" next to the email field * **API consumers**: Can programmatically handle specific error cases * **Debugging**: Includes context about what went wrong ### Wrapping Underlying Errors Preserve the original error for debugging while showing a user-friendly message: ```go theme={null} package todo func (s *Service) Get(ctx context.Context, in *GetInput) (*Todo, error) { todo, err := s.db.FindByID(in.ID) if err != nil { // sql.ErrNoRows -> user-friendly "not found" if errors.Is(err, sql.ErrNoRows) { return nil, contract.ErrNotFound("todo not found") } // Other database errors -> preserve for logging, show generic message return nil, contract.ErrInternal("database error").WithCause(err) } return todo, nil } ``` The `WithCause` method: * **Preserves** the original error for logging and debugging * **Hides** internal details from API responses (security!) * **Supports** `errors.Is()` and `errors.As()` for checking causes ```go theme={null} // In logging middleware if cause := errors.Unwrap(err); cause != nil { log.Printf("Original error: %v", cause) // "connection refused" } // API response shows: "database error" (not the internal details) ``` ## Complete Example Here's a realistic service with proper error handling throughout: ```go theme={null} // user/service.go package user import ( "context" "database/sql" "errors" "regexp" contract "github.com/go-mizu/mizu/contract/v2" ) // API defines user management operations type API interface { Create(ctx context.Context, in *CreateInput) (*User, error) Get(ctx context.Context, in *GetInput) (*User, error) Delete(ctx context.Context, in *DeleteInput) error } // Service implements user.API type Service struct { db *sql.DB } var _ API = (*Service)(nil) var emailRegex = regexp.MustCompile(`^[^\s@]+@[^\s@]+\.[^\s@]+$`) func (s *Service) Create(ctx context.Context, in *CreateInput) (*User, error) { // Validate input - check each field if in.Email == "" { return nil, contract.ErrInvalidArgument("email is required") } if !emailRegex.MatchString(in.Email) { return nil, contract.ErrInvalidArgument("invalid email format"). WithDetail("field", "email"). WithDetail("value", in.Email) } if len(in.Password) < 8 { return nil, contract.ErrInvalidArgument("password must be at least 8 characters"). WithDetail("field", "password") } if in.Name == "" { return nil, contract.ErrInvalidArgument("name is required") } // Check for duplicates exists, err := s.userExists(ctx, in.Email) if err != nil { return nil, contract.ErrInternal("failed to check user").WithCause(err) } if exists { return nil, contract.ErrAlreadyExists("user with this email already exists"). WithDetail("email", in.Email) } // Create the user user, err := s.insertUser(ctx, in) if err != nil { return nil, contract.ErrInternal("failed to create user").WithCause(err) } return user, nil } func (s *Service) Get(ctx context.Context, in *GetInput) (*User, error) { if in.ID == "" { return nil, contract.ErrInvalidArgument("id is required") } user, err := s.findUser(ctx, in.ID) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, contract.ErrNotFound("user not found"). WithDetail("userId", in.ID) } return nil, contract.ErrInternal("failed to fetch user").WithCause(err) } return user, nil } func (s *Service) Delete(ctx context.Context, in *DeleteInput) error { // Get current user from context (set by auth middleware) currentUserID, ok := ctx.Value("userID").(string) if !ok { return contract.ErrUnauthenticated("not authenticated") } // Users can only delete their own account (unless admin) if in.ID != currentUserID { isAdmin, _ := ctx.Value("isAdmin").(bool) if !isAdmin { return contract.ErrPermissionDenied("you can only delete your own account") } } // Check user exists before deleting _, err := s.findUser(ctx, in.ID) if err != nil { if errors.Is(err, sql.ErrNoRows) { return contract.ErrNotFound("user not found") } return contract.ErrInternal("failed to check user").WithCause(err) } // Delete the user if err := s.deleteUser(ctx, in.ID); err != nil { return contract.ErrInternal("failed to delete user").WithCause(err) } return nil } // Helper methods (implementation details) func (s *Service) userExists(ctx context.Context, email string) (bool, error) { /* ... */ } func (s *Service) findUser(ctx context.Context, id string) (*User, error) { /* ... */ } func (s *Service) insertUser(ctx context.Context, in *CreateInput) (*User, error) { /* ... */ } func (s *Service) deleteUser(ctx context.Context, id string) error { /* ... */ } ``` ## Best Practices ### 1. Use Specific Error Codes Specific codes map to correct HTTP status codes: ```go theme={null} // Good: specific codes map to correct HTTP status contract.ErrNotFound("order not found") // -> HTTP 404 contract.ErrInvalidArgument("bad email") // -> HTTP 400 // Bad: using Internal for everything contract.ErrInternal("not found") // -> HTTP 500 (wrong!) ``` ### 2. Write User-Friendly Messages Messages should tell users what went wrong and what to do: ```go theme={null} // Good: tells user what to do contract.ErrInvalidArgument("email must be like user@example.com") contract.ErrPermissionDenied("upgrade to Pro to access this feature") // Bad: cryptic messages contract.ErrInvalidArgument("invalid") contract.ErrPermissionDenied("denied") ``` ### 3. Include Relevant Details Details help clients handle errors intelligently: ```go theme={null} // Good: client can show "Order #12345 not found" contract.ErrNotFound("order not found").WithDetail("orderId", "12345") // Good: client can highlight the email field contract.ErrInvalidArgument("invalid email").WithDetail("field", "email") // Bad: no context contract.ErrNotFound("not found") ``` ### 4. Don't Leak Internal Details Protect your infrastructure from exposure: ```go theme={null} // Good: safe, user-friendly contract.ErrInternal("database temporarily unavailable") // Bad: exposes infrastructure contract.ErrInternal("connection to postgres at 10.0.0.5:5432 refused") contract.ErrInternal("query timeout after 30s on users table") ``` ### 5. Handle Each Error Case Map different underlying errors to appropriate codes: ```go theme={null} package todo func (s *Service) Get(ctx context.Context, in *GetInput) (*Todo, error) { if in.ID == "" { return nil, contract.ErrInvalidArgument("id is required") } todo, err := s.db.Find(in.ID) if err != nil { // Handle each error type appropriately if errors.Is(err, sql.ErrNoRows) { return nil, contract.ErrNotFound("todo not found") } if errors.Is(err, context.DeadlineExceeded) { return nil, contract.ErrDeadlineExceeded("request timed out") } if errors.Is(err, context.Canceled) { return nil, contract.ErrCanceled("request was canceled") } // Unknown errors -> Internal return nil, contract.ErrInternal("failed to fetch todo").WithCause(err) } return todo, nil } ``` ## Testing Errors Test that your errors have the correct code and details: ```go theme={null} package user_test import ( "context" "errors" "testing" contract "github.com/go-mizu/mizu/contract/v2" "yourapp/user" ) func TestGet_NotFound(t *testing.T) { svc := &user.Service{DB: mockDB} _, err := svc.Get(context.Background(), &user.GetInput{ID: "nonexistent"}) // Check it's a contract error var contractErr *contract.Error if !errors.As(err, &contractErr) { t.Fatalf("expected contract.Error, got %T", err) } // Check the code if contractErr.Code != contract.ErrCodeNotFound { t.Errorf("expected NOT_FOUND, got %s", contractErr.Code) } // Check HTTP status if contractErr.HTTPStatus() != 404 { t.Errorf("expected HTTP 404, got %d", contractErr.HTTPStatus()) } } func TestCreate_Validation(t *testing.T) { svc := &user.Service{DB: mockDB} tests := []struct { name string input *user.CreateInput wantCode contract.ErrCode }{ {"empty email", &user.CreateInput{Email: ""}, contract.ErrCodeInvalidArgument}, {"bad email", &user.CreateInput{Email: "not-email"}, contract.ErrCodeInvalidArgument}, {"short password", &user.CreateInput{Email: "a@b.com", Password: "short"}, contract.ErrCodeInvalidArgument}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, err := svc.Create(context.Background(), tt.input) var contractErr *contract.Error if !errors.As(err, &contractErr) { t.Fatalf("expected contract.Error, got %T", err) } if contractErr.Code != tt.wantCode { t.Errorf("got %s, want %s", contractErr.Code, tt.wantCode) } }) } } ``` ## Common Questions ### What happens if I return a regular Go error? Regular errors are treated as `Internal` errors (HTTP 500): ```go theme={null} // This becomes HTTP 500 return nil, errors.New("user not found") // This becomes HTTP 404 (correct!) return nil, contract.ErrNotFound("user not found") ``` **Rule of thumb**: Always use Contract errors for expected error cases. Use regular errors only for unexpected bugs that shouldn't happen. ### Can I create custom error codes? Contract uses standard codes that map consistently across protocols. For custom error semantics, use the details field: ```go theme={null} contract.ErrInvalidArgument("validation failed"). WithDetail("customCode", "DUPLICATE_SKU"). WithDetail("field", "sku") ``` ### Should I log errors in my service? Your service should return errors, not log them. Let middleware handle logging. This keeps your service focused and testable: ```go theme={null} // In your service - just return the error func (s *Service) Get(ctx context.Context, in *GetInput) (*Todo, error) { // ... return nil, contract.ErrInternal("database error").WithCause(dbErr) } // In middleware - log and let it propagate func loggingMiddleware(next mizu.Handler) mizu.Handler { return func(c *mizu.Ctx) error { err := next(c) if err != nil { log.Printf("error: %v", err) // Log the original cause too if cause := errors.Unwrap(err); cause != nil { log.Printf("cause: %v", cause) } } return err } } ``` ## What's Next? Now that you understand error handling: * **[Error Codes](/contract/error-codes)** - Complete reference of all error codes * **[REST Transport](/contract/rest)** - How errors appear in REST responses * **[JSON-RPC Transport](/contract/jsonrpc)** - JSON-RPC error codes * **[Testing](/contract/testing)** - Testing error conditions # Invokers Source: https://docs.go-mizu.dev/contract/invoker Understand how Contract calls your methods efficiently at runtime ## What Is an Invoker? Invokers are the engine that makes Contract fast. They're pre-compiled method callers that let Contract call your service methods without the overhead of reflection on every request. Think of invokers like pre-addressed envelopes: instead of looking up the address every time you send a letter, you prepare the envelope once and just drop letters in it. When you register a service, Contract analyzes your methods using Go's reflection system. This analysis happens **once at startup**, and the results are stored as "invokers" - optimized callers that know exactly how to call each method. ``` Registration time (once, at startup): Service → Reflection Analysis → Invoker Created and Cached Runtime (every request): Request → Invoker.Call() → Your Method ↓ No reflection overhead! ``` ## Why Invokers Matter Without invokers, calling a method by name would require reflection on every single request: ```go theme={null} // Slow way: Reflection on every call // This is expensive because Go has to look up the method, check types, etc. method := reflect.ValueOf(service).MethodByName("Create") args := []reflect.Value{reflect.ValueOf(ctx), reflect.ValueOf(input)} result := method.Call(args) // Expensive reflection call! ``` With invokers, the reflection cost is paid once at startup: ```go theme={null} // Fast way: Pre-compiled invoker // The invoker already knows how to call the method directly result, err := method.Invoker.Call(ctx, input) // Just a regular function call ``` ## Basic Usage Most of the time, you don't interact with invokers directly - transports use them automatically behind the scenes. But understanding how they work helps you build custom transports or debugging tools. ### Getting a Method and Its Invoker ```go theme={null} package main import ( "context" contract "github.com/go-mizu/mizu/contract/v2" "yourapp/todo" ) func main() { impl := todo.NewService() svc := contract.Register[todo.API](impl, contract.WithDefaultResource("todos"), ) // Get a specific method by name method := svc.Resolve("create") // lowercase method name // method.Invoker is ready to use // method.Input tells you the input type // method.Output tells you the output type } ``` ### Calling a Method via Its Invoker ```go theme={null} // Create input input := &todo.CreateInput{Title: "Buy milk"} // Call the method using its invoker ctx := context.Background() result, err := method.Invoker.Call(ctx, input) if err != nil { log.Fatal(err) } // Type assert the result to get the actual type createdTodo := result.(*todo.Todo) fmt.Println(createdTodo.ID, createdTodo.Title) ``` ## The Invoker Interface Invokers implement a simple interface that matches Contract's method patterns: ```go theme={null} type Invoker interface { Call(ctx context.Context, in any) (any, error) } ``` **Parameters:** | Parameter | Type | Description | | --------- | ----------------- | ---------------------------------------------------------------- | | `ctx` | `context.Context` | Request context (carries timeouts, cancellation signals, values) | | `in` | `any` | Input value (or `nil` for methods without input) | **Returns:** | Return | Type | Description | | ------ | ------- | -------------------------------------------------- | | result | `any` | Output value (or `nil` for methods without output) | | err | `error` | Any error returned by your method | ## Calling Different Method Types Contract supports four method patterns. Here's how to call each with invokers: ### Method With Input and Output This is the most common pattern: ```go theme={null} package todo // Your service method: func (s *Service) Create(ctx context.Context, in *CreateInput) (*Todo, error) { return &Todo{ID: "1", Title: in.Title}, nil } ``` ```go theme={null} // Calling with invoker: method := svc.Resolve("create") input := &todo.CreateInput{Title: "Test todo"} result, err := method.Invoker.Call(ctx, input) if err != nil { log.Fatal(err) } createdTodo := result.(*todo.Todo) // Type assert the result ``` ### Method With Output Only (No Input) ```go theme={null} package todo // Your service method: func (s *Service) List(ctx context.Context) (*ListOutput, error) { return &ListOutput{Items: s.todos, Count: len(s.todos)}, nil } ``` ```go theme={null} // Calling with invoker: method := svc.Resolve("list") result, err := method.Invoker.Call(ctx, nil) // Pass nil for input if err != nil { log.Fatal(err) } list := result.(*todo.ListOutput) ``` ### Method With Input Only (No Output) ```go theme={null} package todo // Your service method: func (s *Service) Delete(ctx context.Context, in *DeleteInput) error { delete(s.todos, in.ID) return nil } ``` ```go theme={null} // Calling with invoker: method := svc.Resolve("delete") input := &todo.DeleteInput{ID: "123"} _, err := method.Invoker.Call(ctx, input) // Result is nil, just check error ``` ### Method With Neither Input Nor Output ```go theme={null} package health // Your service method: func (s *Service) Ping(ctx context.Context) error { return nil // Just returns success/failure } ``` ```go theme={null} // Calling with invoker: method := svc.Resolve("ping") _, err := method.Invoker.Call(ctx, nil) // Both input and result are nil if err != nil { // Service is unhealthy } ``` ## Creating Input Instances Dynamically Use `NewInput()` to create a new input instance for a method. This is essential when you don't know the input type at compile time (like when building a transport): ```go theme={null} method := svc.Resolve("create") // Check if the method expects input if method.Input != nil { // Create a new input instance (returns any) input := method.NewInput() // Type assert and populate createInput := input.(*todo.CreateInput) createInput.Title = "Dynamically created todo" // Call the method result, err := method.Invoker.Call(ctx, createInput) if err != nil { log.Fatal(err) } } ``` This pattern is exactly what transports use: they create an input instance, unmarshal JSON into it, then call the invoker. ## How Transports Use Invokers Here's simplified code showing how a transport uses invokers internally. This helps you understand what happens when a request comes in: ```go theme={null} // Simplified transport handler func handleRequest(svc *contract.RegisteredService, methodName string, body []byte) (any, error) { // 1. Find the method by name method := svc.Resolve(methodName) if method == nil { return nil, errors.New("method not found") } // 2. Parse input if the method expects one var input any if method.Input != nil { input = method.NewInput() // Create empty input instance if err := json.Unmarshal(body, input); err != nil { return nil, errors.New("invalid input") } } // 3. Call the method through its invoker result, err := method.Invoker.Call(context.Background(), input) if err != nil { return nil, err } // 4. Return result (transport will serialize it) return result, nil } ``` ## Transport Invoker For building transports, there's a higher-level interface called `TransportInvoker` that handles JSON unmarshaling: ```go theme={null} type TransportInvoker interface { Invoke(ctx context.Context, method *Method, input []byte) (any, error) } ``` **Key difference from Invoker:** * `Invoker.Call()` takes a parsed Go value as input * `TransportInvoker.Invoke()` takes raw JSON bytes and handles unmarshaling ### Using the Default Transport Invoker ```go theme={null} // Get the default transport invoker for a service invoker := contract.DefaultInvoker(svc) // Call methods with raw JSON method := svc.Resolve("create") result, err := invoker.Invoke(ctx, method, []byte(`{"title":"Test"}`)) if err != nil { log.Fatal(err) } ``` ### Creating a Custom Transport Invoker You can wrap the default invoker to add logging, metrics, or other cross-cutting concerns: ```go theme={null} // LoggingInvoker wraps a TransportInvoker with logging type LoggingInvoker struct { inner contract.TransportInvoker } func (l *LoggingInvoker) Invoke(ctx context.Context, method *contract.Method, input []byte) (any, error) { // Log before log.Printf("Calling: %s", method.Name) start := time.Now() // Call the actual method result, err := l.inner.Invoke(ctx, method, input) // Log after duration := time.Since(start) if err != nil { log.Printf("Failed: %s (took %v): %v", method.Name, duration, err) } else { log.Printf("Success: %s (took %v)", method.Name, duration) } return result, err } // Use it when mounting a transport loggingInvoker := &LoggingInvoker{inner: contract.DefaultInvoker(svc)} mcp.Mount(mux, "/mcp", svc, mcp.WithInvoker(loggingInvoker)) ``` ## Error Handling Errors from your service method are returned directly through the invoker: ```go theme={null} package todo func (s *Service) Get(ctx context.Context, in *GetInput) (*Todo, error) { if in.ID == "" { return nil, contract.ErrInvalidArgument("id is required") } todo, ok := s.todos[in.ID] if !ok { return nil, contract.ErrNotFound("todo not found") } return todo, nil } ``` ```go theme={null} // Caller receives the error from your method result, err := method.Invoker.Call(ctx, &todo.GetInput{ID: ""}) if err != nil { // err is the ErrInvalidArgument error from your method fmt.Println(err.Error()) // "id is required" } ``` ## Context Handling The context is passed directly to your method, preserving timeouts, cancellation, and values: ```go theme={null} // With timeout ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() result, err := method.Invoker.Call(ctx, input) if errors.Is(err, context.DeadlineExceeded) { // Handle timeout - your method took too long } // With values (e.g., from authentication middleware) ctx = context.WithValue(ctx, "userID", currentUser.ID) result, err := method.Invoker.Call(ctx, input) // Your method can access ctx.Value("userID") ``` ## Type Safety Always use type assertions when working with invoker results: ```go theme={null} result, err := method.Invoker.Call(ctx, input) if err != nil { return err } // Safe type assertion with check (recommended) todo, ok := result.(*todo.Todo) if !ok { return fmt.Errorf("unexpected type: %T", result) } // Or direct assertion when you're certain of the type // (panics if the type is wrong) todo := result.(*todo.Todo) ``` ## Building a Custom Transport Here's a complete example of using invokers to build a custom transport. This example creates a simple HTTP transport that takes the method name from a query parameter: ```go theme={null} package customtransport import ( "encoding/json" "io" "net/http" contract "github.com/go-mizu/mizu/contract/v2" ) // Mount creates a custom transport handler at the given path func Mount(mux *http.ServeMux, path string, svc *contract.RegisteredService) { invoker := contract.DefaultInvoker(svc) mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { // 1. Get method name from query string methodName := r.URL.Query().Get("method") if methodName == "" { http.Error(w, "method query parameter required", http.StatusBadRequest) return } // 2. Find the method method := svc.Resolve(methodName) if method == nil { http.Error(w, "method not found", http.StatusNotFound) return } // 3. Read request body var body []byte if r.Body != nil { body, _ = io.ReadAll(r.Body) } // 4. Call the method through the transport invoker result, err := invoker.Invoke(r.Context(), method, body) if err != nil { // Check if it's a Contract error for proper status code var cErr *contract.Error if errors.As(err, &cErr) { http.Error(w, err.Error(), cErr.HTTPStatus()) } else { http.Error(w, err.Error(), http.StatusInternalServerError) } return } // 5. Write response as JSON w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(result) }) } ``` **Usage:** ```go theme={null} svc := contract.Register[todo.API](impl, contract.WithDefaultResource("todos")) customtransport.Mount(mux, "/api", svc) // Call: GET /api?method=create with JSON body {"title": "Test"} ``` ## Performance Characteristics Understanding when operations are expensive helps you build efficient systems: | Operation | When | Cost | | ------------------- | ---------------------- | --------------------------- | | Reflection analysis | Registration (startup) | Slow, but only once | | Invoker.Call() | Every request | Fast (direct function call) | | Type assertion | Every request | Very fast | | JSON parsing | Every request | Normal cost | **Key insight**: Heavy reflection happens once at startup. Runtime calls through invokers are as fast as regular function calls. ## Common Patterns ### Checking Method Capabilities ```go theme={null} method := svc.Resolve("create") // Does it expect input? if method.Input != nil { fmt.Println("Input type:", method.Input.Name) } // Does it return output? if method.Output != nil { fmt.Println("Output type:", method.Output.Name) } ``` ### Iterating All Methods ```go theme={null} desc := svc.Descriptor() for _, res := range desc.Resources { fmt.Printf("Resource: %s\n", res.Name) for _, method := range res.Methods { fmt.Printf(" Method: %s\n", method.Name) fmt.Printf(" Has Input: %v\n", method.Input != nil) fmt.Printf(" Has Output: %v\n", method.Output != nil) } } ``` ### Dynamic Method Dispatch Useful for building generic tools that work with any service: ```go theme={null} func callMethod(svc *contract.RegisteredService, name string, inputJSON []byte) ([]byte, error) { method := svc.Resolve(name) if method == nil { return nil, fmt.Errorf("unknown method: %s", name) } invoker := contract.DefaultInvoker(svc) result, err := invoker.Invoke(context.Background(), method, inputJSON) if err != nil { return nil, err } return json.Marshal(result) } ``` ## See Also * **[Registration](/contract/register)** - How invokers are created during registration * **[Middleware](/contract/middleware)** - Wrapping invokers with middleware * **[Architecture](/contract/architecture)** - Where invokers fit in the overall system # JSON-RPC Source: https://docs.go-mizu.dev/contract/jsonrpc Use JSON-RPC 2.0 for batch operations and RPC-style APIs ## What is JSON-RPC? JSON-RPC (Remote Procedure Call) is a protocol where you explicitly name the method you want to call. Unlike REST which uses different HTTP verbs and paths, JSON-RPC sends all requests to a single endpoint with the method name in the request body. Its killer feature? **Batching** - send multiple requests in one HTTP call. ```json theme={null} { "jsonrpc": "2.0", "id": 1, "method": "todos.create", "params": {"title": "Buy milk"} } ``` ## Quick Start ```go theme={null} import ( "github.com/go-mizu/mizu" contract "github.com/go-mizu/mizu/contract/v2" "github.com/go-mizu/mizu/contract/v2/transport/jsonrpc" "yourapp/todo" ) // Your interface is defined in the todo package as todo.API: // 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) // } // Create your service implementation impl := todo.NewService() // Register your service svc := contract.Register[todo.API](impl, contract.WithDefaultResource("todos"), ) // Create mizu app app := mizu.New() // Mount JSON-RPC at /rpc jsonrpc.Mount(app.Router, "/rpc", svc) // Start server app.Listen(":8080") ``` Now call methods: ```bash theme={null} 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"} }' ``` ## Request Format Every JSON-RPC request has the same structure: ```json theme={null} { "jsonrpc": "2.0", "id": 1, "method": "todos.create", "params": {"title": "Buy milk"} } ``` | Field | Required | Description | | --------- | -------- | ---------------------------------------------- | | `jsonrpc` | Yes | Always `"2.0"` | | `id` | No\* | Identifies your request (returned in response) | | `method` | Yes | `resource.method` name to call | | `params` | No | Object with method parameters | \*If you omit `id`, it becomes a "notification" (no response sent). ## Response Format ### Success Response ```json theme={null} { "jsonrpc": "2.0", "id": 1, "result": { "id": "todo_1", "title": "Buy milk", "completed": false } } ``` ### Error Response ```json theme={null} { "jsonrpc": "2.0", "id": 1, "error": { "code": -32603, "message": "todo not found", "data": {"todoId": "123"} } } ``` ## Method Naming Method names follow the pattern `resource.method`: | Interface Method | JSON-RPC Method | | ---------------- | --------------- | | `Create` | `todos.create` | | `List` | `todos.list` | | `Get` | `todos.get` | | `Delete` | `todos.delete` | | `Update` | `todos.update` | The resource name comes from `WithDefaultResource` or `WithResource`. ## Batching Batching lets you send multiple requests in one HTTP call. Instead of N HTTP round trips, you make just 1. ### How to Batch Send an array of requests: ```bash theme={null} 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"} ]' ``` Response: ```json theme={null} [ {"jsonrpc": "2.0", "id": 1, "result": {"id": "1", "title": "First", "completed": false}}, {"jsonrpc": "2.0", "id": 2, "result": {"id": "2", "title": "Second", "completed": false}}, {"jsonrpc": "2.0", "id": 3, "result": {"items": [...], "count": 2}} ] ``` ### When to Use Batching * **Dashboard loads**: Fetch user, settings, and recent items in one call * **Bulk operations**: Create 100 items without 100 HTTP requests * **Related data**: Get a todo and its comments together * **Service-to-service**: Microservices calling each other ## Notifications (Fire and Forget) Omit the `id` field to send a notification. The server won't send a response: ```bash theme={null} curl -X POST http://localhost:8080/rpc \ -H "Content-Type: application/json" \ -d '{"jsonrpc": "2.0", "method": "todos.create", "params": {"title": "Fire and forget"}}' # HTTP 204 No Content ``` Use notifications for: * Logging or analytics events * Triggering background jobs * Sending metrics You can batch notifications: ```bash theme={null} curl -X POST http://localhost:8080/rpc \ -d '[ {"jsonrpc": "2.0", "method": "analytics.log", "params": {"event": "page_view"}}, {"jsonrpc": "2.0", "method": "analytics.log", "params": {"event": "click"}} ]' # HTTP 204 No Content ``` ## Complete Example This example shows a JSON-RPC service using the recommended package-based organization: ```go theme={null} // 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) } ``` ```go theme={null} // 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 } 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, Count: len(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 } ``` ```go theme={null} // 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" "yourapp/todo" ) func main() { impl := todo.NewService() svc := contract.Register[todo.API](impl, contract.WithDefaultResource("todos"), ) app := mizu.New() // Mount JSON-RPC at /rpc jsonrpc.Mount(app.Router, "/rpc", svc) fmt.Println("JSON-RPC server running at http://localhost:8080/rpc") fmt.Println("Methods: todos.create, todos.list, todos.get") app.Listen(":8080") } ``` ### Test It ```bash theme={null} # Create a todo 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"}}' # List all todos curl -X POST http://localhost:8080/rpc \ -d '{"jsonrpc":"2.0","id":2,"method":"todos.list"}' # Get a todo curl -X POST http://localhost:8080/rpc \ -d '{"jsonrpc":"2.0","id":3,"method":"todos.get","params":{"id":"1"}}' # Batch request curl -X POST http://localhost:8080/rpc \ -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"} ]' ``` ## Error Codes JSON-RPC uses standard error codes: | Code | Message | What It Means | | ------ | ---------------- | ----------------------------- | | -32700 | Parse error | Invalid JSON | | -32600 | Invalid Request | Missing `jsonrpc` or `method` | | -32601 | Method not found | Method doesn't exist | | -32602 | Invalid params | Wrong parameter types | | -32603 | Internal error | Your method returned an error | Contract errors map to -32603 (Internal error) with the message from your error: ```go theme={null} // In your method return nil, contract.ErrNotFound("todo not found") // JSON-RPC response { "jsonrpc": "2.0", "id": 1, "error": { "code": -32603, "message": "todo not found", "data": {"code": "NOT_FOUND"} } } ``` ## Multiple Services Mount multiple services on the same endpoint: ```go theme={null} todoSvc := contract.Register[todo.API](todoImpl, contract.WithDefaultResource("todos")) userSvc := contract.Register[user.API](userImpl, contract.WithDefaultResource("users")) app := mizu.New() // Both mounted at /rpc jsonrpc.Mount(app.Router, "/rpc", todoSvc) jsonrpc.Mount(app.Router, "/rpc", userSvc) // Now call: // {"method": "todos.create", ...} // {"method": "users.create", ...} ``` ## Combining with REST Use JSON-RPC alongside REST: ```go theme={null} import ( "github.com/go-mizu/mizu/contract/v2/transport/rest" "github.com/go-mizu/mizu/contract/v2/transport/jsonrpc" ) app := mizu.New() // REST for browsers and simple clients rest.Mount(app.Router, svc) // JSON-RPC for batching and service-to-service jsonrpc.Mount(app.Router, "/rpc", svc) app.Listen(":8080") ``` ## JSON-RPC vs REST | Use JSON-RPC when... | Use REST when... | | ------------------------------ | -------------------------- | | You need batching | Browser is the main client | | Service-to-service calls | You want HTTP caching | | Methods don't map to CRUD | Team knows REST | | You want explicit method names | Building a public API | ## Common Questions ### Can I mix requests and notifications in a batch? Yes! Requests (with `id`) get responses, notifications (without `id`) don't: ```json theme={null} [ {"jsonrpc": "2.0", "id": 1, "method": "todos.get", "params": {"id": "1"}}, {"jsonrpc": "2.0", "method": "analytics.log", "params": {"event": "viewed"}} ] ``` Response only includes the request: ```json theme={null} [ {"jsonrpc": "2.0", "id": 1, "result": {...}} ] ``` ### What ID values can I use? Any JSON value works: ```json theme={null} {"id": 1, ...} {"id": "req-abc-123", ...} {"id": null, ...} ``` ### How do I call methods without a resource? If you didn't use `WithDefaultResource`, use just the method name: ```json theme={null} {"method": "Create", ...} ``` With a resource: ```json theme={null} {"method": "todos.create", ...} ``` ### Can I generate an OpenRPC spec? Yes: ```go theme={null} spec, err := jsonrpc.OpenRPC(svc.Descriptor()) ``` ## What's Next? * **[REST Transport](/contract/rest)** - For browser-friendly APIs * **[MCP Transport](/contract/mcp)** - Connect to AI assistants * **[Error Handling](/contract/errors)** - Error codes and handling * **[Transports Overview](/contract/transports-overview)** - Compare all transports # MCP Source: https://docs.go-mizu.dev/contract/mcp Connect your API to AI assistants like Claude using the Model Context Protocol ## What is MCP? MCP (Model Context Protocol) is a protocol developed by Anthropic that lets AI assistants interact with external tools and services. When you expose your service via MCP: 1. **AI can discover your tools**: Claude can see all your methods and their parameters 2. **AI can call your tools**: Claude can execute methods with proper inputs 3. **AI understands the results**: Responses are formatted for AI interpretation Think of it like giving an AI a "toolbox" - each method in your service becomes a tool the AI can use. ## Quick Start ```go theme={null} import ( "github.com/go-mizu/mizu" contract "github.com/go-mizu/mizu/contract/v2" "github.com/go-mizu/mizu/contract/v2/transport/mcp" "yourapp/todo" ) // Your interface is defined in the todo package as todo.API: // 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) // } // Create your service implementation impl := todo.NewService() // Register your service svc := contract.Register[todo.API](impl, contract.WithDefaultResource("todos"), ) // Create mizu app app := mizu.New() // Mount MCP endpoint mcp.Mount(app.Router, "/mcp", svc) // Start server app.Listen(":8080") ``` Your service methods are now available as AI tools. ## How Methods Become Tools Each method becomes an MCP tool: | Interface Method | MCP Tool Name | | ---------------- | -------------- | | `Create` | `todos.create` | | `List` | `todos.list` | | `Get` | `todos.get` | | `Delete` | `todos.delete` | Tool names follow the pattern `resource.method`. ## The MCP Protocol MCP uses JSON-RPC 2.0 as its transport. Here's how an AI interacts with your server: ### Step 1: Initialize The AI establishes a connection: ```bash theme={null} curl -X POST http://localhost:8080/mcp \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-06-18" } }' ``` Response: ```json theme={null} { "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2025-06-18", "capabilities": { "tools": {"listChanged": false} }, "serverInfo": { "name": "mizu-contract", "version": "0.1.0" } } } ``` ### Step 2: Discover Tools The AI asks what tools are available: ```bash theme={null} curl -X POST http://localhost:8080/mcp \ -d '{ "jsonrpc": "2.0", "id": 2, "method": "tools/list" }' ``` Response: ```json theme={null} { "jsonrpc": "2.0", "id": 2, "result": { "tools": [ { "name": "todos.create", "description": "Create a new todo", "inputSchema": { "type": "object", "properties": { "title": {"type": "string"} }, "required": ["title"] } }, { "name": "todos.list", "description": "List all todos", "inputSchema": { "type": "object", "properties": {} } } ] } } ``` ### Step 3: Call a Tool The AI calls a tool: ```bash theme={null} curl -X POST http://localhost:8080/mcp \ -d '{ "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "todos.create", "arguments": { "title": "Buy groceries" } } }' ``` Success response: ```json theme={null} { "jsonrpc": "2.0", "id": 3, "result": { "content": [ { "type": "text", "text": "{\"id\":\"1\",\"title\":\"Buy groceries\",\"completed\":false}" } ], "isError": false } } ``` Error response: ```json theme={null} { "jsonrpc": "2.0", "id": 3, "result": { "content": [ { "type": "text", "text": "todo not found" } ], "isError": true } } ``` ## Configuration Options Customize your MCP server: ```go theme={null} mcp.Mount(app.Router, "/mcp", svc, // Server information mcp.WithServerInfo(mcp.ServerInfo{ Name: "my-todo-api", Version: "1.0.0", }), // Instructions for the AI mcp.WithInstructions("Use these tools to manage todos. Be careful with delete operations."), ) ``` ### Available Options | Option | Description | | ------------------ | -------------------------------- | | `WithServerInfo` | Set server name and version | | `WithInstructions` | Provide usage guidance to the AI | ## Complete Example This example shows a complete MCP service using the recommended package-based organization: ```go theme={null} // 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"` } type CompleteInput struct { ID string `json:"id"` } type ListOutput struct { Items []*Todo `json:"items"` } ``` ```go theme={null} // 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) Complete(ctx context.Context, in *CompleteInput) (*Todo, error) } ``` ```go theme={null} // 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 } func (s *Service) Complete(ctx context.Context, in *CompleteInput) (*Todo, error) { s.mu.Lock() defer s.mu.Unlock() todo, ok := s.todos[in.ID] if !ok { return nil, contract.ErrNotFound("todo not found") } todo.Completed = true return todo, nil } ``` ```go theme={null} // 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/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 MCP for AI assistants mcp.Mount(app.Router, "/mcp", svc, mcp.WithInstructions("Use these tools to manage a todo list. Available operations: create todos, list all todos, get a specific todo, and mark todos as complete."), ) // Also mount REST for testing rest.Mount(app.Router, svc) fmt.Println("Server running on http://localhost:8080") fmt.Println("MCP endpoint: /mcp (for AI assistants)") fmt.Println("REST endpoint: /todos (for testing)") app.Listen(":8080") } ``` ### Test with curl ```bash theme={null} # Initialize connection curl -X POST http://localhost:8080/mcp \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}' # List available tools curl -X POST http://localhost:8080/mcp \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' # Create a todo curl -X POST http://localhost:8080/mcp \ -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"todos.create","arguments":{"title":"Buy milk"}}}' # List todos curl -X POST http://localhost:8080/mcp \ -d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"todos.list","arguments":{}}}' # Complete a todo curl -X POST http://localhost:8080/mcp \ -d '{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"todos.complete","arguments":{"id":"1"}}}' ``` ## Using with Claude Desktop Claude Desktop can connect to MCP servers: 1. Start your server: ```bash theme={null} go run main.go # Server running on http://localhost:8080 ``` 2. Configure Claude Desktop to connect to `http://localhost:8080/mcp` 3. Ask Claude: * "Create a todo to buy groceries" * "Show me all my todos" * "Mark the first todo as complete" Claude will use your MCP tools to fulfill these requests. ## Security ### Input Validation Always validate inputs - don't trust AI-provided data: ```go theme={null} func (s *Service) Create(ctx context.Context, in *CreateInput) (*Todo, error) { if in.Title == "" { return nil, contract.ErrInvalidArgument("title is required") } if len(in.Title) > 500 { return nil, contract.ErrInvalidArgument("title too long") } // Proceed with validated input } ``` ### Authentication Add authentication with middleware: ```go theme={null} func authMiddleware(next mizu.Handler) mizu.Handler { return func(c *mizu.Ctx) error { token := c.Get("Authorization") if !isValidToken(token) { return c.Status(401).SendString("unauthorized") } return next(c) } } app := mizu.New() app.Use(authMiddleware) mcp.Mount(app.Router, "/mcp", svc) ``` ## Error Handling ### Method Errors When your method returns an error, MCP wraps it with `isError: true`: ```go theme={null} func (s *Service) Get(ctx context.Context, in *GetInput) (*Todo, error) { return nil, contract.ErrNotFound("todo not found") } ``` Response: ```json theme={null} { "result": { "content": [{"type": "text", "text": "todo not found"}], "isError": true } } ``` ### Protocol Errors Invalid requests return JSON-RPC errors: ```json theme={null} { "error": { "code": -32601, "message": "Method not found" } } ``` ## Multiple Services Each service needs its own MCP endpoint: ```go theme={null} todoSvc := contract.Register[todo.API](todoImpl, contract.WithDefaultResource("todos")) userSvc := contract.Register[user.API](userImpl, contract.WithDefaultResource("users")) mcp.Mount(app.Router, "/mcp/todos", todoSvc) mcp.Mount(app.Router, "/mcp/users", userSvc) ``` ## Combining Transports Use MCP alongside REST and JSON-RPC: ```go theme={null} import ( "github.com/go-mizu/mizu/contract/v2/transport/rest" "github.com/go-mizu/mizu/contract/v2/transport/jsonrpc" "github.com/go-mizu/mizu/contract/v2/transport/mcp" ) app := mizu.New() // REST for browsers and testing rest.Mount(app.Router, svc) // JSON-RPC for service-to-service jsonrpc.Mount(app.Router, "/rpc", svc) // MCP for AI assistants mcp.Mount(app.Router, "/mcp", svc) app.Listen(":8080") ``` Same business logic, multiple access methods. ## Common Questions ### How does the AI know what parameters to use? The `tools/list` response includes `inputSchema` for each tool. This JSON Schema tells the AI what parameters are expected, their types, and which are required. ### Can I add descriptions to tools? Tool descriptions come from your method organization. Use clear method names and the `WithInstructions` option to guide the AI. ### What if the AI sends invalid data? Your method receives the data and should validate it. Return `contract.ErrInvalidArgument` for validation failures - this shows as an error to the AI. ### Can I use MCP and REST together? Yes, and it's recommended. REST is great for testing and debugging while MCP serves AI assistants: ```go theme={null} rest.Mount(app.Router, svc) // /todos mcp.Mount(app.Router, "/mcp", svc) // /mcp ``` ## What's Next? * **[REST Transport](/contract/rest)** - For browser-friendly APIs * **[JSON-RPC Transport](/contract/jsonrpc)** - Similar protocol with batching * **[Error Handling](/contract/errors)** - Proper error responses * **[Transports Overview](/contract/transports-overview)** - Compare all transports # Middleware Source: https://docs.go-mizu.dev/contract/middleware Add logging, authentication, rate limiting, and other cross-cutting concerns to your API # Middleware Middleware lets you add functionality that runs before or after your service methods - like logging, authentication, or rate limiting. You write it once, and it applies to all (or some) of your methods automatically. ## What Is Middleware? Think of middleware like security guards at a building entrance. Every visitor (request) passes through them before reaching their destination (your method). The guard can: 1. **Check credentials** - Is this person allowed in? 2. **Log the visit** - Record who came and when 3. **Limit access** - Only let 10 people in per minute 4. **Add information** - Give them a visitor badge (add data to context) ``` Request → Middleware → Your Method → Middleware → Response ↓ ↓ (before) (after) ``` ## Two Types of Middleware Contract supports two levels of middleware: | Type | Level | Use For | | ------------------- | ------------- | --------------------------------- | | **Custom Invokers** | Method calls | Logging, auth checks, metrics | | **HTTP Middleware** | HTTP requests | CORS, request IDs, panic recovery | ## Custom Invokers - Method-Level Middleware The most common way to add middleware is by creating a custom invoker that wraps method calls. ### Basic Structure ```go theme={null} import contract "github.com/go-mizu/mizu/contract/v2" // A custom invoker wraps the default invoker type LoggingInvoker struct { inner contract.TransportInvoker // The original invoker } // Implement the Invoke method func (l *LoggingInvoker) Invoke(ctx context.Context, method *contract.Method, input []byte) (any, error) { // BEFORE: runs before your method start := time.Now() log.Printf("Starting: %s", method.Name) // Call the actual method result, err := l.inner.Invoke(ctx, method, input) // AFTER: runs after your method log.Printf("Finished: %s (took %v)", method.Name, time.Since(start)) return result, err } ``` ### Using Your Custom Invoker Pass your invoker when mounting transports: ```go theme={null} import ( contract "github.com/go-mizu/mizu/contract/v2" "github.com/go-mizu/mizu/contract/v2/transport/mcp" "github.com/go-mizu/mizu/contract/v2/transport/jsonrpc" "github.com/go-mizu/mizu/contract/v2/transport/rest" ) // Create the custom invoker loggingInvoker := &LoggingInvoker{ inner: contract.DefaultInvoker(svc), // Wrap the default } // Use with REST rest.Mount(app.Router, svc, rest.WithInvoker(loggingInvoker)) // Use with MCP mcp.Mount(app.Router, "/mcp", svc, mcp.WithInvoker(loggingInvoker)) // Use with JSON-RPC jsonrpc.Mount(app.Router, "/rpc", svc, jsonrpc.WithInvoker(loggingInvoker)) ``` ## Common Middleware Examples ### Logging Log every method call with timing: ```go theme={null} type LoggingInvoker struct { inner contract.TransportInvoker logger *slog.Logger } func (l *LoggingInvoker) Invoke(ctx context.Context, method *contract.Method, input []byte) (any, error) { start := time.Now() // Call the method result, err := l.inner.Invoke(ctx, method, input) // Log the result l.logger.Info("method called", "method", method.Name, "duration_ms", time.Since(start).Milliseconds(), "success", err == nil, ) return result, err } // Usage loggingInvoker := &LoggingInvoker{ inner: contract.DefaultInvoker(svc), logger: slog.Default(), } ``` ### Authentication Check if the user is logged in: ```go theme={null} type AuthInvoker struct { inner contract.TransportInvoker } func (a *AuthInvoker) Invoke(ctx context.Context, method *contract.Method, input []byte) (any, error) { // Get user from context (set by HTTP middleware earlier) user := UserFromContext(ctx) // Check if this method requires authentication if requiresAuth(method.Name) && user == nil { return nil, contract.ErrUnauthenticated("please log in first") } // User is authenticated, proceed return a.inner.Invoke(ctx, method, input) } // Helper to check which methods need auth func requiresAuth(methodName string) bool { // Public methods that don't need auth publicMethods := map[string]bool{ "Health": true, "Login": true, "Signup": true, } return !publicMethods[methodName] } ``` ### Rate Limiting Prevent too many requests: ```go theme={null} import "golang.org/x/time/rate" type RateLimitInvoker struct { inner contract.TransportInvoker limiter *rate.Limiter } func NewRateLimitInvoker(inner contract.TransportInvoker, requestsPerSecond int) *RateLimitInvoker { return &RateLimitInvoker{ inner: inner, limiter: rate.NewLimiter(rate.Limit(requestsPerSecond), requestsPerSecond), } } func (r *RateLimitInvoker) Invoke(ctx context.Context, method *contract.Method, input []byte) (any, error) { // Check if we're over the limit if !r.limiter.Allow() { return nil, contract.ErrResourceExhausted("too many requests, please slow down") } // Under the limit, proceed return r.inner.Invoke(ctx, method, input) } ``` ### Metrics (Prometheus) Track method calls for monitoring: ```go theme={null} import "github.com/prometheus/client_golang/prometheus" type MetricsInvoker struct { inner contract.TransportInvoker requests *prometheus.CounterVec duration *prometheus.HistogramVec } func NewMetricsInvoker(inner contract.TransportInvoker) *MetricsInvoker { requests := prometheus.NewCounterVec( prometheus.CounterOpts{ Name: "api_requests_total", Help: "Total API requests", }, []string{"method", "status"}, ) duration := prometheus.NewHistogramVec( prometheus.HistogramOpts{ Name: "api_request_duration_seconds", Help: "API request duration", Buckets: prometheus.DefBuckets, }, []string{"method"}, ) prometheus.MustRegister(requests, duration) return &MetricsInvoker{ inner: inner, requests: requests, duration: duration, } } func (m *MetricsInvoker) Invoke(ctx context.Context, method *contract.Method, input []byte) (any, error) { start := time.Now() result, err := m.inner.Invoke(ctx, method, input) // Record metrics status := "success" if err != nil { status = "error" } m.requests.WithLabelValues(method.Name, status).Inc() m.duration.WithLabelValues(method.Name).Observe(time.Since(start).Seconds()) return result, err } ``` ### Distributed Tracing Add trace spans for debugging: ```go theme={null} import "go.opentelemetry.io/otel/trace" type TracingInvoker struct { inner contract.TransportInvoker tracer trace.Tracer } func (t *TracingInvoker) Invoke(ctx context.Context, method *contract.Method, input []byte) (any, error) { // Start a new span for this method call ctx, span := t.tracer.Start(ctx, method.Name) defer span.End() // Call the method result, err := t.inner.Invoke(ctx, method, input) // Record error if any if err != nil { span.RecordError(err) span.SetStatus(codes.Error, err.Error()) } return result, err } ``` ## Chaining Multiple Middleware You often want multiple middleware together. Chain them by wrapping one inside another: ```go theme={null} // Helper function to chain middleware func ChainInvokers( base contract.TransportInvoker, wrappers ...func(contract.TransportInvoker) contract.TransportInvoker, ) contract.TransportInvoker { // Apply wrappers from last to first // So the first wrapper runs first for i := len(wrappers) - 1; i >= 0; i-- { base = wrappers[i](base) } return base } // Define wrapper functions func withLogging(inner contract.TransportInvoker) contract.TransportInvoker { return &LoggingInvoker{inner: inner} } func withMetrics(inner contract.TransportInvoker) contract.TransportInvoker { return NewMetricsInvoker(inner) } func withAuth(inner contract.TransportInvoker) contract.TransportInvoker { return &AuthInvoker{inner: inner} } func withRateLimit(inner contract.TransportInvoker) contract.TransportInvoker { return NewRateLimitInvoker(inner, 100) // 100 req/sec } // Chain them together invoker := ChainInvokers( contract.DefaultInvoker(svc), withLogging, // Runs first (outermost) withMetrics, // Runs second withRateLimit, // Runs third withAuth, // Runs fourth (innermost before method) ) // Use the chained invoker mcp.Mount(mux, "/mcp", svc, mcp.WithInvoker(invoker)) ``` ### Order Matters! The order of middleware is important. Think about it like layers of an onion: ``` Request enters → [1. Logging starts] [2. Metrics starts] [3. Rate limit check] [4. Auth check] [Your Method] [4. Auth done] [3. Rate limit done] [2. Metrics records] [1. Logging finishes] ← Response exits ``` Good ordering: 1. **Logging** first - logs everything, including rejected requests 2. **Metrics** second - tracks all requests 3. **Rate limiting** third - rejects before expensive checks 4. **Authentication** fourth - before business logic ## HTTP Middleware For HTTP-level concerns (like CORS or request IDs), use standard Go HTTP middleware: ### CORS (Cross-Origin Requests) Allow browsers from other domains to call your API: ```go theme={null} func withCORS(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Set CORS headers w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") // Handle preflight requests if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } next.ServeHTTP(w, r) }) } // Usage handler := mcp.NewHandler(svc) mux.Handle("/mcp", withCORS(handler)) ``` ### Request ID Add a unique ID to every request for debugging: ```go theme={null} import "github.com/google/uuid" // Key for storing request ID in context type contextKey string const requestIDKey contextKey = "requestID" func withRequestID(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Get existing ID or create new one requestID := r.Header.Get("X-Request-ID") if requestID == "" { requestID = uuid.New().String() } // Add to context ctx := context.WithValue(r.Context(), requestIDKey, requestID) // Add to response headers w.Header().Set("X-Request-ID", requestID) // Continue with updated context next.ServeHTTP(w, r.WithContext(ctx)) }) } // Access the ID in your service method func (s *Service) Create(ctx context.Context, in *CreateInput) (*Todo, error) { requestID := ctx.Value(requestIDKey).(string) log.Printf("[%s] Creating todo: %s", requestID, in.Title) // ... } ``` ### Panic Recovery Catch panics and return proper errors: ```go theme={null} func withRecovery(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if err := recover(); err != nil { log.Printf("PANIC: %v\n%s", err, debug.Stack()) http.Error(w, "internal server error", http.StatusInternalServerError) } }() next.ServeHTTP(w, r) }) } ``` ### Authentication (Setting User in Context) Extract user from token and add to context: ```go theme={null} type User struct { ID string Email string Role string } type contextKey string const userKey contextKey = "user" func withUser(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { token := r.Header.Get("Authorization") if token != "" { // Validate token and get user user, err := validateToken(token) if err == nil { // Add user to context ctx := context.WithValue(r.Context(), userKey, user) r = r.WithContext(ctx) } } next.ServeHTTP(w, r) }) } // Helper to get user from context func UserFromContext(ctx context.Context) *User { user, _ := ctx.Value(userKey).(*User) return user } ``` ## Combining HTTP and Invoker Middleware Use both together for complete coverage: ```go theme={null} import "yourapp/todo" func main() { impl := todo.NewService() svc := contract.Register[todo.API](impl, contract.WithDefaultResource("todos"), ) // Create invoker middleware chain invoker := ChainInvokers( contract.DefaultInvoker(svc), withLogging, withMetrics, withAuth, ) // Create HTTP handler with invoker handler := mcp.NewHandler(svc, mcp.WithInvoker(invoker)) // Wrap with HTTP middleware handler = withRecovery(handler) handler = withRequestID(handler) handler = withUser(handler) handler = withCORS(handler) mux := http.NewServeMux() mux.Handle("/mcp", handler) http.ListenAndServe(":8080", mux) } ``` Flow: 1. **CORS** - Allow cross-origin requests 2. **User** - Extract user from token 3. **Request ID** - Add tracking ID 4. **Recovery** - Catch panics 5. **Logging** - Log method calls 6. **Metrics** - Track performance 7. **Auth** - Verify user is authorized 8. **Your Method** - Actually do the work ## Passing Data Through Context Context is how middleware shares data: ```go theme={null} // HTTP middleware sets the user func withUser(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user := extractUser(r) ctx := context.WithValue(r.Context(), userKey, user) next.ServeHTTP(w, r.WithContext(ctx)) }) } // Invoker middleware can access the user func (a *AuthInvoker) Invoke(ctx context.Context, method *contract.Method, input []byte) (any, error) { user := ctx.Value(userKey).(*User) if user == nil && requiresAuth(method.Name) { return nil, contract.ErrUnauthenticated("please log in") } return a.inner.Invoke(ctx, method, input) } // Service method can also access the user func (s *Service) Create(ctx context.Context, in *CreateInput) (*Todo, error) { user := ctx.Value(userKey).(*User) log.Printf("User %s is creating a todo", user.Email) // ... } ``` ## Best Practices ### 1. Keep Middleware Focused Each middleware should do one thing well: ```go theme={null} // Good: Each does one thing withLogging(withAuth(withMetrics(base))) // Bad: One middleware trying to do everything type EverythingMiddleware struct{} // Does logging, auth, metrics, etc. ``` ### 2. Don't Do Heavy Work Middleware runs on every request. Keep it fast: ```go theme={null} // Good: Quick cache lookup func (a *AuthInvoker) Invoke(ctx context.Context, method *contract.Method, input []byte) (any, error) { if !a.tokenCache.IsValid(ctx) { return nil, contract.ErrUnauthenticated("invalid token") } return a.inner.Invoke(ctx, method, input) } // Bad: Database call on every request func (a *AuthInvoker) Invoke(ctx context.Context, method *contract.Method, input []byte) (any, error) { user, err := a.db.FindUser(ctx, tokenFromCtx(ctx)) // Slow! // ... } ``` ### 3. Handle Errors Properly Use Contract's error types for consistent handling: ```go theme={null} func (a *AuthInvoker) Invoke(ctx context.Context, method *contract.Method, input []byte) (any, error) { if !isAuthenticated(ctx) { // Use Contract error - maps correctly to HTTP 401 return nil, contract.ErrUnauthenticated("please log in") } return a.inner.Invoke(ctx, method, input) } ``` ### 4. Make Middleware Configurable ```go theme={null} type LoggingInvoker struct { inner contract.TransportInvoker logger *slog.Logger logInput bool // Option to log input } func NewLoggingInvoker(inner contract.TransportInvoker, opts ...LoggingOption) *LoggingInvoker { l := &LoggingInvoker{ inner: inner, logger: slog.Default(), } for _, opt := range opts { opt(l) } return l } type LoggingOption func(*LoggingInvoker) func WithLogger(logger *slog.Logger) LoggingOption { return func(l *LoggingInvoker) { l.logger = logger } } func WithInputLogging(enabled bool) LoggingOption { return func(l *LoggingInvoker) { l.logInput = enabled } } ``` ## See Also * [Service Definition](/contract/service) - Writing services * [Error Handling](/contract/errors) - Return errors from middleware * [Architecture](/contract/architecture) - How transports work * [Invokers](/contract/invoker) - Deep dive into invokers # OpenAPI Source: https://docs.go-mizu.dev/contract/openapi Generate API documentation automatically from your Contract services OpenAPI (formerly known as Swagger) is a standard way to describe REST APIs. Contract automatically generates an OpenAPI specification from your service - no manual YAML writing needed! ## What Is OpenAPI? OpenAPI is a specification format that describes your API: * What endpoints exist * What parameters they accept * What responses they return * What your data types look like This specification can be used to: * **Generate documentation**: Beautiful, interactive API docs with Swagger UI or Redoc * **Generate client code**: Create typed clients in TypeScript, Python, Go, and more * **Test your API**: Import into Postman, Insomnia, or other API tools * **Validate requests**: Ensure requests match the expected format ## Quick Start Generate and serve OpenAPI documentation: ```go theme={null} 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" ) // Your interface is defined in the todo package as todo.API: // 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) // } // Create your service implementation impl := todo.NewService() // Register your service svc := contract.Register[todo.API](impl, contract.WithDefaultResource("todos"), ) app := mizu.New() // Mount REST endpoints rest.Mount(app.Router, svc) // Generate and serve OpenAPI spec specBytes, _ := rest.OpenAPIDocument(svc) app.Get("/openapi.json", func(c *mizu.Ctx) error { c.Set("Content-Type", "application/json") return c.Send(specBytes) }) app.Listen(":8080") ``` Now visit `http://localhost:8080/openapi.json` to see your API specification. ## What Gets Generated Contract analyzes your service and generates a complete OpenAPI 3.0.3 specification. ### From Your Interface ```go theme={null} // todo/api.go package todo 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/types.go package todo type CreateInput struct { Title string `json:"title"` Description string `json:"description"` } type Todo struct { ID string `json:"id"` Title string `json:"title"` Description string `json:"description"` Completed bool `json:"completed"` CreatedAt time.Time `json:"createdAt"` } type GetInput struct { ID string `json:"id" path:"id"` } type ListOutput struct { Items []*Todo `json:"items"` Total int `json:"total"` } ``` ### To OpenAPI Specification ```json theme={null} { "openapi": "3.0.3", "info": { "title": "Todo", "version": "0.1.0" }, "paths": { "/todos": { "post": { "operationId": "todos_Create", "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateInput" } } } }, "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Todo" } } } } } }, "get": { "operationId": "todos_List", "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ListOutput" } } } } } } }, "/todos/{id}": { "get": { "operationId": "todos_Get", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Todo" } } } } } } } }, "components": { "schemas": { "CreateInput": { "type": "object", "properties": { "title": { "type": "string" }, "description": { "type": "string" } }, "required": ["title", "description"] }, "Todo": { "type": "object", "properties": { "id": { "type": "string" }, "title": { "type": "string" }, "description": { "type": "string" }, "completed": { "type": "boolean" }, "createdAt": { "type": "string", "format": "date-time" } }, "required": ["id", "title", "description", "completed", "createdAt"] }, "ListOutput": { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/Todo" } }, "total": { "type": "integer", "format": "int32" } }, "required": ["items", "total"] } } } } ``` ## Adding Interactive Documentation The JSON spec is machine-readable. For human-readable docs, add Swagger UI or Redoc. ### Option 1: Swagger UI Swagger UI provides an interactive interface where users can try your API: ```go theme={null} app := mizu.New() rest.Mount(app.Router, svc) // Serve OpenAPI spec specBytes, _ := rest.OpenAPIDocument(svc) app.Get("/openapi.json", func(c *mizu.Ctx) error { c.Set("Content-Type", "application/json") return c.Send(specBytes) }) // Serve Swagger UI at /docs app.Get("/docs", func(c *mizu.Ctx) error { return c.HTML(200, ` API Documentation
`) }) app.Listen(":8080") ``` Now visit `http://localhost:8080/docs` to see interactive documentation! ### Option 2: Redoc Redoc provides clean, three-panel documentation: ```go theme={null} app.Get("/docs", func(c *mizu.Ctx) error { return c.HTML(200, ` API Documentation `) }) ``` ## Complete Example This example shows OpenAPI generation using the recommended package-based organization: ```go theme={null} // todo/types.go package todo import "time" type Todo struct { ID string `json:"id"` Title string `json:"title"` Description string `json:"description"` Completed bool `json:"completed"` CreatedAt time.Time `json:"createdAt"` } type CreateInput struct { Title string `json:"title"` Description string `json:"description,omitempty"` } type GetInput struct { ID string `json:"id" path:"id"` } type ListOutput struct { Items []*Todo `json:"items"` Total int `json:"total"` } ``` ```go theme={null} // 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) Get(ctx context.Context, in *GetInput) (*Todo, error) List(ctx context.Context) (*ListOutput, error) } ``` ```go theme={null} // todo/service.go package todo import ( "context" "fmt" "sync" "time" 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, Description: in.Description, CreatedAt: time.Now(), } s.todos[todo.ID] = todo return todo, 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 } 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, Total: len(items)}, nil } ``` ```go theme={null} // 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" "yourapp/todo" ) func main() { impl := todo.NewService() svc := contract.Register[todo.API](impl, contract.WithName("Todo"), contract.WithDescription("A simple todo list API"), contract.WithDefaultResource("todos"), ) app := mizu.New() // Mount REST API rest.Mount(app.Router, svc) // Serve OpenAPI specification specBytes, err := rest.OpenAPIDocument(svc) if err != nil { panic(err) } app.Get("/openapi.json", func(c *mizu.Ctx) error { c.Set("Content-Type", "application/json") return c.Send(specBytes) }) // Serve Swagger UI app.Get("/docs", swaggerUIHandler) fmt.Println("Server: http://localhost:8080") fmt.Println("API: http://localhost:8080/todos") fmt.Println("Spec: http://localhost:8080/openapi.json") fmt.Println("Docs: http://localhost:8080/docs") app.Listen(":8080") } func swaggerUIHandler(c *mizu.Ctx) error { return c.HTML(200, ` Todo API Docs
`) } ``` ## Using the OpenAPI Spec ### Generate Client Code Use tools like `openapi-generator` to create typed clients: ```bash theme={null} # Install openapi-generator npm install -g @openapitools/openapi-generator-cli # Generate TypeScript client openapi-generator-cli generate \ -i http://localhost:8080/openapi.json \ -g typescript-fetch \ -o ./client # Generate Python client openapi-generator-cli generate \ -i http://localhost:8080/openapi.json \ -g python \ -o ./python-client # Generate Go client openapi-generator-cli generate \ -i http://localhost:8080/openapi.json \ -g go \ -o ./go-client ``` ### Import into Postman 1. Open Postman 2. Click Import 3. Enter URL: `http://localhost:8080/openapi.json` 4. Your API is now ready to test! ### Validate with oasdiff Check for breaking changes between versions: ```bash theme={null} # Compare two versions of your spec oasdiff diff old-api.json http://localhost:8080/openapi.json ``` ## Service Metadata Add service information that appears in the generated spec: ```go theme={null} svc := contract.Register[todo.API](impl, contract.WithName("Todo"), contract.WithDescription("A simple todo list API"), contract.WithDefaultResource("todos"), ) ``` This produces: ```json theme={null} { "openapi": "3.0.3", "info": { "title": "Todo", "description": "A simple todo list API", "version": "0.1.0" } } ``` ## Multiple Services Generate specs for multiple services: ```go theme={null} todoSvc := contract.Register[todo.API](todoImpl, contract.WithName("Todo"), contract.WithDefaultResource("todos"), ) userSvc := contract.Register[user.API](userImpl, contract.WithName("User"), contract.WithDefaultResource("users"), ) app := mizu.New() // Mount REST endpoints rest.Mount(app.Router, todoSvc) // /todos rest.Mount(app.Router, userSvc) // /users // Serve separate OpenAPI specs todoSpec, _ := rest.OpenAPIDocument(todoSvc) userSpec, _ := rest.OpenAPIDocument(userSvc) app.Get("/api/todo/openapi.json", func(c *mizu.Ctx) error { c.Set("Content-Type", "application/json") return c.Send(todoSpec) }) app.Get("/api/user/openapi.json", func(c *mizu.Ctx) error { c.Set("Content-Type", "application/json") return c.Send(userSpec) }) app.Listen(":8080") ``` ## Type Mappings Contract converts Go types to OpenAPI schema types: | Go Type | OpenAPI Type | Format | | -------------- | ------------------ | ----------------------- | | `string` | `string` | - | | `bool` | `boolean` | - | | `int`, `int32` | `integer` | `int32` | | `int64` | `integer` | `int64` | | `float32` | `number` | `float` | | `float64` | `number` | `double` | | `time.Time` | `string` | `date-time` | | `*T` (pointer) | `anyOf: [T, null]` | - | | `[]T` (slice) | `array` | items: T | | `map[string]T` | `object` | additionalProperties: T | ## Optional Fields Use `omitempty` to mark fields as optional: ```go theme={null} type CreateInput struct { Title string `json:"title"` // required Description string `json:"description,omitempty"` // optional } ``` This produces: ```json theme={null} { "type": "object", "properties": { "title": { "type": "string" }, "description": { "type": "string" } }, "required": ["title"] } ``` ## Path Parameters Fields with the `path` tag are documented as path parameters: ```go theme={null} type GetInput struct { ID string `json:"id" path:"id"` } ``` This produces: ```json theme={null} { "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } } ] } ``` ## Query Parameters For GET requests, input fields not used in the path become query parameters: ```go theme={null} type ListInput struct { Status string `json:"status" query:"status"` Limit int `json:"limit" query:"limit"` } ``` This produces: ```json theme={null} { "parameters": [ { "name": "status", "in": "query", "required": true, "schema": { "type": "string" } }, { "name": "limit", "in": "query", "required": true, "schema": { "type": "integer", "format": "int32" } } ] } ``` ## Common Questions ### How do I add descriptions to endpoints? Method descriptions come from the `contract.WithDescription` option at the method level (not yet implemented). For now, the operation ID includes the method name. ### What OpenAPI version is generated? OpenAPI 3.0.3, a widely supported version. ### Can I customize the generated spec? The current generator produces a standard spec. For extensive customization: 1. Generate the spec with `rest.OpenAPIDocument(svc)` 2. Parse and modify it programmatically 3. Serve the modified version ### Does it support authentication schemes? Yes, when using `WithDefaults` with auth settings: ```go theme={null} svc := contract.Register[todo.API](impl, contract.WithDefaults(contract.Defaults{ Auth: "bearer", }), ) ``` This adds bearer token security to the spec. ### Can I add custom tags or extensions? Not directly. The generator creates a minimal, standards-compliant spec. For advanced customization, modify the generated spec programmatically. ## What's Next? * **[REST Transport](/contract/rest)** - The endpoints documented by OpenAPI * **[Type System](/contract/types)** - How Go types become JSON schemas * **[Transports Overview](/contract/transports-overview)** - Compare all transports # Overview Source: https://docs.go-mizu.dev/contract/overview Learn what the Contract package is and how it helps you build APIs in Go with compile-time safety ## What is Contract? Contract is a Go package that lets you define your API as a Go interface, then automatically expose it through multiple protocols (REST, JSON-RPC, MCP) without duplicating code. Write your business logic once, serve it everywhere. The key insight is simple: **your API is a contract between you and your clients**. Contract takes this literally by using Go interfaces to define that contract, giving you compile-time safety and clear API boundaries. ## The Problem Contract Solves Imagine you're building a todo list application. You want to: * Let web browsers call your API via REST * Let other services call via JSON-RPC for batching * Let AI assistants (like Claude) use your API via MCP * Generate TypeScript types for your frontend Without Contract, you'd write repetitive code for each protocol. Each protocol (REST, JSON-RPC, MCP) has its own request format, response format, and error handling. You'd end up writing essentially the same logic three times, just wrapped differently: ```go theme={null} // REST handler - one version // This handles HTTP requests with methods like GET, POST, PUT, DELETE func handleRESTCreateTodo(w http.ResponseWriter, r *http.Request) { var input CreateInput json.NewDecoder(r.Body).Decode(&input) todo, err := createTodo(input) if err != nil { http.Error(w, err.Error(), 500) return } json.NewEncoder(w).Encode(todo) } // JSON-RPC handler - another version // JSON-RPC uses a different format: method names in the body func handleJSONRPCCreateTodo(req JSONRPCRequest) JSONRPCResponse { var input CreateInput json.Unmarshal(req.Params, &input) todo, err := createTodo(input) if err != nil { return JSONRPCResponse{Error: ...} } return JSONRPCResponse{Result: todo} } // MCP handler - yet another version // MCP wraps responses in "content" blocks for AI assistants ``` That's a lot of repetitive code! Every protocol handler does the same thing: parse input, call your business logic, format the response. And if you add a new field to `CreateInput`, you need to verify every handler handles it correctly. ## The Contract Solution With Contract, you organize your code into a dedicated package (like `todo`) and define your API as a Go interface. The interface is named simply `API` because the package name (`todo`) already provides context: ```go theme={null} package todo import ( "context" ) // API defines the contract for todo operations. // This interface describes what your service can do. // Anyone reading this interface immediately understands your API. type API interface { // Create adds a new todo item. Create(ctx context.Context, in *CreateInput) (*Todo, error) // List returns all todo items. List(ctx context.Context) (*ListOutput, error) // Get retrieves a single todo by ID. Get(ctx context.Context, in *GetInput) (*Todo, error) // Delete removes a todo by ID. Delete(ctx context.Context, in *DeleteInput) error } ``` Then implement it with your business logic. The implementation is a struct named `Service` (again, the package name provides context): ```go theme={null} package todo // Service implements the todo.API interface. // This struct contains your actual business logic and dependencies. type Service struct { db Database // Your database connection } // Create implements todo.API.Create func (s *Service) Create(ctx context.Context, in *CreateInput) (*Todo, error) { // Your business logic here - no HTTP or protocol code! return &Todo{ID: generateID(), Title: in.Title}, nil } // ... implement other methods ``` Finally, register and serve. In your main package, you bring everything together: ```go theme={null} 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" "github.com/go-mizu/mizu/contract/v2/transport/jsonrpc" "github.com/go-mizu/mizu/contract/v2/transport/mcp" "yourapp/todo" // Import your todo package ) func main() { // Create your service instance with its dependencies todoSvc := &todo.Service{ // db: yourDatabase, } // Register your implementation against the interface. // The type parameter [todo.API] tells Contract which interface to use. // Contract inspects this interface to discover all your methods. svc := contract.Register[todo.API](todoSvc, // WithDefaultResource groups all methods under "todos". // This affects URLs in REST and method names in JSON-RPC. contract.WithDefaultResource("todos"), ) app := mizu.New() // Mount on all transports - same service, different protocols rest.Mount(app.Router, svc) // REST: POST /todos, GET /todos, etc. jsonrpc.Mount(app.Router, "/rpc", svc) // JSON-RPC: todos.create, todos.list mcp.Mount(app.Router, "/mcp", svc) // MCP tools for AI assistants app.Listen(":8080") } ``` **One implementation, all protocols.** Change your interface, and all transports update automatically. Add a new method to `todo.API`, implement it in `todo.Service`, and it's instantly available via REST, JSON-RPC, and MCP. ## Why Interface-First? Contract uses Go interfaces to define your API. This is called "interface-first" or "contract-first" design. Let's explore why this approach is powerful. ### Compile-Time Safety Go's compiler checks that your implementation matches your interface. If you forget to implement a method, or if you implement it with the wrong signature, you get a compile error - not a runtime crash: ```go theme={null} package todo type API interface { Create(ctx context.Context, in *CreateInput) (*Todo, error) } // This won't compile! type BrokenService struct{} // Error: BrokenService does not implement todo.API // (missing method Create) ``` This is a huge advantage over dynamic approaches where you might only discover the mismatch when a user calls your API. ### Clear API Boundaries The interface serves as documentation. Anyone can read it and understand exactly what your service does, without digging through implementation details: ```go theme={null} package user // API defines all operations for user management. // This interface is the single source of truth for your API. type API interface { // Create registers a new user account. // Returns an error if the email is already registered. Create(ctx context.Context, in *CreateInput) (*User, error) // Get retrieves a user by their unique ID. // Returns a NOT_FOUND error if the user doesn't exist. Get(ctx context.Context, in *GetInput) (*User, error) // List returns all users with pagination support. // Use Limit and Offset in the input to page through results. List(ctx context.Context, in *ListInput) (*ListOutput, error) // Delete removes a user account permanently. // This action cannot be undone. Delete(ctx context.Context, in *DeleteInput) error } ``` ### Easy Testing Because your business logic is in plain Go methods, you can test it directly without HTTP mocking. You can also create mock implementations for testing code that depends on your service: ```go theme={null} package user_test // MockService implements user.API for testing type MockService struct { CreateFunc func(ctx context.Context, in *CreateInput) (*User, error) } func (m *MockService) Create(ctx context.Context, in *CreateInput) (*User, error) { if m.CreateFunc != nil { return m.CreateFunc(ctx, in) } // Return a default test user return &User{ID: "test-123", Name: in.Name}, nil } func TestSomethingThatUsesUserAPI(t *testing.T) { mock := &MockService{ CreateFunc: func(ctx context.Context, in *CreateInput) (*User, error) { // Custom behavior for this test return &User{ID: "custom-id"}, nil }, } // Use mock in your test... } ``` ### Separation of Concerns The interface separates "what your API does" (the contract) from "how it's implemented" (the business logic). This lets you swap implementations without changing your API: ```go theme={null} package storage // API defines storage operations. // The same interface can have multiple implementations. type API interface { Save(ctx context.Context, in *SaveInput) error Load(ctx context.Context, in *LoadInput) (*Data, error) } // FileService stores data on the local filesystem type FileService struct { basePath string } // S3Service stores data in Amazon S3 type S3Service struct { bucket string } // MemoryService stores data in memory (great for tests) type MemoryService struct { data map[string][]byte } // All three implement storage.API! // Your application can switch between them via configuration. ``` ## How Contract Works Here's what happens when you use Contract, step by step: ``` Your Go Interface (todo.API) │ ▼ contract.Register[todo.API](impl) │ │ 1. Inspects interface using reflection │ 2. Discovers all methods (Create, List, Get, Delete) │ 3. Extracts input/output types │ 4. Generates JSON schemas for each type ▼ ┌─────────────┐ │ Service │ │ Descriptor │ │ - Methods │ │ - Types │ │ - Schemas │ └─────────────┘ │ ┌────────────────┼────────────────┐ ▼ ▼ ▼ ┌─────────┐ ┌──────────┐ ┌─────────┐ │ REST │ │ JSON-RPC │ │ MCP │ │ Handler │ │ Handler │ │ Handler │ └─────────┘ └──────────┘ └─────────┘ │ │ │ ▼ ▼ ▼ Web Browsers Other Services AI Assistants ``` 1. **You define** an interface with your API methods (like `todo.API`) 2. **You implement** the interface with your business logic (like `todo.Service`) 3. **Contract inspects** your interface at startup using Go's reflection capabilities 4. **Contract creates** efficient invokers and JSON schemas from your types 5. **Transport handlers** receive HTTP requests and convert them to method calls 6. **Your method runs** with the parsed input and returns results or errors 7. **Transport handlers** convert the response back to the appropriate protocol format The key insight is that Contract does all the heavy lifting at startup time. When a request comes in, it's a fast, direct call to your method - no runtime reflection or type conversion overhead. ## Key Concepts Let's understand the core concepts you'll work with in Contract. ### Interface (The Contract) An interface defines what operations your API supports. Think of it as a menu at a restaurant - it lists what's available without explaining how each dish is prepared: ```go theme={null} package todo // API is your service's "menu" - it lists all available operations. // Each method becomes an API endpoint that clients can call. type API interface { // Create makes a new todo. Clients call this to add items. Create(ctx context.Context, in *CreateInput) (*Todo, error) // List returns all todos. Clients call this to see everything. List(ctx context.Context) (*ListOutput, error) // Get retrieves one todo by ID. Clients call this for details. Get(ctx context.Context, in *GetInput) (*Todo, error) // Delete removes a todo. Clients call this to clean up. Delete(ctx context.Context, in *DeleteInput) error } ``` ### Implementation (The Service) A struct that implements the interface. This is where your actual business logic lives - the "kitchen" that prepares the dishes: ```go theme={null} package todo // Service implements todo.API with actual business logic. // It contains dependencies like databases, caches, and external services. type Service struct { db Database // Where todos are stored cache Cache // For faster lookups } // Create implements todo.API.Create func (s *Service) Create(ctx context.Context, in *CreateInput) (*Todo, error) { // Validate input if in.Title == "" { return nil, contract.ErrInvalidArgument("title is required") } // Create the todo todo := &Todo{ ID: generateID(), Title: in.Title, } // Save to database if err := s.db.Save(ctx, todo); err != nil { return nil, err } return todo, nil } ``` ### Resource A resource is a namespace that groups related methods. It affects how your API endpoints are named and organized: ```go theme={null} svc := contract.Register[todo.API](impl, contract.WithDefaultResource("todos"), // All methods grouped under "todos" ) ``` With this resource name, your endpoints become: * REST: `/todos` (POST for Create, GET for List), `/todos/{id}` (GET, DELETE) * JSON-RPC: `todos.create`, `todos.list`, `todos.get`, `todos.delete` * MCP tools: `todos.create`, `todos.list`, etc. ### Transport A transport is a protocol handler that converts HTTP requests into method calls. Think of transports as translators - they speak different "languages" (protocols) but all communicate with the same backend (your service): | Transport | Protocol | Best For | | --------- | ---------------------- | ------------------------------------------ | | REST | HTTP REST | Web browsers, curl, most HTTP clients | | JSON-RPC | JSON-RPC 2.0 | Batch operations, service-to-service calls | | MCP | Model Context Protocol | AI assistants like Claude | ### Type Registry Contract automatically analyzes your Go types and creates JSON schemas. These schemas describe your data structures in a language-agnostic way: ```go theme={null} // Your Go type type CreateInput struct { Title string `json:"title"` Description string `json:"description,omitempty"` } // Contract generates this JSON Schema automatically: // { // "type": "object", // "properties": { // "title": {"type": "string"}, // "description": {"type": "string"} // }, // "required": ["title"] // } ``` These schemas are used for: * **OpenAPI documentation**: Generate API docs automatically * **Client code generation**: Create typed clients in TypeScript, Python, etc. * **AI tool definitions**: Tell AI assistants what parameters your tools accept ## What You'll Learn This documentation will teach you everything you need to build APIs with Contract: 1. **[Quick Start](/contract/quick-start)** - Build your first API in 5 minutes with step-by-step instructions 2. **[Defining Services](/contract/service)** - How to structure your interfaces, implementations, and types 3. **[Registration](/contract/register)** - What happens when you call `contract.Register` and available options 4. **[Type System](/contract/types)** - How Go types map to JSON schemas and wire formats 5. **[Error Handling](/contract/errors)** - Return meaningful errors that work across all protocols 6. **[Transports](/contract/transports-overview)** - Choose the right protocol for your use case 7. **[Testing](/contract/testing)** - Test your services easily without HTTP mocking 8. **[Architecture](/contract/architecture)** - Understand how Contract works under the hood ## Quick Example Here's a complete, working example. You can copy this code, run it, and start experimenting: ```go theme={null} package main import ( "context" "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" ) // ============================================================ // Step 1: Define your types // These structs define the shape of your API's data. // ============================================================ // GreetInput is what clients send when calling Greet. // The json tags control how fields are named in JSON. type GreetInput struct { Name string `json:"name"` // The name to greet } // GreetOutput is what clients receive back from Greet. type GreetOutput struct { Message string `json:"message"` // The greeting message } // ============================================================ // Step 2: Define your interface (the contract) // This interface declares what your API can do. // ============================================================ // API defines the greeting service operations. // Any struct that implements this interface can be used as the service. type API interface { // Greet creates a personalized greeting message. // It takes a name and returns a friendly message. Greet(ctx context.Context, in *GreetInput) (*GreetOutput, error) } // ============================================================ // Step 3: Implement the interface // This struct contains your actual business logic. // ============================================================ // Service implements the API interface. type Service struct{} // Greet implements API.Greet with a simple greeting. func (s *Service) Greet(ctx context.Context, in *GreetInput) (*GreetOutput, error) { // This is your business logic - simple in this example, // but could include database calls, external APIs, etc. return &GreetOutput{ Message: fmt.Sprintf("Hello, %s!", in.Name), }, nil } // ============================================================ // Step 4: Wire everything together // ============================================================ func main() { // Create your service instance impl := &Service{} // Register the service with Contract. // Contract analyzes the API interface and prepares it for serving. svc := contract.Register[API](impl, contract.WithDefaultResource("greetings"), ) // Create a mizu app to handle HTTP requests app := mizu.New() // Mount transports - each one exposes your service via a different protocol rest.Mount(app.Router, svc) // REST API jsonrpc.Mount(app.Router, "/rpc", svc) // JSON-RPC endpoint // Start the server fmt.Println("Server running on http://localhost:8080") fmt.Println("") fmt.Println("Try these commands:") fmt.Println(" REST: curl -X POST http://localhost:8080/greetings -d '{\"name\":\"World\"}'") fmt.Println(" JSON-RPC: curl -X POST http://localhost:8080/rpc -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"greetings.greet\",\"params\":{\"name\":\"World\"}}'") app.Listen(":8080") } ``` Test it with these commands: ```bash theme={null} # REST - Uses HTTP POST with JSON body curl -X POST http://localhost:8080/greetings \ -H "Content-Type: application/json" \ -d '{"name": "World"}' # Output: {"message":"Hello, World!"} # JSON-RPC - Uses JSON-RPC envelope format curl -X POST http://localhost:8080/rpc \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"greetings.greet","params":{"name":"World"}}' # Output: {"jsonrpc":"2.0","id":1,"result":{"message":"Hello, World!"}} ``` Both requests call the same `Greet` method - Contract handles the protocol translation automatically. ## Common Questions ### Do I need to learn all the transports? No! Start with REST - it's the most familiar and works with any HTTP client. You can add other transports later as needed. Your business logic stays exactly the same regardless of which transports you use. ### Is Contract only for new projects? Contract works great for new projects, but you can also gradually adopt it in existing codebases. Start by converting one service to see how it fits. Your existing REST handlers can coexist with Contract-powered endpoints. ### How does Contract handle authentication? Contract focuses on business logic, not authentication. Use standard mizu middleware to authenticate requests before they reach your methods. You can pass user information to your service methods through context values: ```go theme={null} func authMiddleware(next mizu.Handler) mizu.Handler { return func(c *mizu.Ctx) error { user := validateToken(c.Get("Authorization")) ctx := context.WithValue(c.Context(), "user", user) c.SetContext(ctx) return next(c) } } ``` ### Why use interfaces instead of structs with method tags? Interfaces give you compile-time safety. If your implementation doesn't match the interface, the compiler catches it immediately. With struct-based or decorator approaches, you might only find out at runtime if a method signature is wrong. Go's type system is your friend - let it help you catch bugs early. ### How do I organize my code? We recommend organizing each service into its own package: ``` yourapp/ ├── main.go # Wires everything together ├── todo/ │ ├── api.go # Interface definition (todo.API) │ ├── service.go # Implementation (todo.Service) │ └── types.go # Input/output types ├── user/ │ ├── api.go # Interface definition (user.API) │ ├── service.go # Implementation (user.Service) │ └── types.go # Input/output types ``` This keeps related code together and makes the package names meaningful (`todo.API` is clearer than `TodoAPI`). ## Next Steps Ready to build your first API? Start with the **[Quick Start Guide](/contract/quick-start)** - you'll have a working API in 5 minutes with detailed explanations of each step. If you prefer understanding the concepts first, read about **[Defining Services](/contract/service)** to learn how to structure your interfaces and implementations properly. # Quick Start Source: https://docs.go-mizu.dev/contract/quick-start Build your first Contract API in 5 minutes - a step-by-step guide for beginners ## What You'll Build This guide walks you through building your first API with Contract. By the end, you'll have a working todo list API accessible via REST and JSON-RPC. You'll learn the three-step pattern that every Contract service follows: 1. **Define your interface** - The contract that describes your API 2. **Implement the interface** - Your business logic 3. **Register and serve** - Make it available via HTTP We'll also show you the recommended project structure using Go packages, where your service lives in a `todo` package with types named `API` and `Service` (the package name provides the "todo" context). ## Prerequisites Before starting, make sure you have: * **Go 1.22 or later** installed ([download Go](https://go.dev/dl/)) * A terminal (Command Prompt, Terminal, or any shell) * A text editor (VS Code, GoLand, or your favorite) * **curl** for testing (usually pre-installed on Mac/Linux) If you're not sure which Go version you have, run: ```bash theme={null} go version ``` ## Step 1: Create Your Project Create a new directory and initialize a Go module. A Go module is a collection of Go packages with a `go.mod` file that tracks dependencies: ```bash theme={null} # Create a new directory for your project mkdir todo-api cd todo-api # Initialize a Go module # This creates a go.mod file that tracks your dependencies go mod init todo-api ``` **What this does**: Creates a new Go project. The `go.mod` file tells Go where to find dependencies and what version of Go you're using. Now create the directory structure for your todo package: ```bash theme={null} # Create the todo package directory mkdir todo ``` Your project structure will look like this: ``` todo-api/ ├── go.mod # Go module file (created by go mod init) ├── main.go # We'll create this - wires everything together └── todo/ ├── api.go # We'll create this - the interface (contract) ├── service.go # We'll create this - the implementation └── types.go # We'll create this - input/output types ``` ## Step 2: Install Dependencies Add the required packages. These commands download the mizu web framework and Contract v2: ```bash theme={null} go get github.com/go-mizu/mizu go get github.com/go-mizu/mizu/contract/v2 ``` **What this does**: * `github.com/go-mizu/mizu` is the mizu web framework that handles HTTP routing * `github.com/go-mizu/mizu/contract/v2` is Contract v2, which provides the interface-first API pattern After running these commands, your `go.mod` file will list these as dependencies. ## Step 3: Create Your Types Create `todo/types.go`. This file defines the data structures your API works with. These are called "DTOs" (Data Transfer Objects) because they define what data moves between clients and your server: ```go theme={null} // todo/types.go package todo // Todo represents a single todo item in our system. // This is the main data type that clients work with. // // The `json` tags tell Go how to convert this struct to/from JSON. // For example, `json:"id"` means the ID field becomes "id" in JSON. type Todo struct { ID string `json:"id"` // Unique identifier for this todo Title string `json:"title"` // What the todo is about Completed bool `json:"completed"` // Whether it's done } // CreateInput is what clients send when creating a new todo. // We only need the title - the ID is generated automatically, // and Completed defaults to false for new todos. type CreateInput struct { Title string `json:"title"` // Required: the todo's title } // GetInput is what clients send when fetching a specific todo. // The ID tells us which todo to retrieve. type GetInput struct { ID string `json:"id"` // The ID of the todo to get } // DeleteInput is what clients send when deleting a todo. type DeleteInput struct { ID string `json:"id"` // The ID of the todo to delete } // ListOutput is what we return when listing all todos. // It wraps the items in an object so we can add metadata (like count) // without breaking clients if we add more fields later. type ListOutput struct { Items []*Todo `json:"items"` // The list of todos Count int `json:"count"` // How many todos total } ``` **Why separate input/output types?** You might wonder why we have separate types like `CreateInput` and `GetInput` instead of just using `Todo` everywhere. There are good reasons: 1. **Input types** describe what clients send to you. For `Create`, we only need a title - we generate the ID. 2. **Output types** describe what you send back. We return the full `Todo` with the generated ID. 3. **Flexibility**: You can add fields to outputs without requiring clients to send them, and vice versa. 4. **Validation**: Input types only contain the fields you actually accept. ## Step 4: Define Your Interface (The Contract) Create `todo/api.go`. This is the heart of Contract - your interface defines what your API can do. Think of it as a "menu" that lists all available operations: ```go theme={null} // todo/api.go package todo import "context" // API defines what operations our todo service supports. // This interface IS our API contract - it's the single source of truth // for what methods exist and what they accept/return. // // The name is simply "API" because the package name "todo" already // provides context. When used outside this package, it's "todo.API" // which reads naturally: "the todo API". // // Contract rules for methods: // - First parameter must be context.Context (for timeouts, cancellation) // - Input parameters must be pointer to struct (*CreateInput) // - Output must be pointer to struct (*Todo) or just error // - Last return value must be error type API interface { // Create adds a new todo item and returns the created todo with its ID. // The client sends a CreateInput with the title, and we return the // complete Todo including the generated ID. Create(ctx context.Context, in *CreateInput) (*Todo, error) // List returns all todos in the system. // This method has no input (just context) because listing doesn't // require any parameters. We return a ListOutput containing the items. List(ctx context.Context) (*ListOutput, error) // Get retrieves a single todo by its ID. // If the todo doesn't exist, we return an error. Get(ctx context.Context, in *GetInput) (*Todo, error) // Delete removes a todo by its ID. // This method only returns error (no output) because there's nothing // meaningful to return after deletion. Delete(ctx context.Context, in *DeleteInput) error } ``` **Key points about the interface**: 1. **Package naming**: The interface is named `API` (not `TodoAPI`) because `todo.API` reads naturally when imported. 2. **Context first**: Every method starts with `ctx context.Context`. This is a Go pattern for passing request-scoped data like timeouts and cancellation signals. 3. **Pointer inputs**: Input types are pointers (`*CreateInput`) so they can be `nil` for methods that don't need input. 4. **Error handling**: Every method returns `error` as the last return value. This is how you communicate failures to clients. 5. **Method naming**: Names like `Create`, `List`, `Get`, `Delete` automatically map to HTTP verbs when using REST. ## Step 5: Implement Your Interface Create `todo/service.go`. This is where your actual code lives - the "kitchen" that prepares the dishes from your "menu" (the interface): ```go theme={null} // todo/service.go package todo import ( "context" "errors" "fmt" "sync" ) // Service implements the todo.API interface. // This is where your actual business logic lives. // // The name is simply "Service" because the package name "todo" already // provides context. When used outside this package, it's "todo.Service". // // In a real application, this struct would hold dependencies like: // - Database connections (*sql.DB, *gorm.DB) // - Cache clients (*redis.Client) // - External service clients // // For this example, we use in-memory storage to keep things simple. type Service struct { mu sync.RWMutex // Protects concurrent access to the map todos map[string]*Todo // In-memory storage (ID -> Todo) nextID int // Simple counter for generating IDs } // NewService creates a new Service instance ready to use. // This is called a "constructor function" in Go. func NewService() *Service { return &Service{ todos: make(map[string]*Todo), } } // Create adds a new todo to our storage. // This implements the API.Create method defined in our interface. func (s *Service) Create(ctx context.Context, in *CreateInput) (*Todo, error) { // Step 1: Validate the input // Always validate user input! Never trust data from clients. if in.Title == "" { return nil, errors.New("title is required") } // Step 2: Lock for thread-safe access // Multiple HTTP requests might call Create at the same time, // so we need to protect our shared map. s.mu.Lock() defer s.mu.Unlock() // Unlock when this function returns // Step 3: Generate a unique ID s.nextID++ id := fmt.Sprintf("todo_%d", s.nextID) // Step 4: Create and store the todo todo := &Todo{ ID: id, Title: in.Title, Completed: false, // New todos start as not completed } s.todos[id] = todo // Step 5: Return the created todo // The client gets back the full todo including the generated ID return todo, nil } // List returns all todos in storage. // This implements the API.List method defined in our interface. func (s *Service) List(ctx context.Context) (*ListOutput, error) { // Use RLock (read lock) since we're only reading, not writing. // Multiple readers can hold RLock simultaneously. s.mu.RLock() defer s.mu.RUnlock() // Convert map values to a slice items := make([]*Todo, 0, len(s.todos)) for _, todo := range s.todos { items = append(items, todo) } // Return wrapped in ListOutput return &ListOutput{ Items: items, Count: len(items), }, nil } // Get retrieves a single todo by ID. // This implements the API.Get method defined in our interface. func (s *Service) Get(ctx context.Context, in *GetInput) (*Todo, error) { s.mu.RLock() defer s.mu.RUnlock() // Look up the todo in our map todo, exists := s.todos[in.ID] if !exists { // Todo not found - return an error // In production, use contract.ErrNotFound for proper HTTP 404 return nil, errors.New("todo not found") } return todo, nil } // Delete removes a todo from storage. // This implements the API.Delete method defined in our interface. func (s *Service) Delete(ctx context.Context, in *DeleteInput) error { s.mu.Lock() defer s.mu.Unlock() // Check if the todo exists before deleting if _, exists := s.todos[in.ID]; !exists { return errors.New("todo not found") } // Remove from map delete(s.todos, in.ID) // Return nil to indicate success (no error) return nil } ``` **Important concepts**: 1. **Package naming**: The struct is named `Service` (not `TodoService`) because `todo.Service` reads naturally. 2. **Constructor function**: `NewService()` is a common Go pattern for creating initialized instances. 3. **Thread safety**: We use `sync.RWMutex` because HTTP servers handle multiple requests concurrently. 4. **Error handling**: Return `nil, error` to indicate failure. Contract translates this to the appropriate protocol response. ## Step 6: Wire Everything Together Create `main.go` in your project root. This file imports your todo package and wires everything together: ```go theme={null} // 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" // Import your local todo package "todo-api/todo" ) func main() { // Step 1: Create an instance of your service // This is the struct that contains your business logic impl := todo.NewService() // Step 2: Register it with Contract // // The generic parameter [todo.API] tells Contract which interface to use. // Contract uses Go's reflection to inspect this interface and discover: // - All the methods (Create, List, Get, Delete) // - The input/output types for each method // - JSON schemas for those types // // This is the "magic" step where your plain Go code becomes an API! svc := contract.Register[todo.API](impl, // WithDefaultResource groups all methods under "todos" // This affects: // - REST paths: /todos, /todos/{id} // - JSON-RPC methods: todos.create, todos.list, etc. contract.WithDefaultResource("todos"), ) // Step 3: Create a mizu app // Mizu is a lightweight web framework that handles HTTP routing app := mizu.New() // Step 4: Mount the REST transport // This creates HTTP endpoints based on your method names: // Create -> POST /todos (create a new resource) // List -> GET /todos (list all resources) // Get -> GET /todos/{id} (get one resource) // Delete -> DELETE /todos/{id} (delete a resource) rest.Mount(app.Router, svc) // Step 5: Mount the JSON-RPC transport // All methods are available at /rpc with names like: // todos.create, todos.list, todos.get, todos.delete jsonrpc.Mount(app.Router, "/rpc", svc) // Step 6: Print helpful info and start the server fmt.Println("═══════════════════════════════════════════════════════════") fmt.Println(" Todo API is running!") fmt.Println("═══════════════════════════════════════════════════════════") fmt.Println() fmt.Println("REST endpoints:") fmt.Println(" POST http://localhost:8080/todos - Create a todo") fmt.Println(" GET http://localhost:8080/todos - List all todos") fmt.Println(" GET http://localhost:8080/todos/{id} - Get a todo") fmt.Println(" DELETE http://localhost:8080/todos/{id} - Delete a todo") fmt.Println() fmt.Println("JSON-RPC endpoint:") fmt.Println(" POST http://localhost:8080/rpc") fmt.Println(" Methods: todos.create, todos.list, todos.get, todos.delete") fmt.Println() // Start the HTTP server on port 8080 // This blocks until the server is shut down app.Listen(":8080") } ``` ## Complete Project Structure Your project should now look like this: ``` todo-api/ ├── go.mod ├── go.sum # Created automatically when you run go mod tidy ├── main.go # Wires everything together └── todo/ ├── api.go # Interface definition (todo.API) ├── service.go # Implementation (todo.Service) └── types.go # Input/output types ``` **Why organize code this way?** 1. **Clear separation**: Each file has a single responsibility 2. **Package-based naming**: `todo.API` and `todo.Service` are clearer than `TodoAPI` and `todoService` 3. **Testability**: You can easily mock `todo.API` for testing 4. **Scalability**: Add more services by creating new packages (`user/`, `order/`, etc.) ## Step 7: Run Your Server Before running, ensure all dependencies are properly resolved: ```bash theme={null} # Download dependencies and update go.sum go mod tidy ``` Then start your API server: ```bash theme={null} go run main.go ``` You should see: ``` ═══════════════════════════════════════════════════════════ Todo API is running! ═══════════════════════════════════════════════════════════ REST endpoints: POST http://localhost:8080/todos - Create a todo GET http://localhost:8080/todos - List all todos GET http://localhost:8080/todos/{id} - Get a todo DELETE http://localhost:8080/todos/{id} - Delete a todo JSON-RPC endpoint: POST http://localhost:8080/rpc Methods: todos.create, todos.list, todos.get, todos.delete ``` **Leave this terminal running** and open a new terminal for testing. ## Step 8: Test Your API Let's test your API using curl. Open a **new terminal window** (keep the server running in the first one). ### Create a Todo (REST) ```bash theme={null} curl -X POST http://localhost:8080/todos \ -H "Content-Type: application/json" \ -d '{"title": "Buy groceries"}' ``` **What this does**: * `POST` tells the server we want to create something * `/todos` is the resource path * `-H "Content-Type: application/json"` tells the server we're sending JSON * `-d '{"title": "Buy groceries"}'` is the JSON body (our CreateInput) **Expected output**: ```json theme={null} {"id":"todo_1","title":"Buy groceries","completed":false} ``` The server generated an ID (`todo_1`) and set `completed` to `false`. ### Create Another Todo ```bash theme={null} curl -X POST http://localhost:8080/todos \ -H "Content-Type: application/json" \ -d '{"title": "Learn Contract"}' ``` **Expected output**: ```json theme={null} {"id":"todo_2","title":"Learn Contract","completed":false} ``` ### List All Todos (REST) ```bash theme={null} curl http://localhost:8080/todos ``` **What this does**: A simple GET request to `/todos` calls our `List` method. **Expected output**: ```json theme={null} {"items":[{"id":"todo_1","title":"Buy groceries","completed":false},{"id":"todo_2","title":"Learn Contract","completed":false}],"count":2} ``` ### Get a Specific Todo (REST) ```bash theme={null} curl http://localhost:8080/todos/todo_1 ``` **What this does**: GET request to `/todos/{id}` calls our `Get` method with `id=todo_1`. **Expected output**: ```json theme={null} {"id":"todo_1","title":"Buy groceries","completed":false} ``` ### Delete a Todo (REST) ```bash theme={null} curl -X DELETE http://localhost:8080/todos/todo_1 ``` **What this does**: DELETE request removes the todo with the given ID. **Expected output**: Empty (HTTP 204 No Content means success with no body) Verify it's deleted: ```bash theme={null} curl http://localhost:8080/todos ``` Now you should only see one todo. ## Step 9: Try JSON-RPC The same service is also available via JSON-RPC. JSON-RPC uses a different format where you specify the method name in the request body. This is useful for: * **Batching**: Send multiple requests in one HTTP call * **RPC-style clients**: Some languages prefer explicit method names over HTTP verbs ### Create via JSON-RPC ```bash theme={null} curl -X POST http://localhost:8080/rpc \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "todos.create", "params": {"title": "Call mom"} }' ``` **What this does**: * All JSON-RPC requests go to `/rpc` as POST * `"jsonrpc": "2.0"` identifies this as JSON-RPC (required) * `"id": 1` helps match requests to responses (you choose the ID) * `"method": "todos.create"` is `{resource}.{method}` * `"params"` is our CreateInput **Expected output**: ```json theme={null} {"jsonrpc":"2.0","id":1,"result":{"id":"todo_3","title":"Call mom","completed":false}} ``` ### List via JSON-RPC ```bash theme={null} curl -X POST http://localhost:8080/rpc \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 2, "method": "todos.list" }' ``` ### Batch Requests (JSON-RPC Only) One of JSON-RPC's killer features is batching. Send multiple requests in one HTTP call: ```bash theme={null} curl -X POST http://localhost:8080/rpc \ -H "Content-Type: application/json" \ -d '[ {"jsonrpc":"2.0","id":1,"method":"todos.create","params":{"title":"First task"}}, {"jsonrpc":"2.0","id":2,"method":"todos.create","params":{"title":"Second task"}}, {"jsonrpc":"2.0","id":3,"method":"todos.list"} ]' ``` All three operations execute in one HTTP call! You get an array of responses back, matched by their `id` values. ## Understanding What Happened Let's recap what Contract did for you behind the scenes: 1. **Inspected your interface**: When you called `contract.Register[todo.API]()`, Contract used Go's reflection to discover all methods in the `todo.API` interface and their input/output types. 2. **Generated JSON schemas**: Your Go structs (`Todo`, `CreateInput`, etc.) were automatically converted to JSON schemas. These schemas are used for documentation and AI tool definitions. 3. **Created REST endpoints**: Based on your method names, Contract created HTTP endpoints: * `Create` → POST /todos (HTTP convention: POST creates resources) * `List` → GET /todos (HTTP convention: GET retrieves resources) * `Get` → GET /todos/ (path parameter for specific resource) * `Delete` → DELETE /todos/ (HTTP convention: DELETE removes resources) 4. **Created JSON-RPC handlers**: All methods became available at `/rpc`: * `todos.create`, `todos.list`, `todos.get`, `todos.delete` 5. **Compile-time safety**: If your `Service` didn't implement all methods in `API`, Go's compiler would have caught it before you even ran the program. ## Common Questions ### Why doesn't my method appear as an endpoint? Methods must follow Contract's rules: * **Must be in the interface**: Methods only on the struct (not in the interface) won't be exposed * **First parameter must be `context.Context`**: This is required for proper request handling * **Input must be pointer to struct**: Use `*CreateInput`, not `CreateInput` * **Last return must be `error`**: Every method needs to report success/failure ### How do I add more transports? Import the transport package and mount it: ```go theme={null} import "github.com/go-mizu/mizu/contract/v2/transport/mcp" // In main(): mcp.Mount(app.Router, "/mcp", svc) // For AI assistants like Claude ``` ### How do I handle errors properly? The quick start uses simple `errors.New()`. For production, use Contract's typed errors for proper HTTP status codes: ```go theme={null} import contract "github.com/go-mizu/mizu/contract/v2" // Returns HTTP 404 Not Found return nil, contract.ErrNotFound("todo not found") // Returns HTTP 400 Bad Request return nil, contract.ErrInvalidArgument("title is required") // Returns HTTP 401 Unauthorized return nil, contract.ErrUnauthenticated("please log in") ``` See the [Error Handling](/contract/errors) guide for all error types. ### How do I add a database? Replace the in-memory map with your database client. The Service struct holds your dependencies: ```go theme={null} type Service struct { db *sql.DB // Or *gorm.DB, *pgx.Pool, etc. } func NewService(db *sql.DB) *Service { return &Service{db: db} } func (s *Service) Create(ctx context.Context, in *CreateInput) (*Todo, error) { // Use s.db to insert into your database result, err := s.db.ExecContext(ctx, "INSERT INTO todos (title, completed) VALUES (?, ?)", in.Title, false, ) if err != nil { return nil, err } id, _ := result.LastInsertId() return &Todo{ ID: fmt.Sprintf("todo_%d", id), Title: in.Title, }, nil } ``` ## What's Next? Now that you have a working API, explore these topics: * **[Defining Services](/contract/service)** - Learn all the method signature patterns and interface design * **[Registration](/contract/register)** - Understand all registration options * **[Error Handling](/contract/errors)** - Proper error handling across protocols * **[REST Transport](/contract/rest)** - Deep dive into REST configuration * **[JSON-RPC Transport](/contract/jsonrpc)** - Batch requests and notifications * **[MCP Transport](/contract/mcp)** - Connect your API to AI assistants * **[Testing](/contract/testing)** - How to test your services # Registration Source: https://docs.go-mizu.dev/contract/register Learn how to register services with Contract and understand all available options ## 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: 1. **Inspects your interface** - Uses Go's reflection to discover all methods in your interface 2. **Extracts type information** - Finds all input and output types for each method 3. **Generates schemas** - Creates JSON schemas from your Go types (used for validation and documentation) 4. **Creates invokers** - Builds efficient callable wrappers for your methods 5. **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: ```go theme={null} 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: ```go theme={null} 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) { ... } ``` ```go theme={null} // 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: ```go theme={null} 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: ```go theme={null} 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**: ```go theme={null} // 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: ```go theme={null} 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: ```go theme={null} 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: ```go theme={null} 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) } ``` ```go theme={null} 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: ```go theme={null} // 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: ```go theme={null} 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**: 1. **API versioning**: Add version prefix to paths ```go theme={null} contract.WithMethodHTTP("Create", "POST", "/v2/todos") ``` 2. **Custom actions**: Operations that don't fit CRUD ```go theme={null} contract.WithMethodHTTP("Archive", "POST", "/todos/{id}/archive") contract.WithMethodHTTP("Publish", "POST", "/posts/{id}/publish") contract.WithMethodHTTP("SendEmail", "POST", "/users/{id}/send-email") ``` 3. **Nested resources**: Related sub-resources ```go theme={null} 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: ```go theme={null} 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: ```go theme={null} 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: ```go theme={null} package chat type API interface { // Chat returns a stream of message chunks Chat(ctx context.Context, in *ChatInput) (*ChatOutput, error) } ``` ```go theme={null} 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: ```go theme={null} 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 ### Call Invoke a method programmatically without going through HTTP: ```go theme={null} 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 ### NewInput Create a new instance of a method's input type. This is useful for transports and testing: ```go theme={null} 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. ## How Method Names are Transformed 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 ```go theme={null} 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) } ``` ## Path Parameter Extraction 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: 1. **Field with `path:"id"` tag** - Explicit path parameter binding 2. **Field named `ID`** - Standard Go naming 3. **Field ending in `ID`** - Like `TodoID`, `UserID` ### Using the path Tag The `path` tag explicitly marks a field as a path parameter: ```go theme={null} 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: 1. Extracts `abc123` from the URL path 2. Creates a new `GetInput` instance 3. Sets the `ID` field to `"abc123"` 4. Passes this to your method ### Multiple Path Parameters For nested resources, you can have multiple path parameters: ```go theme={null} 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 } ``` ```go theme={null} 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: ```go theme={null} // 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"` } ``` ```go theme={null} // 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 } ``` ```go theme={null} // todo/service.go package todo type Service struct { // dependencies... } var _ API = (*Service)(nil) func NewService() *Service { return &Service{} } // Implement all methods... ``` ```go theme={null} // 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: ```go theme={null} // 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`: ```go theme={null} // 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: ```go theme={null} 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 ``` ```go theme={null} // 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: ```go theme={null} var _ API = (*Service)(nil) // Fails at compile time if Service doesn't implement API ``` ## Best Practices ### Use Descriptive Metadata Good names and descriptions help users (and AI assistants) understand your API: ```go theme={null} 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: ```go theme={null} // 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: ```go theme={null} // 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"), ) } ``` ```go theme={null} // 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: ```go theme={null} 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: ```go theme={null} // 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: ```go theme={null} 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](/contract/types)** - How Go types are converted to JSON schemas * **[Transports](/contract/transports-overview)** - Mount your service on REST, JSON-RPC, or MCP * **[Error Handling](/contract/errors)** - Handle errors consistently across protocols # REST Source: https://docs.go-mizu.dev/contract/rest Learn how to expose your Contract service as a RESTful HTTP API ## What is the REST Transport? The REST transport automatically creates standard HTTP endpoints from your service methods. It uses naming conventions to determine HTTP methods (GET, POST, PUT, DELETE) and paths, so you don't need to write routing code. REST is the most familiar protocol for web APIs. It's what you use when calling APIs with curl, fetch in JavaScript, or any HTTP client. ## Quick Start Mount your service with the REST transport: ```go theme={null} 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" ) // Your interface is defined in the todo package as todo.API: // 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) // Delete(ctx context.Context, in *DeleteInput) error // } // Create your service implementation impl := todo.NewService() // Register your service svc := contract.Register[todo.API](impl, contract.WithDefaultResource("todos"), ) // Create mizu app app := mizu.New() // Mount REST endpoints rest.Mount(app.Router, svc) // Start server app.Listen(":8080") ``` Your service now has these endpoints: | Method | Path | Handler | | ------ | ------------- | ------- | | POST | `/todos` | Create | | GET | `/todos` | List | | GET | `/todos/{id}` | Get | | DELETE | `/todos/{id}` | Delete | ## Method Name to HTTP Mapping Contract automatically maps method names to HTTP verbs: | Method Name Starts With | HTTP Method | Path Pattern | | --------------------------------- | ----------- | ---------------------- | | `Create`, `Add`, `New` | POST | `/{resource}` | | `List`, `All`, `Search`, `Find*s` | GET | `/{resource}` | | `Get`, `Find`, `Fetch`, `Read` | GET | `/{resource}/{id}` | | `Update`, `Edit`, `Modify`, `Set` | PUT | `/{resource}/{id}` | | `Delete`, `Remove` | DELETE | `/{resource}/{id}` | | `Patch` | PATCH | `/{resource}/{id}` | | Other names | POST | `/{resource}/{action}` | ### Examples ```go theme={null} package product // API defines the contract for product operations type API interface { // POST /products Create(ctx context.Context, in *CreateInput) (*Product, error) // GET /products List(ctx context.Context) (*ListOutput, error) // GET /products (with query params) Search(ctx context.Context, in *SearchInput) (*ListOutput, error) // GET /products/{id} Get(ctx context.Context, in *GetInput) (*Product, error) // PUT /products/{id} Update(ctx context.Context, in *UpdateInput) (*Product, error) // DELETE /products/{id} Delete(ctx context.Context, in *DeleteInput) error // PATCH /products/{id} Patch(ctx context.Context, in *PatchInput) (*Product, error) // POST /products/archive (custom action) Archive(ctx context.Context, in *ArchiveInput) error // POST /products/publish (custom action) Publish(ctx context.Context, in *PublishInput) (*Product, error) } ``` ## Mount Options ### Mount Mount all routes directly on the router: ```go theme={null} rest.Mount(app.Router, svc) ``` ### MountAt Mount routes under a prefix: ```go theme={null} rest.MountAt(app.Router, "/api/v1", svc) // Now endpoints are: // POST /api/v1/todos // GET /api/v1/todos // etc. ``` ### Getting Routes Get route definitions without mounting: ```go theme={null} routes, err := rest.Routes(svc) if err != nil { log.Fatal(err) } for _, route := range routes { fmt.Printf("%s %s -> %s.%s\n", route.Method, route.Path, route.Resource, route.Handler) } ``` ## Making Requests ### Create (POST) ```bash theme={null} curl -X POST http://localhost:8080/todos \ -H "Content-Type: application/json" \ -d '{"title": "Buy groceries"}' ``` Response: ```json theme={null} { "id": "todo_1", "title": "Buy groceries", "completed": false } ``` ### List (GET) ```bash theme={null} curl http://localhost:8080/todos ``` Response: ```json theme={null} { "items": [ {"id": "todo_1", "title": "Buy groceries", "completed": false}, {"id": "todo_2", "title": "Walk the dog", "completed": true} ], "count": 2 } ``` ### Get (GET with ID) ```bash theme={null} curl http://localhost:8080/todos/todo_1 ``` Response: ```json theme={null} { "id": "todo_1", "title": "Buy groceries", "completed": false } ``` ### Update (PUT) ```bash theme={null} curl -X PUT http://localhost:8080/todos/todo_1 \ -H "Content-Type: application/json" \ -d '{"id": "todo_1", "title": "Buy groceries", "completed": true}' ``` Response: ```json theme={null} { "id": "todo_1", "title": "Buy groceries", "completed": true } ``` ### Delete (DELETE) ```bash theme={null} curl -X DELETE http://localhost:8080/todos/todo_1 ``` Response: HTTP 204 No Content (empty body) ## Path Parameters For methods that need an ID from the URL path, use the `path` tag: ```go theme={null} type GetInput struct { ID string `json:"id" path:"id"` // Extracted from {id} in the path } type UpdateInput struct { ID string `json:"id" path:"id"` Title string `json:"title"` Completed bool `json:"completed"` } ``` ### Multiple Path Parameters For nested resources: ```go theme={null} // Path: /users/{userId}/posts/{id} type GetPostInput struct { UserID string `json:"userId" path:"userId"` PostID string `json:"postId" path:"id"` } ``` Register with custom HTTP binding: ```go theme={null} svc := contract.Register[PostAPI](impl, contract.WithMethodHTTP("GetPost", "GET", "/users/{userId}/posts/{id}"), ) ``` ## Query Parameters By default, input comes from the request body. For GET requests, you may want query parameters: ```go theme={null} type ListInput struct { Limit int `json:"limit" query:"limit"` Offset int `json:"offset" query:"offset"` Status string `json:"status" query:"status"` } ``` Request: ```bash theme={null} curl "http://localhost:8080/todos?limit=10&offset=0&status=completed" ``` ## Custom HTTP Bindings Override the automatic HTTP mapping with `WithMethodHTTP`: ```go theme={null} svc := contract.Register[TodoAPI](impl, contract.WithDefaultResource("todos"), // Custom path for archive action contract.WithMethodHTTP("Archive", "POST", "/todos/{id}/archive"), // API versioning contract.WithMethodHTTP("Create", "POST", "/v2/todos"), ) ``` Or set multiple bindings at once: ```go theme={null} svc := contract.Register[TodoAPI](impl, contract.WithHTTP(map[string]contract.HTTPBinding{ "Create": {Method: "POST", Path: "/v2/todos"}, "Get": {Method: "GET", Path: "/v2/todos/{id}"}, "List": {Method: "GET", Path: "/v2/todos"}, }), ) ``` ## Complete Example This example shows a complete REST service using the recommended package-based organization: ```go theme={null} // 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 ListOutput struct { Items []*Todo `json:"items"` Count int `json:"count"` } ``` ```go theme={null} // 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) Update(ctx context.Context, in *UpdateInput) (*Todo, error) Delete(ctx context.Context, in *DeleteInput) error } ``` ```go theme={null} // 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("todo_%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, Count: len(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 } func (s *Service) Update(ctx context.Context, in *UpdateInput) (*Todo, error) { s.mu.Lock() defer s.mu.Unlock() todo, ok := s.todos[in.ID] if !ok { return nil, contract.ErrNotFound("todo not found") } todo.Title = in.Title todo.Completed = in.Completed return todo, nil } func (s *Service) Delete(ctx context.Context, in *DeleteInput) error { s.mu.Lock() defer s.mu.Unlock() if _, ok := s.todos[in.ID]; !ok { return contract.ErrNotFound("todo not found") } delete(s.todos, in.ID) return nil } ``` ```go theme={null} // 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" "yourapp/todo" ) func main() { impl := todo.NewService() svc := contract.Register[todo.API](impl, contract.WithName("Todo"), contract.WithDefaultResource("todos"), ) app := mizu.New() // Mount REST rest.Mount(app.Router, svc) // Print routes fmt.Println("REST Endpoints:") fmt.Println(" POST /todos - Create a todo") fmt.Println(" GET /todos - List all todos") fmt.Println(" GET /todos/{id} - Get a todo") fmt.Println(" PUT /todos/{id} - Update a todo") fmt.Println(" DELETE /todos/{id} - Delete a todo") app.Listen(":8080") } ``` ## Error Responses Use Contract errors for proper HTTP status codes: ```go theme={null} // HTTP 404 return nil, contract.ErrNotFound("todo not found") // HTTP 400 return nil, contract.ErrInvalidArgument("title is required") // HTTP 401 return nil, contract.ErrUnauthenticated("please log in") // HTTP 403 return nil, contract.ErrPermissionDenied("admin access required") // HTTP 500 return nil, contract.ErrInternal("database error") ``` Error response format: ```json theme={null} { "error": { "code": "NOT_FOUND", "message": "todo not found" } } ``` ## Multiple Services Mount multiple services on the same router: ```go theme={null} todoSvc := contract.Register[TodoAPI](todoImpl, contract.WithDefaultResource("todos")) userSvc := contract.Register[UserAPI](userImpl, contract.WithDefaultResource("users")) app := mizu.New() rest.Mount(app.Router, todoSvc) // /todos/* rest.Mount(app.Router, userSvc) // /users/* app.Listen(":8080") ``` ## Using with Middleware Apply middleware to your routes: ```go theme={null} app := mizu.New() // Global middleware app.Use(loggingMiddleware) app.Use(corsMiddleware) // Auth middleware for specific routes app.Use(authMiddleware, "/todos/*") // Mount REST rest.Mount(app.Router, svc) app.Listen(":8080") ``` ## OpenAPI Documentation Generate OpenAPI spec from your service: ```go theme={null} spec, err := rest.OpenAPI(svc.Descriptor()) if err != nil { log.Fatal(err) } // Serve the spec app.Get("/openapi.json", func(c *mizu.Ctx) error { return c.JSON(200, spec) }) ``` See the [OpenAPI documentation](/contract/openapi) for more details. ## Common Questions ### How do I change the path for a method? Use `WithMethodHTTP`: ```go theme={null} contract.WithMethodHTTP("Archive", "POST", "/todos/{id}/archive") ``` ### How do I add query parameters? Use the `query` tag on input struct fields: ```go theme={null} type ListInput struct { Page int `json:"page" query:"page"` } ``` ### Can I use different content types? By default, REST uses `application/json`. For other content types, use standard mizu handlers instead of Contract. ### How do I handle file uploads? File uploads aren't currently supported through Contract. Use standard mizu file handling for upload endpoints. ### How do I add authentication? Use mizu middleware to add authentication before requests reach your service methods. See [Mizu Middleware](/middlewares) documentation. ## What's Next? * **[JSON-RPC Transport](/contract/jsonrpc)** - For batching and RPC-style APIs * **[MCP Transport](/contract/mcp)** - Connect your API to AI assistants * **[OpenAPI](/contract/openapi)** - Generate API documentation * **[Error Handling](/contract/errors)** - Proper error responses # Go Source: https://docs.go-mizu.dev/contract/sdk-go Generate zero-dependency, type-safe Go client libraries from your contract definitions 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 ```yaml theme={null} # 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 ```bash theme={null} mizu contract gen api.yaml --client --lang go --output ./sdk/todoclient --package todoclient ``` ### Step 3: Use the Generated Client ```go theme={null} 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: ```go theme={null} // 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 ```go theme={null} // 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 | Option | Description | | ---------------------------------------- | ----------------------------------- | | `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 Type | Go Type | | ----------------- | ----------------- | | `string` | `string` | | `bool` | `bool` | | `int`, `int32` | `int32` | | `int64` | `int64` | | `int8`, `int16` | `int8`, `int16` | | `uint`, `uint32` | `uint32` | | `uint64` | `uint64` | | `float32` | `float32` | | `float64` | `float64` | | `time.Time` | `time.Time` | | `json.RawMessage` | `json.RawMessage` | | `any` | `any` | | `[]T` | `[]T` | | `map[string]T` | `map[string]T` | ### Struct Types Contract struct types generate Go structs with JSON tags: ```go theme={null} // 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 Definition | Go Type | JSON Tag | | -------------------------- | ------- | ----------------------- | | Required field | `T` | `json:"name"` | | `optional: true` | `*T` | `json:"name,omitempty"` | | `nullable: true` | `*T` | `json:"name"` | | Both optional and nullable | `*T` | `json:"name,omitempty"` | Example: ```go theme={null} // 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: ```go theme={null} // Contract: enum: [pending, active, completed] // Generated: // Status is one of: pending, active, completed Status string `json:"status"` ``` ### Slice and Map Types ```go theme={null} // 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: ```go theme={null} // 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: ```go theme={null} // 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: ```go theme={null} type Client struct { Todos *TodosResource Users *UsersResource // ... } ``` Access resources through the client: ```go theme={null} client.Todos.Create(ctx, input) client.Users.Get(ctx, &GetInput{ID: "user-123"}) ``` ### Method Signatures Generated method signatures follow these patterns: | Contract Method | Generated Signature | | ----------------------- | -------------------------------------- | | No input, no output | `Method(ctx) error` | | No input, with output | `Method(ctx) (*Output, error)` | | With input, no output | `Method(ctx, *Input) error` | | With input, with output | `Method(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 ```go theme={null} func (r *ResponsesResource) Stream(ctx context.Context, in *StreamRequest) *EventStream[ResponseEvent] ``` ### EventStream API ```go theme={null} 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 ```go theme={null} 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: ```go theme={null} 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: ```go theme={null} 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 ```go theme={null} 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 ```go theme={null} // 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: ```go theme={null} // 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 ```go theme={null} 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 ```go theme={null} // 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 ```go theme={null} // 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 * [Overview](/contract/sdk-overview) - Introduction to SDK generation * [Python](/contract/sdk-python) - Generate Python clients * [TypeScript](/contract/sdk-typescript) - Generate TypeScript clients * [Types](/contract/types) - Contract type system * [REST Transport](/contract/rest) - Serve your API over REST # Overview Source: https://docs.go-mizu.dev/contract/sdk-overview Generate native, type-safe client SDKs in Go, Python, and TypeScript from your contract definitions Mizu Contract includes built-in SDK generators that create native, fully-typed client libraries from your contract definitions. Generate idiomatic code for each language with resource-based API access and first-class streaming support. ## What is SDK Generation? SDK generation creates client libraries from your API contract. Instead of writing HTTP client code by hand for each language, you define your API once and generate clients automatically. ``` Contract Definition (api.yaml) │ ▼ ┌──────────────────┐ │ mizu contract gen│ │ (Go/Py/TS) │ └──────────────────┘ │ ▼ Native Client Library (Types + Client + Streaming) ``` ## Quick Start Generate SDK clients using the CLI: ```bash theme={null} # Generate a Go SDK client mizu contract gen api.yaml --client --lang go --output ./sdk/go # Generate a Python SDK client mizu contract gen api.yaml --client --lang python --output ./sdk/python # Generate a TypeScript SDK client mizu contract gen api.yaml --client --lang typescript --output ./sdk/typescript # Generate all languages at once mizu contract gen api.yaml --client --lang all --output ./sdk ``` ## Why Use Native SDK Generators? ### Benefits Over Manual Client Code | Aspect | Manual Code | Generated SDK | | ---------------- | ------------------------ | --------------------- | | **Type Safety** | Error-prone, manual sync | Always matches server | | **Maintenance** | Update everywhere | Regenerate once | | **Consistency** | Varies by author | Uniform patterns | | **Time to Ship** | Hours per language | Seconds | ### Benefits Over Generic OpenAPI-Based Generation | Feature | Generic Generators | Mizu SDK Generators | | ---------------- | ------------------ | ------------------------ | | **Code Style** | Generic, verbose | Idiomatic, minimal | | **Dependencies** | Often many | Minimal (stdlib-focused) | | **API Feel** | HTTP-centric | Resource-oriented | | **Streaming** | Limited support | First-class SSE | | **Size** | Large output | Compact, focused | **When to use OpenAPI generators?** OpenAPI generators support 40+ languages and have mature tooling. Use them when you need languages Mizu doesn't support natively, or when you need OpenAPI compatibility with other tools. See [Client Generation](/contract/client-generation) for OpenAPI approaches. ## Available SDK Generators Mizu provides native SDK generators for three languages: Zero dependencies, stdlib only. Fluent resource-based API with EventStream for streaming. Modern Python with type hints. Sync and async clients using httpx. Runtime agnostic (Node/Bun/Deno). AsyncIterable streaming with full type inference. ## Quick Comparison | Feature | Go SDK | Python SDK | TypeScript SDK | | ---------------- | ----------------- | ---------------------- | ------------------ | | **Output** | Single `.go` file | uv/pip project | npm package | | **HTTP Client** | `net/http` | `httpx` | native `fetch` | | **Streaming** | `EventStream[T]` | Iterator/AsyncIterator | `AsyncIterable` | | **Dependencies** | None (stdlib) | httpx | None | | **Runtimes** | Go 1.18+ | Python 3.8+ | Node 18+/Bun/Deno | ## How It Works ### Step 1: Define Your Contract Create a contract definition in YAML: ```yaml theme={null} # api.yaml name: TodoAPI description: Todo list API defaults: base_url: http://localhost:8080 resources: - name: todos description: Manage todo items methods: - name: create description: Create a new todo input: CreateTodoInput output: Todo http: method: POST path: /todos - name: list description: List all todos output: TodoList http: method: GET path: /todos - name: get description: Get a todo by ID input: GetTodoInput output: Todo http: method: GET path: /todos/{id} - name: delete description: Delete a todo input: DeleteTodoInput 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: created_at type: time.Time optional: true - name: TodoList kind: slice elem: Todo - name: CreateTodoInput kind: struct fields: - name: title type: string - name: GetTodoInput kind: struct fields: - name: id type: string - name: DeleteTodoInput kind: struct fields: - name: id type: string ``` ### Step 2: Generate SDK Clients ```bash theme={null} # Generate Go SDK mizu contract gen api.yaml --client --lang go --output ./sdk/go --package todoclient # Generate Python SDK mizu contract gen api.yaml --client --lang python --output ./sdk/python --package todoclient --version 1.0.0 # Generate TypeScript SDK mizu contract gen api.yaml --client --lang typescript --output ./sdk/typescript --package todoclient --version 1.0.0 ``` ### Step 3: Use the Generated Clients ```go Go theme={null} package main import ( "context" "fmt" "todoclient" ) func main() { client := todoclient.NewClient("http://localhost:8080") ctx := context.Background() todo, err := client.Todos.Create(ctx, &todoclient.CreateTodoInput{ Title: "Learn Mizu SDKs", }) if err != nil { panic(err) } fmt.Printf("Created: %s\n", todo.ID) } ``` ```python Python theme={null} from todoclient import Client client = Client(base_url="http://localhost:8080") todo = client.todos.create(title="Learn Mizu SDKs") print(f"Created: {todo.id}") ``` ```typescript TypeScript theme={null} import { Client } from 'todoclient'; const client = new Client({ baseURL: 'http://localhost:8080' }); const todo = await client.todos.create({ title: 'Learn Mizu SDKs' }); console.log(`Created: ${todo.id}`); ``` ## CLI Reference ### Command Syntax ```bash theme={null} mizu contract gen [flags] ``` ### Flags | Flag | Short | Description | Default | | ----------- | ----- | -------------------------------------------------------------------------- | ------------------------- | | `--lang` | | Target language: `go`, `golang`, `python`, `py`, `typescript`, `ts`, `all` | `typescript` | | `--output` | `-o` | Output directory | Same as input file | | `--package` | | Package/module name | Derived from service name | | `--version` | | Package version (Python/TypeScript) | `0.0.0` | | `--client` | `-c` | Generate full SDK client (not just types) | `false` | ### Examples ```bash theme={null} # Types only (lightweight, no HTTP client) mizu contract gen api.yaml --lang go --package api # Full SDK client for Go mizu contract gen api.yaml --client --lang go --output ./sdk/go # Full SDK client for Python with version mizu contract gen api.yaml --client --lang python --output ./sdk/python --version 1.0.0 # Full SDK client for TypeScript mizu contract gen api.yaml --client --lang typescript --output ./sdk/typescript # All languages at once mizu contract gen api.yaml --client --lang all --output ./sdk ``` ## Choosing the Right Approach Use this decision matrix to choose between SDK generation and OpenAPI: | If you need... | Use | | ---------------------------------- | ---------------------------------------------------- | | Go, Python, or TypeScript client | **Native SDK generators** | | Maximum type safety | **Native SDK generators** | | Minimal dependencies | **Native SDK generators** | | Streaming (SSE) support | **Native SDK generators** | | Other languages (Java, Ruby, etc.) | OpenAPI generators | | OpenAPI ecosystem compatibility | OpenAPI generators | | Both approaches | Both! Generate native SDKs and serve `/openapi.json` | ## CI/CD Integration Regenerate SDKs on contract changes: ```yaml theme={null} # .github/workflows/sdk.yml name: Generate SDKs on: push: paths: - 'api.yaml' jobs: generate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install Mizu CLI run: go install github.com/go-mizu/mizu/cmd/cli@latest - name: Generate SDKs run: | mizu contract gen api.yaml --client --lang go --output ./sdk/go mizu contract gen api.yaml --client --lang python --output ./sdk/python mizu contract gen api.yaml --client --lang typescript --output ./sdk/typescript - name: Commit SDKs run: | git add sdk/ git commit -m "chore: regenerate SDKs" || true git push ``` ## What's Next? Complete guide to generating and using Go SDKs Complete guide to generating and using Python SDKs Complete guide to generating and using TypeScript SDKs Alternative approach using OpenAPI specification ## See Also * [Service Definition](/contract/service) - How to define your API interface * [Types](/contract/types) - Type system and mappings * [Client Generation](/contract/client-generation) - OpenAPI-based approach * [REST Transport](/contract/rest) - Exposing your service as REST API # Python Source: https://docs.go-mizu.dev/contract/sdk-python Generate modern, type-safe Python client libraries with sync and async support from your contract definitions The Python SDK generator creates modern Python client libraries from your contract definitions. The generated code features full type hints, dataclass models, and both synchronous and asynchronous clients powered by httpx. ## Key Features * **Modern Python**: Python 3.8+ with full type hints * **Sync and Async**: Both `Client` (sync) and `AsyncClient` (async) clients * **httpx Powered**: Industry-standard HTTP client with automatic retries * **Dataclass Models**: Clean, typed data structures * **uv-Ready**: Includes `pyproject.toml` for modern Python packaging * **SSE Streaming**: First-class Server-Sent Events support ## Quick Start ### Step 1: Define Your Contract ```yaml theme={null} # 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 ```bash theme={null} mizu contract gen api.yaml --client --lang python --output ./sdk/python --package todoclient --version 1.0.0 ``` ### Step 3: Install and Use ```bash theme={null} # Install the generated SDK cd sdk/python uv pip install -e . # Or with pip pip install -e . ``` ```python theme={null} from todoclient import Client # Create a client client = Client(base_url="http://localhost:8080") # Create a todo todo = client.todos.create(title="Learn Mizu Python SDK") print(f"Created: {todo.id}") # List all todos result = client.todos.list() print(f"Total: {result.total} todos") # Get a specific todo todo = client.todos.get(id=todo.id) print(f"Got: {todo.id} - {todo.title}") # Delete client.todos.delete(id=todo.id) print("Deleted successfully") ``` ## Generated Code Structure ``` sdk/python/ ├── pyproject.toml └── src/ └── todoclient/ ├── __init__.py ├── _client.py ├── _types.py ├── _resource.py └── _streaming.py ``` ### pyproject.toml ```toml theme={null} [project] name = "todoclient" version = "1.0.0" requires-python = ">=3.8" dependencies = ["httpx>=0.24.0"] [build-system] requires = ["hatchling"] build-backend = "hatchling.build" ``` ### **init**.py ```python theme={null} from ._client import Client, AsyncClient __all__ = ["Client", "AsyncClient"] ``` ## Client Configuration ### Creating a Client ```python theme={null} from todoclient import Client # Basic client client = Client(base_url="http://localhost:8080") # With authentication client = Client( api_key="your-api-key", base_url="http://localhost:8080", ) # With all options client = Client( api_key="your-api-key", base_url="http://localhost:8080", timeout=30.0, max_retries=3, default_headers={"X-Custom-Header": "value"}, ) ``` ### Configuration Options | Option | Type | Default | Description | | ----------------- | ------------------------ | ------------------- | -------------------------- | | `api_key` | `str \| None` | `None` | Authentication token | | `base_url` | `str \| None` | Service default | API base URL | | `timeout` | `float \| None` | `None` (no timeout) | Request timeout in seconds | | `max_retries` | `int` | `2` | Maximum retry attempts | | `default_headers` | `Dict[str, str] \| None` | `None` | Headers for all requests | ### Modifying Client Options Create a new client with modified options: ```python theme={null} # Original client client = Client(api_key="key-1", base_url="http://localhost:8080") # New client with different API key other_client = client.with_options(api_key="key-2") # New client with additional headers debug_client = client.with_options( default_headers={"X-Debug": "true"} ) ``` ### Closing the Client Always close the client when done: ```python theme={null} client = Client(base_url="http://localhost:8080") try: # Use client... pass finally: client.close() ``` Or use a context manager pattern: ```python theme={null} from contextlib import closing with closing(Client(base_url="http://localhost:8080")) as client: todo = client.todos.create(title="Task") ``` ## Async Client The SDK includes a fully async client for use with `asyncio`: ```python theme={null} import asyncio from todoclient import AsyncClient async def main(): client = AsyncClient(base_url="http://localhost:8080") try: # Create a todo todo = await client.todos.create(title="Async task") print(f"Created: {todo.id}") # List todos result = await client.todos.list() for t in result.items: print(f"- {t.title}") finally: await client.close() asyncio.run(main()) ``` ### Async Context Manager ```python theme={null} async def main(): client = AsyncClient(base_url="http://localhost:8080") try: todos = await client.todos.list() finally: await client.aclose() # or await client.close() ``` ## Type System ### Type Mapping Reference | Contract Type | Python Type | | ------------------------ | -------------- | | `string` | `str` | | `bool` | `bool` | | `int`, `int8`-`int64` | `int` | | `uint`, `uint8`-`uint64` | `int` | | `float32`, `float64` | `float` | | `time.Time` | `datetime` | | `json.RawMessage` | `object` | | `any` | `object` | | `[]T` | `List[T]` | | `map[string]T` | `Dict[str, T]` | ### Dataclass Types Contract struct types generate Python dataclasses: ```python theme={null} from dataclasses import dataclass from typing import Optional from datetime import datetime @dataclass class Todo: id: str title: str done: bool created_at: datetime ``` ### Optional and Nullable Fields | Contract Definition | Python Type | | -------------------------- | ------------- | | Required field | `T` | | `optional: true` | `Optional[T]` | | `nullable: true` | `Optional[T]` | | Both optional and nullable | `Optional[T]` | Example: ```python theme={null} @dataclass class UpdateInput: title: str # required done: Optional[bool] = None # optional ``` ### Enum Fields Enum fields use literal type hints in the docstring: ```python theme={null} @dataclass class Task: # status: One of "pending", "active", "completed" status: str ``` ### List and Dict Types ```python theme={null} from typing import List, Dict # Contract: kind: slice, elem: Todo TodoList = List[Todo] # Contract: kind: map, elem: string Metadata = Dict[str, str] ``` ## Resources and Methods ### Resource Pattern Each contract resource becomes a property on the client: ```python theme={null} client.todos # TodosResource client.users # UsersResource ``` ### Method Signatures Methods use Python naming conventions (snake\_case): | Contract Method | Python Signature | | --------------- | -------------------------- | | `Create` | `create(**kwargs) -> Todo` | | `List` | `list() -> ListOutput` | | `Get` | `get(id: str) -> Todo` | | `Delete` | `delete(id: str) -> None` | ### Calling Methods Methods accept keyword arguments matching the input type fields: ```python theme={null} # If input type is CreateInput { title: string } todo = client.todos.create(title="My task") # If input type is UpdateInput { title: string, done: bool (optional) } todo = client.todos.update(id="123", title="Updated", done=True) ``` ## Streaming (SSE) For methods with streaming support, the SDK provides iterator-based consumption: ### Sync Streaming ```python theme={null} # Stream events for event in client.responses.stream(model="gpt-4", input="Hello"): if event.type == "text_delta": print(event.text, end="", flush=True) elif event.type == "completed": print("\n--- Done ---") ``` ### Async Streaming ```python theme={null} async for event in client.responses.stream(model="gpt-4", input="Hello"): if event.type == "text_delta": print(event.text, end="", flush=True) ``` ### Collecting All Events ```python theme={null} # Collect all events into a list events = list(client.responses.stream(model="gpt-4", input="Hello")) # Or async events = [event async for event in client.responses.stream(...)] ``` ## Error Handling ### Error Types The SDK defines three error types: ```python theme={null} class SDKError(Exception): """Base exception for all SDK errors.""" pass class APIConnectionError(SDKError): """Raised when unable to connect to the API.""" pass class APIStatusError(SDKError): """Raised when the API returns an error status code.""" status_code: int body: Any ``` ### Handling Errors ```python theme={null} from todoclient import Client from todoclient._client import APIStatusError, APIConnectionError client = Client(base_url="http://localhost:8080") try: todo = client.todos.get(id="nonexistent") except APIStatusError as e: if e.status_code == 404: print("Todo not found") elif e.status_code == 401: print("Unauthorized") elif e.status_code == 400: print(f"Bad request: {e}") else: print(f"API error {e.status_code}: {e}") except APIConnectionError as e: print(f"Connection failed: {e}") ``` ### Automatic Retries The client automatically retries failed requests (except for 4xx errors): ```python theme={null} # Configure max retries client = Client( base_url="http://localhost:8080", max_retries=5, # Retry up to 5 times ) ``` Retries are attempted for: * Network errors * 5xx server errors * Timeout errors ## Advanced Usage ### Custom Headers ```python theme={null} # Default headers for all requests client = Client( base_url="http://localhost:8080", default_headers={ "X-Request-ID": "abc-123", "X-API-Version": "2024-01", }, ) ``` ### Authentication Modes The SDK supports different authentication modes based on your service configuration: ```python theme={null} # Bearer token (default) client = Client(api_key="your-token") # Sends: Authorization: Bearer your-token # Basic auth (if service configured with auth: basic) client = Client(api_key="base64-encoded-credentials") # Sends: Authorization: Basic base64-encoded-credentials # No auth (if service configured with auth: none) client = Client() # Sends no Authorization header ``` ### Timeout Configuration ```python theme={null} # Set timeout in seconds client = Client( base_url="http://localhost:8080", timeout=30.0, # 30 second timeout ) # No timeout (wait indefinitely) client = Client( base_url="http://localhost:8080", timeout=None, ) ``` ### Using with Existing httpx Client The generated SDK uses httpx internally. For advanced use cases, you can access the underlying client: ```python theme={null} # The SDK creates its own httpx client client = Client(base_url="http://localhost:8080") # client._http is the httpx.Client instance ``` ## Complete Example ### Server ```go theme={null} // 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 ```python theme={null} #!/usr/bin/env python3 """Example usage of the generated Todo client.""" from todoclient import Client def main(): # Create client client = Client(base_url="http://localhost:8080") try: # Create a todo todo = client.todos.create(title="Buy groceries") print(f"Created: {todo.id}") # List all todos result = client.todos.list() print(f"\nAll todos ({result.total} total):") for t in result.items: status = "x" if t.done else " " print(f" [{status}] {t.id}: {t.title}") # Get a specific todo fetched = client.todos.get(id=todo.id) print(f"\nFetched: {fetched.title}") # Delete the todo client.todos.delete(id=todo.id) print(f"\nDeleted: {todo.id}") finally: client.close() if __name__ == "__main__": main() ``` ### Async Example ```python theme={null} #!/usr/bin/env python3 """Async example usage of the generated Todo client.""" import asyncio from todoclient import AsyncClient async def main(): client = AsyncClient(base_url="http://localhost:8080") try: # Create multiple todos concurrently tasks = [ client.todos.create(title="Task 1"), client.todos.create(title="Task 2"), client.todos.create(title="Task 3"), ] todos = await asyncio.gather(*tasks) for todo in todos: print(f"Created: {todo.id} - {todo.title}") # List all result = await client.todos.list() print(f"\nTotal: {result.total} todos") finally: await client.close() if __name__ == "__main__": asyncio.run(main()) ``` ## Installation and Distribution ### Installing Locally ```bash theme={null} # Using uv (recommended) cd sdk/python uv pip install -e . # Using pip cd sdk/python pip install -e . ``` ### Publishing to PyPI ```bash theme={null} cd sdk/python # Build the package uv build # or: python -m build # Upload to PyPI uv publish # or: twine upload dist/* ``` ### Installing from Git ```bash theme={null} pip install git+https://github.com/yourorg/yourrepo.git#subdirectory=sdk/python ``` ## See Also * [Overview](/contract/sdk-overview) - Introduction to SDK generation * [Go](/contract/sdk-go) - Generate Go clients * [TypeScript](/contract/sdk-typescript) - Generate TypeScript clients * [Types](/contract/types) - Contract type system * [REST Transport](/contract/rest) - Serve your API over REST # TypeScript Source: https://docs.go-mizu.dev/contract/sdk-typescript Generate runtime-agnostic, fully-typed TypeScript client libraries from your contract definitions The TypeScript SDK generator creates modern TypeScript client libraries from your contract definitions. The generated code works across Node.js, Bun, and Deno without modifications, uses native `fetch`, and provides full type inference with a resource-based developer experience. ## Key Features * **Runtime Agnostic**: Works on Node.js 18+, Bun, and Deno * **Zero Dependencies**: Uses native `fetch` API only * **TypeScript-First**: Full type inference and autocompletion * **ES Modules**: Modern JavaScript with `async/await` * **SSE Streaming**: First-class `AsyncIterable` streaming * **npm-Ready**: Includes `package.json` and `tsconfig.json` ## Quick Start ### Step 1: Define Your Contract ```yaml theme={null} # 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 ```bash theme={null} mizu contract gen api.yaml --client --lang typescript --output ./sdk/typescript --package todoclient --version 1.0.0 ``` ### Step 3: Install and Use ```bash theme={null} # Install dependencies and build cd sdk/typescript npm install npm run build ``` ```typescript theme={null} import { Client } from './dist/index.js'; // Create a client const client = new Client({ baseURL: 'http://localhost:8080' }); // Create a todo const todo = await client.todos.create({ title: 'Learn Mizu TypeScript SDK' }); console.log(`Created: ${todo.id}`); // List all todos const result = await client.todos.list(); console.log(`Total: ${result.total} todos`); // Get a specific todo const fetched = await client.todos.get({ id: todo.id }); console.log(`Got: ${fetched.id} - ${fetched.title}`); // Delete await client.todos.delete({ id: todo.id }); console.log('Deleted successfully'); ``` ## Generated Code Structure ``` sdk/typescript/ ├── package.json ├── tsconfig.json └── src/ ├── index.ts ├── _client.ts ├── _types.ts ├── _resources.ts └── _streaming.ts ``` ### package.json ```json theme={null} { "name": "todoclient", "version": "1.0.0", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { ".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" } }, "files": ["dist"], "scripts": { "build": "tsc", "typecheck": "tsc --noEmit" }, "devDependencies": { "typescript": "^5.0.0" } } ``` ### tsconfig.json ```json theme={null} { "compilerOptions": { "target": "ES2020", "module": "ESNext", "moduleResolution": "bundler", "lib": ["ES2020", "DOM"], "strict": true, "esModuleInterop": true, "skipLibCheck": true, "declaration": true, "outDir": "./dist", "rootDir": "./src" }, "include": ["src/**/*"] } ``` ## Client Configuration ### Creating a Client ```typescript theme={null} import { Client } from 'todoclient'; // Basic client const client = new Client({ baseURL: 'http://localhost:8080' }); // With authentication const client = new Client({ apiKey: 'your-api-key', baseURL: 'http://localhost:8080', }); // With all options const client = new Client({ apiKey: 'your-api-key', baseURL: 'http://localhost:8080', timeout: 30000, // 30 seconds maxRetries: 3, defaultHeaders: { 'X-Custom-Header': 'value', }, }); ``` ### Configuration Options | Option | Type | Default | Description | | ---------------- | ------------------------- | --------------- | ------------------------------- | | `apiKey` | `string?` | `undefined` | Authentication token | | `baseURL` | `string?` | Service default | API base URL | | `timeout` | `number?` | `60000` | Request timeout in milliseconds | | `maxRetries` | `number?` | `2` | Maximum retry attempts | | `defaultHeaders` | `Record?` | `{}` | Headers for all requests | ### Accessing Config ```typescript theme={null} const client = new Client({ baseURL: 'http://localhost:8080' }); // Access current configuration console.log(client.config.baseURL); // "http://localhost:8080" console.log(client.config.timeout); // 60000 ``` ## Type System ### Type Mapping Reference | Contract Type | TypeScript Type | | ------------------------ | ------------------- | | `string` | `string` | | `bool` | `boolean` | | `int`, `int8`-`int64` | `number` | | `uint`, `uint8`-`uint64` | `number` | | `float32`, `float64` | `number` | | `time.Time` | `string` (ISO 8601) | | `json.RawMessage` | `unknown` | | `any` | `unknown` | | `[]T` | `T[]` | | `map[string]T` | `Record` | ### Interface Types Contract struct types generate TypeScript interfaces: ```typescript theme={null} export interface Todo { id: string; title: string; done: boolean; createdAt: string; // ISO 8601 date string } ``` ### Optional and Nullable Fields | Contract Definition | TypeScript Type | | -------------------------- | ------------------------ | | Required field | `T` | | `optional: true` | `T?` (optional property) | | `nullable: true` | `T \| null` | | Both optional and nullable | `(T \| null)?` | Example: ```typescript theme={null} export interface UpdateInput { title: string; // required done?: boolean; // optional description?: string | null; // optional and nullable } ``` ### Enum Fields Enum fields generate as union types: ```typescript theme={null} export interface Task { /** One of: "pending", "active", "completed" */ status: "pending" | "active" | "completed"; } ``` ### Const Fields Const fields generate as literal types: ```typescript theme={null} export interface TextContent { type: "text"; // always "text" content: string; } ``` ### Array and Record Types ```typescript theme={null} // Contract: kind: slice, elem: Todo export type TodoList = Todo[]; // Contract: kind: map, elem: string export type Metadata = Record; ``` ### Union Types (Discriminated) Union types generate as TypeScript union types: ```typescript theme={null} export type ContentPart = ContentPartText | ContentPartImage; // Type guards export function isContentPartText(v: ContentPart): v is ContentPartText { return v.type === "text"; } export function isContentPartImage(v: ContentPart): v is ContentPartImage { return v.type === "image"; } ``` Usage: ```typescript theme={null} function handleContent(part: ContentPart) { if (isContentPartText(part)) { console.log("Text:", part.content); } else if (isContentPartImage(part)) { console.log("Image:", part.url); } } // Or with type narrowing function handleContent(part: ContentPart) { switch (part.type) { case "text": console.log("Text:", part.content); break; case "image": console.log("Image:", part.url); break; } } ``` ## Resources and Methods ### Resource Pattern Each contract resource becomes a property on the client: ```typescript theme={null} client.todos // TodosResource client.users // UsersResource ``` ### Method Signatures Methods use camelCase naming: | Contract Method | TypeScript Signature | | --------------- | ---------------------------------------------- | | `Create` | `create(input: CreateInput): Promise` | | `List` | `list(): Promise` | | `Get` | `get(input: { id: string }): Promise` | | `Delete` | `delete(input: { id: string }): Promise` | ### Calling Methods ```typescript theme={null} // Create with input object const todo = await client.todos.create({ title: 'My task' }); // List (no input) const result = await client.todos.list(); // Get with ID const fetched = await client.todos.get({ id: 'todo-123' }); // Delete await client.todos.delete({ id: 'todo-123' }); ``` ## Streaming (SSE) For methods with streaming support, the SDK provides `AsyncIterable` streams: ### Basic Streaming ```typescript theme={null} const stream = client.responses.stream({ model: 'gpt-4', input: 'Hello!', }); for await (const event of stream) { if (event.type === 'text_delta') { process.stdout.write(event.text); } else if (event.type === 'completed') { console.log('\n--- Done ---'); } } ``` ### Stream with Cancellation ```typescript theme={null} const stream = client.responses.stream({ model: 'gpt-4', input: 'Write a long story', }); // Cancel after 5 seconds setTimeout(() => { stream.controller.abort(); }, 5000); try { for await (const event of stream) { console.log(event); } } catch (error) { if (error.name === 'AbortError') { console.log('Stream cancelled'); } else { throw error; } } ``` ### Collecting Stream Events ```typescript theme={null} // Collect all events into an array const events: ResponseEvent[] = []; for await (const event of client.responses.stream(input)) { events.push(event); } // Or with Array.fromAsync (Node.js 22+) const events = await Array.fromAsync(client.responses.stream(input)); ``` ### Stream Type The stream object implements `AsyncIterable` and has an `AbortController`: ```typescript theme={null} interface Stream extends AsyncIterable { readonly controller: AbortController; } ``` ## Error Handling ### Error Types The SDK defines three error classes: ```typescript theme={null} export class SDKError extends Error { name = "SDKError"; } export class APIConnectionError extends SDKError { name = "APIConnectionError"; } export class APIStatusError extends SDKError { name = "APIStatusError"; readonly status: number; readonly body: unknown; } ``` ### Handling Errors ```typescript theme={null} import { Client, APIStatusError, APIConnectionError } from 'todoclient'; const client = new Client({ baseURL: 'http://localhost:8080' }); try { const todo = await client.todos.get({ id: 'nonexistent' }); } catch (error) { if (error instanceof APIStatusError) { switch (error.status) { case 404: console.log('Todo not found'); break; case 401: console.log('Unauthorized'); break; case 400: console.log(`Bad request: ${error.message}`); break; default: console.log(`API error ${error.status}: ${error.message}`); } } else if (error instanceof APIConnectionError) { console.log(`Connection failed: ${error.message}`); } else { throw error; } } ``` ### Automatic Retries The client automatically retries failed requests: ```typescript theme={null} const client = new Client({ baseURL: 'http://localhost:8080', maxRetries: 5, // Retry up to 5 times }); ``` Retries are attempted for: * Network errors * Timeout errors Note: 4xx and 5xx errors are NOT retried (they throw immediately). ## Advanced Usage ### Custom Headers ```typescript theme={null} const client = new Client({ baseURL: 'http://localhost:8080', defaultHeaders: { 'X-Request-ID': 'abc-123', 'X-API-Version': '2024-01', }, }); ``` ### Authentication Modes The SDK supports different authentication modes: ```typescript theme={null} // Bearer token (default) const client = new Client({ apiKey: 'your-token' }); // Sends: Authorization: Bearer your-token // Basic auth (if service configured with auth: basic) const client = new Client({ apiKey: 'base64-credentials' }); // Sends: Authorization: Basic base64-credentials // No auth (if service configured with auth: none) const client = new Client({}); // Sends no Authorization header ``` ### Timeout Configuration ```typescript theme={null} // Set timeout in milliseconds const client = new Client({ baseURL: 'http://localhost:8080', timeout: 30000, // 30 seconds }); ``` ### Using with Different Runtimes The SDK works across all modern JavaScript runtimes: ```typescript Node.js theme={null} // Requires Node.js 18+ for native fetch import { Client } from 'todoclient'; const client = new Client({ baseURL: 'http://localhost:8080' }); const todos = await client.todos.list(); ``` ```typescript Bun theme={null} // Works out of the box with Bun import { Client } from 'todoclient'; const client = new Client({ baseURL: 'http://localhost:8080' }); const todos = await client.todos.list(); ``` ```typescript Deno theme={null} // Works with Deno import { Client } from 'npm:todoclient'; const client = new Client({ baseURL: 'http://localhost:8080' }); const todos = await client.todos.list(); ``` ## Complete Example ### Server ```go theme={null} // 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 ```typescript theme={null} // example.ts import { Client } from 'todoclient'; async function main() { const client = new Client({ baseURL: 'http://localhost:8080' }); // Create a todo const todo = await client.todos.create({ title: 'Buy groceries' }); console.log(`Created: ${todo.id}`); // List all todos const result = await client.todos.list(); console.log(`\nAll todos (${result.total} total):`); for (const t of result.items) { const status = t.done ? 'x' : ' '; console.log(` [${status}] ${t.id}: ${t.title}`); } // Get a specific todo const fetched = await client.todos.get({ id: todo.id }); console.log(`\nFetched: ${fetched.title}`); // Delete await client.todos.delete({ id: todo.id }); console.log(`\nDeleted: ${todo.id}`); } main().catch(console.error); ``` ### Streaming Example ```typescript theme={null} // streaming-example.ts import { Client } from 'chatclient'; async function main() { const client = new Client({ apiKey: process.env.API_KEY, baseURL: 'http://localhost:8080', }); console.log('Streaming response:\n'); const stream = client.chat.stream({ model: 'gpt-4', messages: [ { role: 'user', content: 'Write a haiku about programming' }, ], }); for await (const event of stream) { switch (event.type) { case 'content_delta': process.stdout.write(event.delta); break; case 'done': console.log('\n\n--- Stream complete ---'); break; } } } main().catch(console.error); ``` ## Installation and Distribution ### Installing Locally ```bash theme={null} cd sdk/typescript npm install npm run build # Link for local development npm link # In another project npm link todoclient ``` ### Publishing to npm ```bash theme={null} cd sdk/typescript # Build npm run build # Publish npm publish # Or with a specific tag npm publish --tag beta ``` ### Installing from Git ```bash theme={null} npm install github:yourorg/yourrepo#main/sdk/typescript ``` ### Using Without npm The generated code can be used directly in Deno or Bun without npm: ```typescript theme={null} // Deno - import from URL import { Client } from 'https://raw.githubusercontent.com/yourorg/yourrepo/main/sdk/typescript/src/index.ts'; // Or copy the src/ folder directly import { Client } from './sdk/index.ts'; ``` ## See Also * [Overview](/contract/sdk-overview) - Introduction to SDK generation * [Go](/contract/sdk-go) - Generate Go clients * [Python](/contract/sdk-python) - Generate Python clients * [Types](/contract/types) - Contract type system * [REST Transport](/contract/rest) - Serve your API over REST # Services Source: https://docs.go-mizu.dev/contract/service Learn how to design interfaces and implementations for Contract - the foundation of your API ## The Interface-First Approach In Contract, a **service** is defined by two parts: 1. **An interface** - Declares what operations your API supports (the "what") 2. **An implementation** - Contains the actual business logic (the "how") This separation is intentional and powerful. The interface is your API contract - it's the source of truth for what your service can do. The implementation is a detail that clients don't need to know about. We recommend organizing each service in its own Go package. This lets you use clean, simple names like `API` and `Service` - the package name provides the context: ```go theme={null} package todo import "context" // API defines the contract for todo operations. // When imported, this becomes "todo.API" which reads naturally. type API interface { Create(ctx context.Context, in *CreateInput) (*Todo, error) List(ctx context.Context) (*ListOutput, error) } // Service implements todo.API with actual business logic. // When imported, this becomes "todo.Service". type Service struct { db *sql.DB // Dependencies go here } func (s *Service) Create(ctx context.Context, in *CreateInput) (*Todo, error) { // Your business logic here } ``` ## Why Interface-First? Before diving into the details, let's understand why this approach is valuable. ### Compile-Time Safety If your implementation doesn't match the interface, Go's compiler tells you immediately - before your code even runs: ```go theme={null} package todo type API interface { Create(ctx context.Context, in *CreateInput) (*Todo, error) } type Service struct{} // This won't compile! // Error: cannot use &Service{} (type *Service) as type API // missing method Create // You can add a compile-time check explicitly: var _ API = (*Service)(nil) // Fails if Service doesn't implement API ``` The compiler catches mismatches before your code even runs. This is a huge advantage over dynamic approaches where you might only discover problems when users hit a bug in production. ### Clear Contracts The interface IS your API documentation. Anyone can read it and understand exactly what your service does, without digging through implementation details: ```go theme={null} package user // API defines all operations for user management. // Reading this interface tells you everything about what the service can do. type API interface { // Create registers a new user account. // Returns the created user with a generated ID. Create(ctx context.Context, in *CreateInput) (*User, error) // Get retrieves a user by their unique ID. // Returns NOT_FOUND error if the user doesn't exist. Get(ctx context.Context, in *GetInput) (*User, error) // Update modifies an existing user's profile. // Only fields provided in the input are changed. Update(ctx context.Context, in *UpdateInput) (*User, error) // Delete removes a user account permanently. // This operation cannot be undone. Delete(ctx context.Context, in *DeleteInput) error // List returns all users with pagination support. List(ctx context.Context, in *ListInput) (*ListOutput, error) } ``` ### Easy Testing Because the interface separates "what" from "how", you can easily create mock implementations for testing: ```go theme={null} package user_test import "yourapp/user" // MockService implements user.API for testing. // It stores users in memory instead of a real database. type MockService struct { users map[string]*user.User } func (m *MockService) Get(ctx context.Context, in *user.GetInput) (*user.User, error) { if u, ok := m.users[in.ID]; ok { return u, nil } return nil, errors.New("not found") } func (m *MockService) Create(ctx context.Context, in *user.CreateInput) (*user.User, error) { u := &user.User{ID: "test-123", Email: in.Email} m.users[u.ID] = u return u, nil } // ... implement other methods // Now use MockService in your tests! func TestSomethingThatNeedsUsers(t *testing.T) { mock := &MockService{users: make(map[string]*user.User)} // Use mock instead of real service } ``` ### Multiple Implementations You can have different implementations for different situations - all sharing the same interface: ```go theme={null} package storage // API defines storage operations. // The same interface can have multiple implementations. type API interface { Save(ctx context.Context, in *SaveInput) error Load(ctx context.Context, in *LoadInput) (*Data, error) } // FileService stores data on the local filesystem. // Good for development and single-server deployments. type FileService struct { basePath string } // S3Service stores data in Amazon S3. // Good for production with multiple servers. type S3Service struct { bucket string client *s3.Client } // MemoryService stores data in memory. // Perfect for tests - fast and isolated. type MemoryService struct { data map[string][]byte } // All three implement storage.API! // Your application can switch between them based on configuration. ``` ## Package Organization We recommend organizing each service in its own package with a consistent structure: ``` yourapp/ ├── main.go # Wires everything together ├── todo/ │ ├── api.go # Interface definition (todo.API) │ ├── service.go # Implementation (todo.Service) │ └── types.go # Input/output types ├── user/ │ ├── api.go # Interface definition (user.API) │ ├── service.go # Implementation (user.Service) │ └── types.go # Input/output types ``` **Why this structure?** 1. **Package-based naming**: `todo.API` is clearer than `TodoAPI`, and `user.Service` is clearer than `userService` 2. **Encapsulation**: Each package is self-contained with its own types 3. **Discoverability**: Finding the todo API? Look in the `todo` package 4. **Scalability**: Adding a new service? Create a new package ## Defining Your Interface ### Basic Structure Every interface method must follow specific patterns. The rules are straightforward: ```go theme={null} package myservice type API interface { // Every method MUST have ctx as the first parameter // This is required for timeouts, cancellation, and request-scoped values MethodName(ctx context.Context, ...) ... } ``` ### Method Signature Patterns Contract supports four method patterns. Let's explore each one with detailed explanations. #### Pattern 1: Input and Output The most common pattern - receive data, return data: ```go theme={null} func MethodName(ctx context.Context, in *InputType) (*OutputType, error) ``` **Breakdown:** * `ctx context.Context` - Required first parameter for request context * `in *InputType` - Pointer to a struct containing the input data * `*OutputType` - Pointer to a struct containing the result * `error` - Always the last return value; nil means success **Example:** ```go theme={null} package order type API interface { // Create places a new order. // Input: order details (items, customer, shipping address) // Output: the created order with generated ID and totals Create(ctx context.Context, in *CreateInput) (*Order, error) // Get retrieves order details by ID. // Input: the order ID to look up // Output: full order information Get(ctx context.Context, in *GetInput) (*Order, error) // CalculateShipping determines shipping cost. // Input: destination and items // Output: available shipping options with prices CalculateShipping(ctx context.Context, in *ShippingInput) (*ShippingOutput, error) } ``` **When to use:** Most CRUD operations, calculations, queries - any method that receives data and returns data. #### Pattern 2: Output Only (No Input) Return data without receiving any input parameters: ```go theme={null} func MethodName(ctx context.Context) (*OutputType, error) ``` **Breakdown:** * No input parameter beyond context * Still returns output and error **Example:** ```go theme={null} package dashboard type API interface { // GetStats returns current system statistics. // No input needed - just returns current stats. GetStats(ctx context.Context) (*StatsOutput, error) // ListAll returns all items without any filters. // Useful for small datasets or admin interfaces. ListAll(ctx context.Context) (*ListOutput, error) // Health returns detailed health information. // More detailed than a simple ping. Health(ctx context.Context) (*HealthOutput, error) } ``` **When to use:** Listing all items, getting current status, dashboard data, health checks that return information. #### Pattern 3: Input Only (No Output) Accept input but return only success/failure: ```go theme={null} func MethodName(ctx context.Context, in *InputType) error ``` **Breakdown:** * Accepts input data * Only returns error (nil means success) * No meaningful data to return **Example:** ```go theme={null} package notification type API interface { // Send delivers a notification to a user. // We don't need to return anything - just success/failure. Send(ctx context.Context, in *SendInput) error // Delete removes a notification by ID. // Nothing to return after deletion. Delete(ctx context.Context, in *DeleteInput) error // MarkAllRead marks all notifications as read for a user. // Fire-and-forget operation. MarkAllRead(ctx context.Context, in *MarkReadInput) error } ``` **When to use:** Delete operations, fire-and-forget updates, operations where you only care about success/failure. #### Pattern 4: No Input or Output Just do something and report success/failure: ```go theme={null} func MethodName(ctx context.Context) error ``` **Breakdown:** * Only context as input * Only error as output * Simplest possible signature **Example:** ```go theme={null} package maintenance type API interface { // Ping checks if the service is responsive. // Returns nil if healthy, error if not. Ping(ctx context.Context) error // Cleanup runs maintenance tasks. // No parameters needed, no results to return. Cleanup(ctx context.Context) error // CheckDB verifies database connectivity. // Simple health check that returns error if DB is down. CheckDB(ctx context.Context) error } ``` **When to use:** Health checks, ping endpoints, administrative operations with no configuration. ## Defining Input and Output Types Types define the shape of data that flows through your API. Good type design makes your API clear and easy to use. ### Input Types Input types describe what clients send to your methods. They should only contain fields that clients actually need to provide: ```go theme={null} package user // CreateInput contains everything needed to create a new user. // Notice we don't include ID - that's generated by the server. type CreateInput struct { // Required fields - must be provided by the client Email string `json:"email"` // User's email address Password string `json:"password"` // Plain text (we'll hash it) Name string `json:"name"` // Display name // Optional fields - zero value if not provided Phone string `json:"phone,omitempty"` // "" if not sent Avatar string `json:"avatar,omitempty"` // "" if not sent } ``` **Key points:** * Use pointer types in method signatures: `*CreateInput` * Use `json` tags to control field names in JSON * Add `omitempty` for optional fields (tells JSON encoder to skip empty values) * Don't include server-generated fields (like ID, CreatedAt) ### Output Types Output types describe what you return to clients. They can include computed and server-generated fields: ```go theme={null} package user // User represents a user in the system. // This is what clients receive back from the API. type User struct { ID string `json:"id"` // Server-generated unique identifier Email string `json:"email"` // From input Name string `json:"name"` // From input CreatedAt time.Time `json:"createdAt"` // Server-generated timestamp UpdatedAt time.Time `json:"updatedAt"` // Server-managed timestamp } // ListOutput wraps a list of users with metadata. // Wrapping in a struct lets us add pagination info. type ListOutput struct { Users []*User `json:"users"` // The actual user data Total int `json:"total"` // Total count for pagination Page int `json:"page"` // Current page number } ``` ### Why Separate Input and Output Types? You might wonder why we don't just use one `User` type everywhere. Here's why separation is valuable: ```go theme={null} package todo // BAD: Using same type for input and output // Problems: // - Clients might try to set ID (server-generated) // - Clients might try to set CreatedAt (server-managed) // - No way to distinguish required vs optional fields type Todo struct { ID string `json:"id"` Title string `json:"title"` Completed bool `json:"completed"` CreatedAt time.Time `json:"createdAt"` } // GOOD: Separate types for different purposes // CreateInput - only what clients provide type CreateInput struct { Title string `json:"title"` // Only field needed to create } // UpdateInput - what can be updated type UpdateInput struct { ID string `json:"id"` // Which todo to update Title string `json:"title"` // Can change title Completed bool `json:"completed"` // Can change status } // Todo - full representation with server data type Todo struct { ID string `json:"id"` // Server-generated Title string `json:"title"` // From input Completed bool `json:"completed"` // From input or default CreatedAt time.Time `json:"createdAt"` // Server-generated UpdatedAt time.Time `json:"updatedAt"` // Server-managed } ``` ### Nested Types For complex data, nest types within each other. This creates clear, reusable structures: ```go theme={null} package order // Order represents a complete order with all its details. type Order struct { ID string `json:"id"` Customer *Customer `json:"customer"` // Nested customer info Items []*Item `json:"items"` // List of ordered items Shipping *Address `json:"shipping"` // Where to ship Billing *Address `json:"billing"` // Billing address (can reuse Address) Total Money `json:"total"` // Order total } // Customer contains customer information. type Customer struct { ID string `json:"id"` Name string `json:"name"` Email string `json:"email"` } // Item represents one line item in an order. type Item struct { ProductID string `json:"productId"` Name string `json:"name"` Quantity int `json:"quantity"` Price Money `json:"price"` } // Address is a physical address. // Reused for both shipping and billing. type Address struct { Street string `json:"street"` City string `json:"city"` State string `json:"state"` Country string `json:"country"` Zip string `json:"zip"` } // Money represents a monetary amount. type Money struct { Amount int64 `json:"amount"` // Amount in cents Currency string `json:"currency"` // ISO currency code } ``` ## Implementing Your Interface Now let's implement the interface with actual business logic. ### Basic Implementation Create a struct that implements all interface methods: ```go theme={null} package todo import ( "context" "database/sql" "time" ) // Service implements todo.API. // It contains dependencies needed to perform operations. type Service struct { db *sql.DB // Database connection } // Compile-time check that Service implements API. // If Service is missing methods, this line causes a compile error. var _ API = (*Service)(nil) // NewService creates a new todo Service with the given dependencies. // This is the "constructor" - the standard Go pattern for creating instances. func NewService(db *sql.DB) *Service { return &Service{db: db} } // Create implements todo.API.Create func (s *Service) Create(ctx context.Context, in *CreateInput) (*Todo, error) { // Validate input if in.Title == "" { return nil, errors.New("title is required") } // Create the todo now := time.Now() todo := &Todo{ ID: generateID(), Title: in.Title, Completed: false, CreatedAt: now, UpdatedAt: now, } // Save to database _, err := s.db.ExecContext(ctx, "INSERT INTO todos (id, title, completed, created_at, updated_at) VALUES (?, ?, ?, ?, ?)", todo.ID, todo.Title, todo.Completed, todo.CreatedAt, todo.UpdatedAt, ) if err != nil { return nil, err } return todo, nil } // List implements todo.API.List func (s *Service) List(ctx context.Context) (*ListOutput, error) { rows, err := s.db.QueryContext(ctx, "SELECT id, title, completed, created_at, updated_at FROM todos") if err != nil { return nil, err } defer rows.Close() var todos []*Todo for rows.Next() { t := &Todo{} if err := rows.Scan(&t.ID, &t.Title, &t.Completed, &t.CreatedAt, &t.UpdatedAt); err != nil { return nil, err } todos = append(todos, t) } return &ListOutput{ Items: todos, Count: len(todos), }, nil } // ... implement other methods ``` ### With Multiple Dependencies Real services often need multiple dependencies. Inject them all through the constructor: ```go theme={null} package user import ( "context" "database/sql" "log/slog" "github.com/redis/go-redis/v9" ) // Service implements user.API with all required dependencies. type Service struct { db *sql.DB // Primary database cache *redis.Client // Cache for frequent lookups mailer EmailSender // For sending emails (interface for testability) logger *slog.Logger // Structured logging } // EmailSender is an interface so we can mock it in tests. type EmailSender interface { Send(ctx context.Context, to, subject, body string) error } // NewService creates a new user Service with all dependencies. func NewService( db *sql.DB, cache *redis.Client, mailer EmailSender, logger *slog.Logger, ) *Service { return &Service{ db: db, cache: cache, mailer: mailer, logger: logger, } } func (s *Service) Create(ctx context.Context, in *CreateInput) (*User, error) { s.logger.Info("creating user", "email", in.Email) // Create user in database user := &User{ID: generateID(), Email: in.Email, Name: in.Name} if err := s.saveToDatabase(ctx, user); err != nil { s.logger.Error("failed to save user", "error", err) return nil, err } // Cache the user for fast lookups if err := s.cacheUser(ctx, user); err != nil { s.logger.Warn("failed to cache user", "error", err) // Don't fail the request - caching is optional } // Send welcome email if err := s.mailer.Send(ctx, user.Email, "Welcome!", "Thanks for signing up!"); err != nil { s.logger.Warn("failed to send welcome email", "error", err) // Don't fail the request - email is best-effort } return user, nil } ``` ### Compile-Time Interface Check Always add a compile-time check to ensure your implementation is complete. This catches missing methods immediately: ```go theme={null} package todo 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) Delete(ctx context.Context, in *DeleteInput) error } type Service struct { db *sql.DB } // This line is a compile-time assertion. // If Service doesn't implement all methods of API, compilation fails. // The syntax works because: // - (*Service)(nil) creates a nil pointer of type *Service // - var _ API = ... checks if that pointer satisfies the API interface var _ API = (*Service)(nil) // Now implement the methods... func (s *Service) Create(ctx context.Context, in *CreateInput) (*Todo, error) { // ... } ``` If you forget to implement a method, you'll see a clear error: ``` cannot use (*Service)(nil) (type *Service) as type API in assignment: *Service does not implement API (missing Delete method) ``` ## Method Naming Conventions Method names affect how they're exposed via REST. Contract uses naming conventions to determine HTTP methods: | Method Name Pattern | REST Endpoint | HTTP Method | | ----------------------- | ---------------------- | ----------- | | `Create`, `Add`, `New` | `/{resource}` | POST | | `List`, `All`, `Search` | `/{resource}` | GET | | `Get`, `Find`, `Fetch` | `/{resource}/{id}` | GET | | `Update`, `Edit`, `Set` | `/{resource}/{id}` | PUT | | `Delete`, `Remove` | `/{resource}/{id}` | DELETE | | `Patch` | `/{resource}/{id}` | PATCH | | Other names | `/{resource}/{action}` | POST | **Examples:** ```go theme={null} package product type API interface { Create(...) // POST /products - Create new product List(...) // GET /products - List all products Get(...) // GET /products/{id} - Get one product Update(...) // PUT /products/{id} - Update product Delete(...) // DELETE /products/{id} - Delete product Search(...) // GET /products - Search (with query params) Archive(...) // POST /products/archive - Custom action } ``` ## Using Context Every method receives `context.Context` as its first parameter. Context is Go's standard way to pass request-scoped data and control request lifecycle. ### Cancellation Check if the client cancelled the request. This is important for long-running operations: ```go theme={null} func (s *Service) ProcessLargeDataset(ctx context.Context, in *ProcessInput) (*Output, error) { for i, item := range in.Items { // Check if context was cancelled before each item if err := ctx.Err(); err != nil { // Client disconnected or timeout occurred return nil, err } // Process the item if err := s.processItem(ctx, item); err != nil { return nil, err } } return &Output{ProcessedCount: len(in.Items)}, nil } ``` ### Timeouts Pass context to database and external calls. The context carries timeout information: ```go theme={null} func (s *Service) Get(ctx context.Context, in *GetInput) (*Todo, error) { // The context timeout is automatically respected by the database driver row := s.db.QueryRowContext(ctx, "SELECT * FROM todos WHERE id = $1", in.ID) todo := &Todo{} err := row.Scan(&todo.ID, &todo.Title, &todo.Completed) if err == sql.ErrNoRows { return nil, contract.ErrNotFound("todo not found") } return todo, err } ``` ### Request-Scoped Values Access values set by middleware (like authenticated user): ```go theme={null} // Key type for context values (prevents collisions) type contextKey string const userIDKey contextKey = "userID" func (s *Service) Create(ctx context.Context, in *CreateInput) (*Todo, error) { // Get authenticated user ID from context (set by auth middleware) userID, ok := ctx.Value(userIDKey).(string) if !ok { return nil, contract.ErrUnauthenticated("not logged in") } // Use userID in your logic todo := &Todo{ ID: generateID(), Title: in.Title, OwnerID: userID, // Associate todo with the user } // Save and return return s.saveTodo(ctx, todo) } ``` ## Complete Example Here's a complete, production-ready service demonstrating all the concepts: ```go theme={null} // user/types.go package user import "time" // User represents a user in the system. type User struct { ID string `json:"id"` Email string `json:"email"` Name string `json:"name"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } // CreateInput contains data needed to create a user. type CreateInput struct { Email string `json:"email"` Name string `json:"name"` } // GetInput identifies which user to retrieve. type GetInput struct { ID string `json:"id"` } // UpdateInput contains data for updating a user. type UpdateInput struct { ID string `json:"id"` Name string `json:"name"` } // DeleteInput identifies which user to delete. type DeleteInput struct { ID string `json:"id"` } // ListInput contains pagination parameters. type ListInput struct { Limit int `json:"limit"` Offset int `json:"offset"` } // ListOutput contains paginated user results. type ListOutput struct { Users []*User `json:"users"` Total int `json:"total"` } ``` ```go theme={null} // user/api.go package user import "context" // API defines all user management operations. type API interface { // Create registers a new user account. Create(ctx context.Context, in *CreateInput) (*User, error) // Get retrieves a user by ID. Get(ctx context.Context, in *GetInput) (*User, error) // Update modifies a user's profile. Update(ctx context.Context, in *UpdateInput) (*User, error) // Delete removes a user account. Delete(ctx context.Context, in *DeleteInput) error // List returns users with pagination. List(ctx context.Context, in *ListInput) (*ListOutput, error) } ``` ```go theme={null} // user/service.go package user import ( "context" "database/sql" "time" "github.com/google/uuid" contract "github.com/go-mizu/mizu/contract/v2" ) // Service implements user.API. type Service struct { db *sql.DB } // Compile-time interface check. var _ API = (*Service)(nil) // NewService creates a new user Service. func NewService(db *sql.DB) *Service { return &Service{db: db} } func (s *Service) Create(ctx context.Context, in *CreateInput) (*User, error) { // Validate input if in.Email == "" { return nil, contract.ErrInvalidArgument("email is required") } if in.Name == "" { return nil, contract.ErrInvalidArgument("name is required") } // Create user now := time.Now() user := &User{ ID: uuid.New().String(), Email: in.Email, Name: in.Name, CreatedAt: now, UpdatedAt: now, } // Save to database _, err := s.db.ExecContext(ctx, "INSERT INTO users (id, email, name, created_at, updated_at) VALUES ($1, $2, $3, $4, $5)", user.ID, user.Email, user.Name, user.CreatedAt, user.UpdatedAt, ) if err != nil { return nil, err } return user, nil } func (s *Service) Get(ctx context.Context, in *GetInput) (*User, error) { user := &User{} err := s.db.QueryRowContext(ctx, "SELECT id, email, name, created_at, updated_at FROM users WHERE id = $1", in.ID, ).Scan(&user.ID, &user.Email, &user.Name, &user.CreatedAt, &user.UpdatedAt) if err == sql.ErrNoRows { return nil, contract.ErrNotFound("user not found") } if err != nil { return nil, err } return user, nil } func (s *Service) Update(ctx context.Context, in *UpdateInput) (*User, error) { now := time.Now() result, err := s.db.ExecContext(ctx, "UPDATE users SET name = $1, updated_at = $2 WHERE id = $3", in.Name, now, in.ID, ) if err != nil { return nil, err } rows, _ := result.RowsAffected() if rows == 0 { return nil, contract.ErrNotFound("user not found") } return s.Get(ctx, &GetInput{ID: in.ID}) } func (s *Service) Delete(ctx context.Context, in *DeleteInput) error { result, err := s.db.ExecContext(ctx, "DELETE FROM users WHERE id = $1", in.ID) if err != nil { return err } rows, _ := result.RowsAffected() if rows == 0 { return contract.ErrNotFound("user not found") } return nil } func (s *Service) List(ctx context.Context, in *ListInput) (*ListOutput, error) { // Default limit limit := in.Limit if limit <= 0 { limit = 10 } // Query users rows, err := s.db.QueryContext(ctx, "SELECT id, email, name, created_at, updated_at FROM users LIMIT $1 OFFSET $2", limit, in.Offset, ) if err != nil { return nil, err } defer rows.Close() var users []*User for rows.Next() { user := &User{} if err := rows.Scan(&user.ID, &user.Email, &user.Name, &user.CreatedAt, &user.UpdatedAt); err != nil { return nil, err } users = append(users, user) } // Get total count var total int s.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM users").Scan(&total) return &ListOutput{ Users: users, Total: total, }, nil } ``` ## Best Practices ### Keep Services Focused Each service should handle one domain. Don't create "god services" that do everything: ```go theme={null} // GOOD: Focused services, one domain each package todo // type API interface { Create, List, Get, Delete } package user // type API interface { Create, Get, Update, Delete } package order // type API interface { Create, Get, List, Cancel } // BAD: One giant service doing everything package api type EverythingAPI interface { CreateTodo(...) CreateUser(...) CreateOrder(...) // 50 more methods from different domains... } ``` ### Use Clear, Consistent Naming Names should be descriptive and follow a consistent pattern: ```go theme={null} // GOOD: Clear and consistent naming package subscription type API interface { Create(ctx context.Context, in *CreateInput) (*Subscription, error) Get(ctx context.Context, in *GetInput) (*Subscription, error) Cancel(ctx context.Context, in *CancelInput) error Renew(ctx context.Context, in *RenewInput) (*Subscription, error) } // BAD: Vague and inconsistent naming package sub type SubAPI interface { MakeSub(ctx context.Context, in *Input) (*Output, error) FetchSubscription(ctx context.Context, in *GetInput) (*Sub, error) DoCancel(ctx context.Context, in *CancelInput) error } ``` ### Inject Dependencies Pass dependencies through the constructor, never use globals: ```go theme={null} // GOOD: Dependency injection through constructor type Service struct { db Database cache Cache } func NewService(db Database, cache Cache) *Service { return &Service{db: db, cache: cache} } // BAD: Global dependencies var globalDB *sql.DB // DON'T DO THIS func (s *Service) Create(...) { globalDB.Query(...) // Untestable, hidden dependency } ``` ### Keep Business Logic in the Implementation The interface should only declare operations. All logic goes in the implementation: ```go theme={null} // Interface: Pure contract, no logic type API interface { ProcessPayment(ctx context.Context, in *PaymentInput) (*Payment, error) } // Implementation: All the business logic func (s *Service) ProcessPayment(ctx context.Context, in *PaymentInput) (*Payment, error) { // 1. Validation if in.Amount <= 0 { return nil, contract.ErrInvalidArgument("amount must be positive") } // 2. Business rules fee := s.calculateFee(in.Amount) total := in.Amount + fee // 3. External integrations chargeResult, err := s.stripe.Charge(ctx, in.CardToken, total) if err != nil { return nil, fmt.Errorf("payment failed: %w", err) } // 4. Persistence payment := &Payment{ ID: chargeResult.ID, Amount: in.Amount, Fee: fee, Status: "completed", } if err := s.db.SavePayment(ctx, payment); err != nil { return nil, err } return payment, nil } ``` ## Common Questions ### Can I have multiple return values? No, methods can only return `(*Output, error)` or `error`. If you need to return multiple values, wrap them in a struct: ```go theme={null} // Instead of: func Get() (*User, *Profile, error) // NOT SUPPORTED // Use a wrapper struct: type GetUserResult struct { User *User `json:"user"` Profile *Profile `json:"profile"` } func (s *Service) GetUser(ctx context.Context, in *GetInput) (*GetUserResult, error) { user, err := s.getUser(ctx, in.ID) if err != nil { return nil, err } profile, err := s.getProfile(ctx, in.ID) if err != nil { return nil, err } return &GetUserResult{User: user, Profile: profile}, nil } ``` ### Should input types be pointers? Yes, always use pointers in the method signature: ```go theme={null} // GOOD - use pointers func (s *Service) Create(ctx context.Context, in *CreateInput) (*Output, error) // BAD - don't use value types func (s *Service) Create(ctx context.Context, in CreateInput) (Output, error) ``` Contract requires pointers for proper JSON unmarshaling and to support nil inputs. ### How do I handle optional fields? Fields not provided by clients will have their zero value. For most cases, this works fine: ```go theme={null} type UpdateInput struct { ID string `json:"id"` // Required - must be provided Title string `json:"title"` // Optional - "" if not sent Count int `json:"count"` // Optional - 0 if not sent } ``` For fields where you need to distinguish "not sent" from "sent as empty/zero", use pointers: ```go theme={null} type UpdateInput struct { ID string `json:"id"` Title *string `json:"title"` // nil = not sent, "" = sent as empty Count *int `json:"count"` // nil = not sent, 0 = sent as zero } func (s *Service) Update(ctx context.Context, in *UpdateInput) (*Todo, error) { todo, _ := s.Get(ctx, &GetInput{ID: in.ID}) // Only update if field was explicitly sent if in.Title != nil { todo.Title = *in.Title } if in.Count != nil { todo.Count = *in.Count } return s.save(ctx, todo) } ``` ## What's Next? Now that you know how to define services: * **[Registration](/contract/register)** - How to register your service with Contract * **[Error Handling](/contract/errors)** - Handle errors consistently across protocols * **[Type System](/contract/types)** - Deep dive into type conversion and schemas * **[Testing](/contract/testing)** - Test your services effectively # Testing Source: https://docs.go-mizu.dev/contract/testing Learn how to test your Contract services - from unit tests to integration tests # Testing One of Contract's biggest advantages is testability. Since your services are just plain Go structs with methods, you can test them directly without HTTP mocking, framework setup, or complex test fixtures. ## Why Contract Services Are Easy to Test Traditional API testing looks like this: ```go theme={null} // Testing a typical HTTP handler - lots of ceremony func TestCreateTodo(t *testing.T) { // Setup HTTP server server := httptest.NewServer(myHandler) defer server.Close() // Create HTTP request body := strings.NewReader(`{"title":"Test"}`) req, _ := http.NewRequest("POST", server.URL+"/todos", body) req.Header.Set("Content-Type", "application/json") // Make HTTP call resp, err := http.DefaultClient.Do(req) // Parse response... // Check status code... // Unmarshal JSON... } ``` With Contract: ```go theme={null} // Testing a Contract service - just call the method! func TestCreate(t *testing.T) { svc := todo.NewService() result, err := svc.Create(context.Background(), &todo.CreateInput{Title: "Test"}) if err != nil { t.Fatalf("unexpected error: %v", err) } if result.Title != "Test" { t.Errorf("expected 'Test', got '%s'", result.Title) } } ``` ## Unit Testing Your Service Unit tests test your business logic directly, without any transport layer. ### Basic Test ```go theme={null} package todo_test import ( "context" "testing" "yourmodule/service/todo" ) func TestCreate(t *testing.T) { // Create service instance svc := todo.NewService() // Call the method directly result, err := svc.Create(context.Background(), &todo.CreateInput{ Title: "Buy groceries", }) // Check the results if err != nil { t.Fatalf("unexpected error: %v", err) } if result.Title != "Buy groceries" { t.Errorf("expected 'Buy groceries', got '%s'", result.Title) } if result.ID == "" { t.Error("expected ID to be set") } } ``` ### Table-Driven Tests For testing multiple scenarios, use table-driven tests: ```go theme={null} func TestCreate_Validation(t *testing.T) { tests := []struct { name string input *todo.CreateInput wantErr bool errMsg string }{ { name: "valid title", input: &todo.CreateInput{Title: "Buy milk"}, wantErr: false, }, { name: "empty title", input: &todo.CreateInput{Title: ""}, wantErr: true, errMsg: "title is required", }, { name: "title too long", input: &todo.CreateInput{Title: strings.Repeat("x", 1001)}, wantErr: true, errMsg: "title too long", }, } svc := todo.NewService() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, err := svc.Create(context.Background(), tt.input) if tt.wantErr { if err == nil { t.Error("expected error, got nil") } else if !strings.Contains(err.Error(), tt.errMsg) { t.Errorf("expected error containing '%s', got '%s'", tt.errMsg, err.Error()) } } else { if err != nil { t.Errorf("unexpected error: %v", err) } } }) } } ``` ### Testing Error Types When using Contract's error types, verify the error code: ```go theme={null} import ( "errors" contract "github.com/go-mizu/mizu/contract/v2" ) func TestGet_NotFound(t *testing.T) { svc := todo.NewService() _, err := svc.Get(context.Background(), &todo.GetInput{ID: "nonexistent"}) // Check that we got an error if err == nil { t.Fatal("expected error, got nil") } // Check that it's a Contract error var contractErr *contract.Error if !errors.As(err, &contractErr) { t.Fatalf("expected contract.Error, got %T: %v", err, err) } // Check the error code if contractErr.Code != contract.ErrCodeNotFound { t.Errorf("expected NOT_FOUND, got %s", contractErr.Code) } // Check the HTTP status would be correct if contractErr.HTTPStatus() != 404 { t.Errorf("expected HTTP 404, got %d", contractErr.HTTPStatus()) } } ``` ## Testing Services with Dependencies Real services have dependencies like databases and caches. Use dependency injection and interfaces for testability. ### Define Interfaces ```go theme={null} // In your service package (e.g., todo/service.go) package todo type Service struct { db Database cache Cache } // Database interface for mocking type Database interface { FindTodo(ctx context.Context, id string) (*Todo, error) SaveTodo(ctx context.Context, todo *Todo) error DeleteTodo(ctx context.Context, id string) error } // Cache interface for mocking type Cache interface { Get(ctx context.Context, key string) ([]byte, error) Set(ctx context.Context, key string, value []byte) error } ``` ### Create Mock Implementations ```go theme={null} // Simple mock for testing type MockDatabase struct { todos map[string]*Todo } func NewMockDatabase() *MockDatabase { return &MockDatabase{todos: make(map[string]*Todo)} } func (m *MockDatabase) FindTodo(ctx context.Context, id string) (*Todo, error) { todo, ok := m.todos[id] if !ok { return nil, sql.ErrNoRows } return todo, nil } func (m *MockDatabase) SaveTodo(ctx context.Context, todo *Todo) error { m.todos[todo.ID] = todo return nil } func (m *MockDatabase) DeleteTodo(ctx context.Context, id string) error { delete(m.todos, id) return nil } ``` ### Test with Mocks ```go theme={null} func TestGet_WithMock(t *testing.T) { // Setup mock with test data mockDB := NewMockDatabase() mockDB.todos["1"] = &Todo{ID: "1", Title: "Test Todo"} // Create service with mock svc := &Service{db: mockDB} // Test the method result, err := svc.Get(context.Background(), &GetInput{ID: "1"}) if err != nil { t.Fatalf("unexpected error: %v", err) } if result.Title != "Test Todo" { t.Errorf("expected 'Test Todo', got '%s'", result.Title) } } ``` ## Integration Testing Transports Sometimes you need to test the full HTTP flow. Contract makes this easy too. ### Testing REST Endpoints ```go theme={null} import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "github.com/go-mizu/mizu" contract "github.com/go-mizu/mizu/contract/v2" "github.com/go-mizu/mizu/contract/v2/transport/rest" "yourapp/todo" ) // Your interface is defined in the todo package as todo.API func TestREST_Create(t *testing.T) { // Setup impl := todo.NewService() svc := contract.Register[todo.API](impl, contract.WithDefaultResource("todos"), ) app := mizu.New() rest.Mount(app.Router, svc) // Create request body := strings.NewReader(`{"title": "Test"}`) req := httptest.NewRequest("POST", "/todos", body) req.Header.Set("Content-Type", "application/json") // Execute rec := httptest.NewRecorder() app.Router.ServeHTTP(rec, req) // Verify status code if rec.Code != http.StatusOK { t.Errorf("expected 200, got %d: %s", rec.Code, rec.Body.String()) } // Verify response body var result Todo if err := json.NewDecoder(rec.Body).Decode(&result); err != nil { t.Fatalf("failed to decode response: %v", err) } if result.Title != "Test" { t.Errorf("expected 'Test', got '%s'", result.Title) } } func TestREST_Get_NotFound(t *testing.T) { impl := todo.NewService() svc := contract.Register[todo.API](impl, contract.WithDefaultResource("todos"), ) app := mizu.New() rest.Mount(app.Router, svc) req := httptest.NewRequest("GET", "/todos/nonexistent", nil) rec := httptest.NewRecorder() app.Router.ServeHTTP(rec, req) // Should return 404 for not found if rec.Code != http.StatusNotFound { t.Errorf("expected 404, got %d", rec.Code) } } ``` ### Testing JSON-RPC ```go theme={null} import ( "github.com/go-mizu/mizu/contract/v2/transport/jsonrpc" "yourapp/todo" ) func TestJSONRPC_Create(t *testing.T) { impl := todo.NewService() svc := contract.Register[todo.API](impl, contract.WithDefaultResource("todos"), ) app := mizu.New() jsonrpc.Mount(app.Router, "/rpc", svc) body := strings.NewReader(`{ "jsonrpc": "2.0", "id": 1, "method": "todos.create", "params": {"title": "Test"} }`) req := httptest.NewRequest("POST", "/rpc", body) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() app.Router.ServeHTTP(rec, req) // Parse JSON-RPC response var resp struct { JSONRPC string `json:"jsonrpc"` ID int `json:"id"` Result json.RawMessage `json:"result"` Error *struct { Code int `json:"code"` Message string `json:"message"` } `json:"error"` } json.NewDecoder(rec.Body).Decode(&resp) if resp.Error != nil { t.Fatalf("unexpected error: %s", resp.Error.Message) } if resp.Result == nil { t.Fatal("expected result, got nil") } } func TestJSONRPC_Batch(t *testing.T) { impl := todo.NewService() svc := contract.Register[todo.API](impl, contract.WithDefaultResource("todos"), ) app := mizu.New() jsonrpc.Mount(app.Router, "/rpc", svc) body := strings.NewReader(`[ {"jsonrpc": "2.0", "id": 1, "method": "todos.create", "params": {"title": "First"}}, {"jsonrpc": "2.0", "id": 2, "method": "todos.create", "params": {"title": "Second"}} ]`) req := httptest.NewRequest("POST", "/rpc", body) rec := httptest.NewRecorder() app.Router.ServeHTTP(rec, req) var responses []map[string]any json.NewDecoder(rec.Body).Decode(&responses) if len(responses) != 2 { t.Errorf("expected 2 responses, got %d", len(responses)) } } ``` ### Testing MCP ```go theme={null} import "github.com/go-mizu/mizu/contract/v2/transport/mcp" func TestMCP_ToolsCall(t *testing.T) { impl := todo.NewService() svc := contract.Register[todo.API](impl, contract.WithDefaultResource("todos"), ) app := mizu.New() mcp.Mount(app.Router, "/mcp", svc) // First, initialize initBody := `{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}` req := httptest.NewRequest("POST", "/mcp", strings.NewReader(initBody)) rec := httptest.NewRecorder() app.Router.ServeHTTP(rec, req) // Then, call a tool callBody := `{ "jsonrpc":"2.0", "id":2, "method":"tools/call", "params":{"name":"todos.create","arguments":{"title":"Test"}} }` req = httptest.NewRequest("POST", "/mcp", strings.NewReader(callBody)) rec = httptest.NewRecorder() app.Router.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Errorf("expected 200, got %d: %s", rec.Code, rec.Body.String()) } var resp struct { Result struct { Content []struct { Type string `json:"type"` Text string `json:"text"` } `json:"content"` IsError bool `json:"isError"` } `json:"result"` } json.NewDecoder(rec.Body).Decode(&resp) if resp.Result.IsError { t.Error("expected success, got error") } } ``` ## Test Helpers Create helper functions to reduce boilerplate: ```go theme={null} // Setup helper func setupTestApp(t *testing.T) *mizu.App { t.Helper() impl := todo.NewService() svc := contract.Register[todo.API](impl, contract.WithDefaultResource("todos"), ) app := mizu.New() rest.Mount(app.Router, svc) jsonrpc.Mount(app.Router, "/rpc", svc) return app } // Usage func TestIntegration(t *testing.T) { app := setupTestApp(t) // Test REST req := httptest.NewRequest("POST", "/todos", strings.NewReader(`{"title":"Test"}`)) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() app.Router.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Errorf("expected 200, got %d", rec.Code) } } ``` ## Best Practices ### 1. Test Business Logic Separately from Transports ```go theme={null} // Good: separate tests for each layer // Unit test - business logic func TestService_Create(t *testing.T) { svc := NewService() result, err := svc.Create(ctx, input) // Assert... } // Integration test - HTTP transport func TestREST_Create(t *testing.T) { // HTTP request/response test } ``` ### 2. Use Subtests for Organization ```go theme={null} func TestTodoService(t *testing.T) { svc := todo.NewService() t.Run("Create", func(t *testing.T) { result, err := svc.Create(ctx, &todo.CreateInput{Title: "Test"}) // ... }) t.Run("Get", func(t *testing.T) { result, err := svc.Get(ctx, &todo.GetInput{ID: "1"}) // ... }) t.Run("Delete", func(t *testing.T) { err := svc.Delete(ctx, &todo.DeleteInput{ID: "1"}) // ... }) } ``` ### 3. Test Error Conditions Don't just test the happy path: ```go theme={null} func TestGet_Errors(t *testing.T) { svc := todo.NewService() t.Run("not found", func(t *testing.T) { _, err := svc.Get(ctx, &todo.GetInput{ID: "nonexistent"}) assertErrorCode(t, err, contract.ErrCodeNotFound) }) t.Run("empty id", func(t *testing.T) { _, err := svc.Get(ctx, &todo.GetInput{ID: ""}) assertErrorCode(t, err, contract.ErrCodeInvalidArgument) }) } // Helper function func assertErrorCode(t *testing.T, err error, want contract.ErrorCode) { t.Helper() var contractErr *contract.Error if !errors.As(err, &contractErr) { t.Fatalf("expected contract.Error, got %T", err) } if contractErr.Code != want { t.Errorf("expected %s, got %s", want, contractErr.Code) } } ``` ### 4. Use Parallel Tests When Safe ```go theme={null} func TestService(t *testing.T) { t.Parallel() // This test can run in parallel with others tests := []struct { name string input *CreateInput }{ {"test 1", &CreateInput{Title: "One"}}, {"test 2", &CreateInput{Title: "Two"}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() // Subtests can run in parallel svc := NewService() // Each subtest gets its own service svc.Create(ctx, tt.input) // ... }) } } ``` ## See Also * [Services](/contract/service) - Writing testable services * [Errors](/contract/errors) - Testing error conditions * [Architecture](/contract/architecture) - Understanding what to test at each layer # Overview Source: https://docs.go-mizu.dev/contract/transports-overview Understand the different protocols Contract supports and choose the right one for your project 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: | Transport | What It Is | Best For | | ------------ | -------------------------------- | ------------------------------- | | **REST** | Traditional HTTP API | Web apps, mobile apps, curl | | **JSON-RPC** | Remote procedure calls over HTTP | Batch operations, microservices | | **MCP** | Model Context Protocol | AI assistants like Claude | | **OpenAPI** | API documentation standard | Docs, 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 | Feature | REST | JSON-RPC | MCP | | -------------------- | ------------------- | --------------- | -------------- | | **HTTP Methods** | GET/POST/PUT/DELETE | POST only | POST only | | **Batch Requests** | No | Yes | No | | **Cacheable** | Yes (GET requests) | No | No | | **Self-Documenting** | Via OpenAPI | No | Via tools/list | | **Error Format** | HTTP status codes | JSON-RPC errors | isError flag | ### REST - The Classic Choice REST is the most widely known API style. It uses HTTP methods to represent actions: ```bash theme={null} # 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. ```bash theme={null} # 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: ```bash theme={null} 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". ```bash theme={null} # 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. ```bash theme={null} # 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: ```go theme={null} 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: ```go theme={null} return nil, contract.ErrNotFound("todo not found") ``` Each transport formats it appropriately: | Transport | Error Response | | --------- | ----------------------------------------------------------------------------------------- | | REST | HTTP 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: ```go theme={null} // 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"` } ``` ```go theme={null} // 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) } ``` ```go theme={null} // 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 } ``` ```go theme={null} // 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: ```go theme={null} 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: ```go theme={null} rest.Mount(app.Router, svc) ``` ### Building Microservices JSON-RPC for service-to-service (batching is valuable): ```go theme={null} jsonrpc.Mount(app.Router, "/rpc", svc) ``` ### Building AI-Powered Apps MCP for AI assistants, plus REST for human debugging: ```go theme={null} 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: ```go theme={null} 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 Method | Transport Method Name | | ---------------- | --------------------- | | `Create` | `todos.create` | | `List` | `todos.list` | | `Get` | `todos.get` | For REST, method names are mapped to HTTP verbs and paths automatically. ## What's Next? Each transport has its own detailed documentation: * **[REST Transport](/contract/rest)** - HTTP verbs, path patterns, customization * **[JSON-RPC Transport](/contract/jsonrpc)** - Batching, notifications, error codes * **[MCP Transport](/contract/mcp)** - AI integration, Claude Desktop setup * **[OpenAPI](/contract/openapi)** - Documentation, Swagger UI, code generation # Types Source: https://docs.go-mizu.dev/contract/types Understand how Contract converts your Go types into JSON schemas for validation and documentation ## What is the Type System? When you register a service, Contract automatically creates JSON schemas from your Go types. Think of schemas as a universal language that describes the shape of your data - what fields exist, what types they have, and which ones are required. These schemas are used for: * **API documentation** - OpenAPI specs include accurate type information so developers know exactly what to send * **Input validation** - Requests can be validated against the schema before hitting your code * **Client code generation** - Generate typed clients in TypeScript, Python, or any language * **AI tool definitions** - MCP tells AI assistants exactly what parameters your tools accept ## How Types are Discovered When you call `contract.Register[todo.API](impl)`, Contract inspects every method in your interface and extracts the input and output types: ```go theme={null} package todo type API interface { // Input: CreateInput, Output: Todo Create(ctx context.Context, in *CreateInput) (*Todo, error) // Input: none (just context), Output: ListOutput List(ctx context.Context) (*ListOutput, error) // Input: GetInput, Output: Todo Get(ctx context.Context, in *GetInput) (*Todo, error) } ``` Contract discovers `CreateInput`, `Todo`, `ListOutput`, and `GetInput`. It also recursively discovers any types those structs reference. Each unique type is registered once and stored in the service descriptor. ## Go to JSON Schema Conversion Contract converts Go types to JSON Schema types automatically. This table shows the mapping: ### Primitive Types | Go Type | JSON Schema Type | JSON Example | | --------------------------------------------- | ------------------------------------------- | ------------------------ | | `string` | `{"type": "string"}` | `"hello"` | | `int`, `int8`, `int16`, `int32`, `int64` | `{"type": "integer"}` | `42` | | `uint`, `uint8`, `uint16`, `uint32`, `uint64` | `{"type": "integer"}` | `42` | | `float32`, `float64` | `{"type": "number"}` | `3.14` | | `bool` | `{"type": "boolean"}` | `true` | | `time.Time` | `{"type": "string", "format": "date-time"}` | `"2024-01-15T10:30:00Z"` | | `[]byte` | `{"type": "string", "format": "byte"}` | `"SGVsbG8="` (base64) | ### Struct Types Structs become JSON objects with properties. Each exported field becomes a property: ```go theme={null} package user type User struct { ID string `json:"id"` Name string `json:"name"` Email string `json:"email"` Age int `json:"age"` IsActive bool `json:"isActive"` CreatedAt time.Time `json:"createdAt"` } ``` This produces the following JSON Schema: ```json theme={null} { "type": "object", "properties": { "id": {"type": "string"}, "name": {"type": "string"}, "email": {"type": "string"}, "age": {"type": "integer"}, "isActive": {"type": "boolean"}, "createdAt": {"type": "string", "format": "date-time"} }, "required": ["id", "name", "email", "age", "isActive", "createdAt"] } ``` Notice that all fields without `omitempty` are marked as required. ### Slice/Array Types Go slices become JSON arrays: ```go theme={null} package todo type ListOutput struct { Items []*Todo `json:"items"` Total int `json:"total"` } ``` Produces: ```json theme={null} { "type": "object", "properties": { "items": { "type": "array", "items": {"$ref": "#/components/schemas/Todo"} }, "total": {"type": "integer"} }, "required": ["items", "total"] } ``` The `$ref` syntax means "reference to the Todo schema defined elsewhere." This avoids duplicating the Todo schema everywhere it's used. ### Map Types Go maps become JSON objects with dynamic keys: ```go theme={null} package config type Config struct { Settings map[string]string `json:"settings"` Metadata map[string]any `json:"metadata"` } ``` Produces: ```json theme={null} { "type": "object", "properties": { "settings": { "type": "object", "additionalProperties": {"type": "string"} }, "metadata": { "type": "object", "additionalProperties": {} } }, "required": ["settings", "metadata"] } ``` The `additionalProperties` field describes what type the map values can be. ### Nested Structs Nested structs are automatically discovered and referenced: ```go theme={null} package order type Order struct { ID string `json:"id"` Customer *User `json:"customer"` Items []*Item `json:"items"` Shipping *Address `json:"shipping"` } type User struct { ID string `json:"id"` Name string `json:"name"` Email string `json:"email"` } type Item struct { ProductID string `json:"productId"` Quantity int `json:"quantity"` Price float64 `json:"price"` } type Address struct { Street string `json:"street"` City string `json:"city"` Country string `json:"country"` } ``` Contract generates schemas for `Order`, `User`, `Item`, and `Address`, with the Order schema referencing the others: ```json theme={null} { "type": "object", "properties": { "id": {"type": "string"}, "customer": {"$ref": "#/components/schemas/User"}, "items": { "type": "array", "items": {"$ref": "#/components/schemas/Item"} }, "shipping": {"$ref": "#/components/schemas/Address"} }, "required": ["id", "customer", "items", "shipping"] } ``` ### Pointer Types Pointer types are treated the same as non-pointer types in the schema. The pointer affects nullability at runtime but doesn't change the schema: ```go theme={null} package todo type UpdateInput struct { Title *string `json:"title"` // Can be null in JSON Count int `json:"count"` // Cannot be null in JSON } ``` ## JSON Tags Contract uses the `json` struct tag to determine field names and behavior. Let's explore all the options. ### Field Naming The `json` tag controls the field name in JSON: ```go theme={null} package todo type Todo struct { ID string `json:"id"` // JSON field: "id" Title string `json:"title"` // JSON field: "title" CreatedAt string `json:"created_at"` // JSON field: "created_at" UpdatedAt string `json:"updatedAt"` // JSON field: "updatedAt" } ``` Without tags, Go field names are used as-is (including uppercase first letter): ```go theme={null} package todo type Todo struct { ID string // JSON field: "ID" (not recommended) Title string // JSON field: "Title" (not recommended) } ``` **Best practice**: Always use `json` tags with lowercase or camelCase field names. This matches JSON conventions and makes your API easier to use from JavaScript. ### Optional Fields with omitempty Use `omitempty` for optional fields. Fields with `omitempty`: * Are not marked as "required" in the schema * Are omitted from JSON output when they have their zero value ```go theme={null} package todo type UpdateInput struct { ID string `json:"id"` // Required in JSON schema Title string `json:"title,omitempty"` // Optional in JSON schema Phone string `json:"phone,omitempty"` // Optional in JSON schema } ``` ### Excluding Fields Use `-` to completely exclude a field from JSON (both serialization and schema): ```go theme={null} package user type User struct { ID string `json:"id"` Name string `json:"name"` PasswordHash string `json:"-"` // Never appears in JSON or schema } ``` This is essential for sensitive fields that should never leave your server. ## Accessing Type Information After registration, you can access type information from the service descriptor. This is useful for building tools, documentation, or debugging. ### Getting All Types ```go theme={null} package main import ( "fmt" contract "github.com/go-mizu/mizu/contract/v2" "yourapp/todo" ) func main() { svc := contract.Register[todo.API](impl, contract.WithDefaultResource("todos"), ) desc := svc.Descriptor() fmt.Println("Registered Types:") for _, t := range desc.Types { fmt.Printf(" %s (%s)\n", t.Name, t.Kind) for _, f := range t.Fields { optional := "" if f.Optional { optional = " (optional)" } fmt.Printf(" - %s: %s%s\n", f.Name, f.Type, optional) } } } ``` Output: ``` Registered Types: CreateInput (struct) - title: string GetInput (struct) - id: string Todo (struct) - id: string - title: string - completed: boolean ListOutput (struct) - items: []Todo - total: integer ``` ### Type Definition Structure Each type in the descriptor has this structure: ```go theme={null} type Type struct { Name string // Type name: "CreateInput", "Todo", etc. Description string // From documentation (if available) Kind TypeKind // "struct", "slice", "map", "union" Fields []Field // For struct types: list of fields Elem string // For slice/map: element type name } type Field struct { Name string // Field name from json tag: "title", "id" Description string // From `desc` tag (if present) Type string // Type reference: "string", "integer", "Todo" Optional bool // True if field has omitempty Nullable bool // True if field is a pointer type } ``` ## Type Kinds Contract supports several type kinds: ### Struct The most common kind - a collection of named fields: ```go theme={null} // Kind: "struct" type User struct { ID string `json:"id"` Name string `json:"name"` } ``` ### Slice An ordered collection of elements: ```go theme={null} // Kind: "slice", Elem: "User" type Users []*User ``` ### Map A dictionary with string keys: ```go theme={null} // Kind: "map", Elem: "string" type Headers map[string]string ``` ### Union (Future) Discriminated unions for "one of these types" scenarios: ```go theme={null} // Kind: "union" (not yet implemented) // Coming in a future version ``` ## Additional Field Tags Beyond `json`, Contract recognizes additional struct tags for documentation and behavior. ### Description Tag Add descriptions that appear in OpenAPI docs and AI tool definitions: ```go theme={null} package todo type CreateInput struct { Title string `json:"title" desc:"The todo item title (required)"` Priority int `json:"priority" desc:"Priority from 1 (low) to 5 (high), default is 3"` } ``` These descriptions help both humans reading the docs and AI assistants understanding what values to provide. ### Required Tag Explicitly mark fields as required (overrides omitempty): ```go theme={null} package todo type CreateInput struct { Title string `json:"title" required:"true"` // Definitely required Notes string `json:"notes,omitempty"` // Optional } ``` ### Path Tag Mark fields as URL path parameters for REST: ```go theme={null} package todo type GetInput struct { // This field is extracted from the URL path, not the request body ID string `json:"id" path:"id"` } ``` When a client calls `GET /todos/abc123`, the `ID` field is populated with `"abc123"` from the URL. ## Complete Example Here's a complete example showing type definitions and how to inspect them: ```go theme={null} // todo/types.go package todo import "time" // Todo represents a single todo item type Todo struct { ID string `json:"id" desc:"Unique identifier"` Title string `json:"title" desc:"The todo title"` Description string `json:"description,omitempty" desc:"Optional detailed description"` Completed bool `json:"completed" desc:"Whether the todo is done"` Priority int `json:"priority" desc:"Priority 1-5, higher is more urgent"` Tags []string `json:"tags,omitempty" desc:"Optional categorization tags"` CreatedAt time.Time `json:"createdAt" desc:"When the todo was created"` UpdatedAt time.Time `json:"updatedAt" desc:"When the todo was last modified"` } // CreateInput contains fields for creating a new todo type CreateInput struct { Title string `json:"title" required:"true" desc:"The todo title (required)"` Description string `json:"description,omitempty" desc:"Optional description"` Priority int `json:"priority,omitempty" desc:"Priority 1-5, defaults to 3"` Tags []string `json:"tags,omitempty" desc:"Optional tags"` } // GetInput identifies a todo to retrieve type GetInput struct { ID string `json:"id" path:"id" desc:"The todo's unique identifier"` } // ListInput contains filters and pagination for listing todos type ListInput struct { Completed *bool `json:"completed,omitempty" desc:"Filter by completion status"` Tag string `json:"tag,omitempty" desc:"Filter by tag"` Limit int `json:"limit,omitempty" desc:"Max items to return (default 20)"` Offset int `json:"offset,omitempty" desc:"Items to skip for pagination"` } // ListOutput contains paginated todo results type ListOutput struct { Items []*Todo `json:"items" desc:"The list of todos"` Total int `json:"total" desc:"Total count (for pagination)"` } ``` ```go theme={null} // todo/api.go package todo import "context" type API interface { Create(ctx context.Context, in *CreateInput) (*Todo, error) Get(ctx context.Context, in *GetInput) (*Todo, error) List(ctx context.Context, in *ListInput) (*ListOutput, error) } ``` ```go theme={null} // main.go - inspecting types package main import ( "fmt" contract "github.com/go-mizu/mizu/contract/v2" "yourapp/todo" ) func main() { impl := todo.NewService() svc := contract.Register[todo.API](impl, contract.WithDefaultResource("todos"), ) desc := svc.Descriptor() // Print methods and their types fmt.Println("=== Methods ===") for _, res := range desc.Resources { for _, m := range res.Methods { fmt.Printf("%s.%s\n", res.Name, m.Name) if m.Input != nil { fmt.Printf(" Input: %s\n", m.Input.Name) } if m.Output != nil { fmt.Printf(" Output: %s\n", m.Output.Name) } } } // Print all types fmt.Println("\n=== Types ===") for _, t := range desc.Types { fmt.Printf("%s:\n", t.Name) for _, f := range t.Fields { optional := "" if f.Optional { optional = " (optional)" } fmt.Printf(" - %s: %s%s\n", f.Name, f.Type, optional) if f.Description != "" { fmt.Printf(" %s\n", f.Description) } } } } ``` ## Tips for Good Type Design ### Use Clear, Descriptive Names Names should communicate purpose: ```go theme={null} // Good: clear purpose from the name type CreateUserInput struct { ... } type UpdateOrderInput struct { ... } type OrderSummaryOutput struct { ... } // Bad: vague, could mean anything type Input struct { ... } type Data struct { ... } type Result struct { ... } ``` ### Use Appropriate Go Types Choose the right Go type for each field: ```go theme={null} package todo type Todo struct { ID string `json:"id"` // IDs are strings (UUIDs, etc.) Title string `json:"title"` // Text content Count int `json:"count"` // Whole numbers Price float64 `json:"price"` // Decimal numbers Active bool `json:"active"` // True/false flags CreatedAt time.Time `json:"createdAt"` // Timestamps Tags []string `json:"tags"` // Lists } ``` ### Group Related Types Organize types by purpose: ```go theme={null} // Input types - what clients send type CreateTodoInput struct { ... } type UpdateTodoInput struct { ... } type DeleteTodoInput struct { ... } // Output types - what you return type Todo struct { ... } type TodoList struct { ... } // Shared types - used in multiple places type Pagination struct { ... } type SortOptions struct { ... } ``` ### Keep Types Focused Each type should have one clear purpose: ```go theme={null} // Good: each type serves one purpose type CreateInput struct { Title string `json:"title"` // Only field needed to create } type UpdateInput struct { ID string `json:"id"` // Which todo Title string `json:"title"` // New title Completed bool `json:"completed"` // New status } // Bad: one type for everything (confusing) type TodoInput struct { ID string `json:"id,omitempty"` Title string `json:"title,omitempty"` Completed *bool `json:"completed,omitempty"` // Which operation is this for? Who knows! } ``` ## Common Questions ### Are all fields required by default? Yes. Exported struct fields without `omitempty` are marked as required in the schema. Add `omitempty` to make a field optional: ```go theme={null} type Input struct { Name string `json:"name"` // Required Email string `json:"email,omitempty"` // Optional } ``` ### Can I use interfaces as field types? No. Contract needs concrete types to generate schemas. If you have a field that could be multiple types, consider: 1. Using a struct with optional fields 2. Creating separate methods for each type 3. Using `map[string]any` for truly dynamic data (loses type safety) ### How do I handle nullable fields? Use pointer types for fields that can be explicitly null (as opposed to just missing): ```go theme={null} type UpdateInput struct { Title *string `json:"title"` // Can be: missing, null, or "value" Count int `json:"count"` // Can be: missing or number (0 is valid) } ``` With a pointer: * `nil` means "not provided" or "set to null" * `""` (empty string) means "set to empty string" Without a pointer: * `""` could mean either "not provided" or "set to empty" ### What happens to unexported fields? Unexported fields (lowercase names) are ignored. They don't appear in JSON or schemas: ```go theme={null} type User struct { ID string `json:"id"` // Included Name string `json:"name"` // Included password string // Ignored (unexported) } ``` ### Can I customize the generated schema? Not directly. Design your Go types to produce the schema you want. The mapping from Go to JSON Schema is deterministic, so you can predict the output by understanding the rules in this document. If you need very specific schema features not supported by Go types, consider: 1. Using a code generator to create types from an existing schema 2. Generating OpenAPI specs and editing them manually 3. Contributing a feature request to Contract ## What's Next? Now that you understand the type system: * **[Registration](/contract/register)** - How types are discovered during registration * **[OpenAPI](/contract/openapi)** - How schemas appear in API documentation * **[Error Handling](/contract/errors)** - Returning errors from your methods # Hello, Mizu Source: https://docs.go-mizu.dev/examples/001-hello-mizu Learn how to build your first web server with Mizu. Let's start with your first Mizu app. You will build a small web server that shows **Hello, Mizu** in your browser. It is a simple way to see how Mizu works while keeping everything clear and easy to follow. Mizu uses Go's standard library under the hood, so if you have seen `net/http` before, this will look familiar. ### Code Create a file called `main.go` and add this code: ```go theme={null} package main import ( "log" "net/http" "github.com/go-mizu/mizu" ) // home handles requests to the root path ("/") func home(c *mizu.Ctx) error { return c.Text(http.StatusOK, "Hello, Mizu") } func main() { app := mizu.New() // Register the route app.Get("/", home) // Start the server if err := app.Listen(":8080"); err != nil { log.Fatal(err) } } ``` Here is what happens step by step: * `mizu.New()` creates your web app. * `app.Get("/", home)` connects the URL `/` to your `home` handler. * `home()` sends a plain text message with status code `200 OK`. * `app.Listen(":8080")` runs the server and waits for requests. ### Run Open your terminal and run: ``` go run . ``` You will see: ``` Listening on :8080 ``` Then open [http://localhost:8080](http://localhost:8080) in your browser. You should see **Hello, Mizu** on the screen. ### How it works When you visit `/`, Mizu matches that route and calls the `home` function. Inside `home`, the line `c.Text(http.StatusOK, "Hello, Mizu")` sends a text response back to the browser with a 200 OK status. This is the basic flow in every Mizu app: receive a request, handle it with a function, and send a response using the context `c`. ### Try something new You can change the message to anything you like: ```go theme={null} func home(c *mizu.Ctx) error { return c.Text(http.StatusOK, "Hello, Go!") } ``` Or add another route: ```go theme={null} func about(c *mizu.Ctx) error { return c.Text(http.StatusOK, "This is the about page.") } app.Get("/about", about) ``` Now if you visit [http://localhost:8080/about](http://localhost:8080/about), you will see your new page. That is it, you have built your first Mizu web server. Next, move on to [Static Files](./002-static-files) to learn how to serve HTML, CSS, and images. # Static Files Source: https://docs.go-mizu.dev/examples/002-static-files Learn how to serve static HTML, CSS, and JS files. In this lesson, you will learn how to serve HTML, CSS, JavaScript, and image files using Mizu. Static files make your web pages come alive with layout, color, and style. Mizu makes this simple with the `Static` method, which connects a URL path to a folder of files that your browser can load directly. Serving static files is a common part of web development. It lets you show pages and assets without writing routes for each file. ### Code Create a file named `main.go`: ```go theme={null} package main import ( "log" "net/http" "github.com/go-mizu/mizu" ) func main() { app := mizu.New() // Serve files from the local "public" directory app.Static("/", http.Dir("./public")) if err := app.Listen(":8080"); err != nil { log.Fatal(err) } } ``` Now create a folder named `public` in the same directory and add a file `index.html`: ```html theme={null} Hello, Mizu

Hello, Mizu!

This page is served directly from the public folder.

``` ### Run In your terminal, run: ``` go run . ``` Then open your browser and go to [http://localhost:8080](http://localhost:8080). You should see your HTML page with **Hello, Mizu!** centered on the screen. ### How it works The `Static` method uses Go's built-in `http.FileServer` to serve files from a folder. When you visit `/`, Mizu looks inside the `./public` folder and finds the file that matches the requested path. Files like `/index.html`, `/style.css`, and `/script.js` are served automatically without defining routes. ### Try something new Add a new file called `script.js` inside your `public` folder: ```js theme={null} console.log("Hello from Mizu static files"); ``` Link it to your HTML file: ```html theme={null} ``` Reload your browser and open the console to see the message. You now know how to serve HTML, CSS, JavaScript, and images directly from your Go app using Mizu. Next, move on to [Routing Basics](./003-routing-basics) to learn how to create multiple pages and routes. # Routing Basics Source: https://docs.go-mizu.dev/examples/003-routing-basics Learn how to define routes and handlers in Mizu. In this lesson, you will learn how routing works in Mizu. Routing controls what happens when someone visits a specific URL. You can define routes for different paths like `/`, `/about`, or `/api`, and Mizu will call the right handler function automatically. Each route uses a plain Go function that takes a `*mizu.Ctx` and returns an error. This keeps your code simple and easy to follow. ### Code Create a file named `main.go` and add this code: ```go theme={null} package main import ( "log" "net/http" "time" "github.com/go-mizu/mizu" ) // Home page handler func home(c *mizu.Ctx) error { return c.Text(http.StatusOK, "Welcome to Mizu!") } // About page handler func about(c *mizu.Ctx) error { return c.Text(http.StatusOK, "This is the About page.") } // Hello page handler with path parameter func hello(c *mizu.Ctx) error { name := c.Param("name") return c.Text(http.StatusOK, "Hello, "+name) } // Time API handler returning JSON func apiTime(c *mizu.Ctx) error { return c.JSON(http.StatusOK, map[string]string{ "time": time.Now().Format(time.RFC3339), }) } func main() { app := mizu.New() // Register routes app.Get("/", home) app.Get("/about", about) app.Get("/hello/{name}", hello) app.Get("/api/time", apiTime) if err := app.Listen(":8080"); err != nil { log.Fatal(err) } } ``` ### Run Open your terminal and run: ``` go run . ``` Then open your browser and visit: * [http://localhost:8080/](http://localhost:8080/) Shows "Welcome to Mizu!" * [http://localhost:8080/about](http://localhost:8080/about) Shows "This is the About page." * [http://localhost:8080/hello/Alice](http://localhost:8080/hello/Alice) Shows "Hello, Alice" * [http://localhost:8080/api/time](http://localhost:8080/api/time) Shows the current time in JSON format. ### How it works Each route is matched to a specific function. For example, `/hello/{name}` includes a placeholder, and Mizu automatically extracts the value so you can read it with `c.Param("name")`. When a request matches a path, Mizu runs the correct handler. Inside your function, you can use `c.Text()` to send text or `c.JSON()` to return structured data. Every handler returns an error so you can add custom error handling later if needed. ### Try something new Add a route that returns a custom status code or HTML content. For example: ```go theme={null} func notFound(c *mizu.Ctx) error { return c.Text(http.StatusNotFound, "Page not found") } ``` Register it like this: ```go theme={null} app.Get("/404", notFound) ``` Then visit [http://localhost:8080/404](http://localhost:8080/404) to test it. Routing is the core of every Mizu app. Once you are comfortable defining routes and handlers, you are ready to build full pages and APIs. Next, continue to [JSON Responses](./004-json-responses) to learn how to send structured data from your routes. # JSON Responses Source: https://docs.go-mizu.dev/examples/004-json-responses Learn how to send JSON data from your routes. In this lesson, you will learn how to send JSON responses using Mizu. JSON is a common format for web apps and APIs to exchange data. Mizu provides a helper method that automatically converts your Go values to JSON and sets the correct headers. You will see how to return structured data from handlers in a clean and simple way. ### Code Create a file named `main.go` and add this code: ```go theme={null} package main import ( "log" "net/http" "time" "github.com/go-mizu/mizu" ) // Welcome handler returning JSON func welcome(c *mizu.Ctx) error { data := map[string]string{ "message": "Welcome to Mizu JSON API", } return c.JSON(http.StatusOK, data) } // Current time handler func currentTime(c *mizu.Ctx) error { data := map[string]any{ "time": time.Now().Format(time.RFC3339), "status": "ok", } return c.JSON(http.StatusOK, data) } // User info handler func userInfo(c *mizu.Ctx) error { user := struct { Name string `json:"name"` Email string `json:"email"` }{ Name: "Alice", Email: "alice@example.com", } return c.JSON(http.StatusOK, user) } func main() { app := mizu.New() app.Get("/", welcome) app.Get("/time", currentTime) app.Get("/user", userInfo) if err := app.Listen(":8080"); err != nil { log.Fatal(err) } } ``` ### Run Run the server: ``` go run . ``` Then open these links in your browser or use `curl`: * [http://localhost:8080/](http://localhost:8080/) Returns: `{"message":"Welcome to Mizu JSON API"}` * [http://localhost:8080/time](http://localhost:8080/time) Returns the current server time and a status field. * [http://localhost:8080/user](http://localhost:8080/user) Returns: `{"name":"Alice","email":"alice@example.com"}` ### How it works The `c.JSON(statusCode, data)` method converts any Go value to JSON, sets the `Content-Type: application/json` header, and sends the response. You can return maps, slices, or structs. Mizu uses Go's built-in JSON encoder, and you can use struct tags to define custom field names. If encoding fails, Mizu sends an error response automatically. This makes it easy to build APIs that return structured data without handling JSON manually. ### Try something new Add another route that returns a list of posts: ```go theme={null} func listPosts(c *mizu.Ctx) error { posts := []map[string]any{ {"id": 1, "title": "First Post"}, {"id": 2, "title": "Learning Mizu"}, } return c.JSON(http.StatusOK, posts) } ``` Then register it in `main()`: ```go theme={null} app.Get("/posts", listPosts) ``` Visit [http://localhost:8080/posts](http://localhost:8080/posts) to see the JSON array. Now you can send structured JSON data from any route with just one line of code. Next, continue to [Context and Lifecycle](./005-context-lifecycle) to learn how Mizu manages requests, deadlines, and cleanup. # Context and Lifecycle Source: https://docs.go-mizu.dev/examples/005-context-lifecycle Learn how to handle timeouts and cancellations safely. In this lesson, you will learn how Mizu manages the lifecycle of an HTTP request. Every request in Mizu has its own context, which lets you handle timeouts, cancellations, and background operations safely. This helps you write clean, responsive handlers that stop when the client disconnects or when the server shuts down. ### Code Create a file named `main.go` and add this code: ```go theme={null} package main import ( "context" "log" "net/http" "time" "github.com/go-mizu/mizu" ) // Slow handler that respects the request context func slow(c *mizu.Ctx) error { select { case <-time.After(3 * time.Second): return c.Text(http.StatusOK, "Finished after 3 seconds") case <-c.Context().Done(): // Context canceled (client disconnected or timeout reached) return c.Text(499, "Request cancelled") } } // Fast handler that uses a manual timeout func timed(c *mizu.Ctx) error { ctx, cancel := context.WithTimeout(c.Context(), 2*time.Second) defer cancel() select { case <-time.After(1 * time.Second): return c.Text(http.StatusOK, "Completed within timeout") case <-ctx.Done(): return c.Text(http.StatusGatewayTimeout, "Operation timed out") } } func main() { app := mizu.New() app.Get("/slow", slow) app.Get("/timed", timed) if err := app.Listen(":8080"); err != nil { log.Fatal(err) } } ``` ### Run Run the server with: ``` go run . ``` Then visit these URLs in your browser or use `curl`: * [http://localhost:8080/slow](http://localhost:8080/slow) Wait a few seconds to see the message "Finished after 3 seconds". If you stop loading early, the handler detects it and stops. * [http://localhost:8080/timed](http://localhost:8080/timed) Returns quickly with "Completed within timeout". If you increase the delay past 2 seconds, it will show "Operation timed out". ### How it works Each request in Go includes a `context.Context` that ends when the request finishes. Mizu provides `c.Context()` to access it easily. You can use it to cancel work early, set timeouts, or handle cleanup when the request is done. In the `/slow` example, the handler simulates a long operation. If the user closes the browser or the server shuts down, the context is canceled and your code exits early. In `/timed`, you create your own timeout with `context.WithTimeout`, giving full control over how long the handler should run. This approach is important for tasks like database queries, API calls, or background jobs where you should stop safely when the request ends. ### Try something new Change the values inside `time.After()` or `context.WithTimeout()` to see how timeouts behave. You can also print `ctx.Err()` to understand why the context ended. Now you know how Mizu's context system helps manage request lifecycles in a simple, predictable way. Next, continue to [Query, Form, and Path](./006-query-form-path) to learn how to read data from incoming requests. # Query, Form, and Path Parameters Source: https://docs.go-mizu.dev/examples/006-query-form-path Learn how to read query, form, and path data. # Query, Form, and Path In this lesson, you will learn how to read data sent by users through query strings, form submissions, and path parameters. These are the main ways a client can send information to your server. Mizu provides simple helper methods on `Ctx` that make handlers easy to read and maintain. ### Code Create a file named `main.go` and add this code: ```go theme={null} package main import ( "log" "net/http" "github.com/go-mizu/mizu" ) // Handle query parameters like /search?q=go func search(c *mizu.Ctx) error { query := c.Query("q") if query == "" { return c.Text(http.StatusBadRequest, "Missing query parameter 'q'") } return c.Text(http.StatusOK, "You searched for: "+query) } // Handle form submissions from HTML forms func submit(c *mizu.Ctx) error { form, err := c.Form() if err != nil { return c.Text(http.StatusBadRequest, "Invalid form data") } name := form.Get("name") email := form.Get("email") if name == "" || email == "" { return c.Text(http.StatusBadRequest, "Both name and email are required") } return c.Text(http.StatusOK, "Form submitted by "+name+" ("+email+")") } // Handle dynamic paths like /hello/alice func greet(c *mizu.Ctx) error { name := c.Param("name") if name == "" { return c.Text(http.StatusBadRequest, "Name is required") } return c.Text(http.StatusOK, "Hello, "+name) } func main() { app := mizu.New() app.Get("/search", search) app.Post("/submit", submit) app.Get("/hello/{name}", greet) if err := app.Listen(":8080"); err != nil { log.Fatal(err) } } ``` ### Run Start your server with: ``` go run . ``` Then test the routes: * Visit [http://localhost:8080/search?q=mizu](http://localhost:8080/search?q=mizu) Response: `You searched for: mizu` * Send a form using curl: ``` curl -X POST -d "name=Alice&email=alice@example.com" http://localhost:8080/submit ``` Response: `Form submitted by Alice (alice@example.com)` * Visit [http://localhost:8080/hello/Bob](http://localhost:8080/hello/Bob) Response: `Hello, Bob` ### How it works Mizu’s `Ctx` helpers wrap parts of Go’s `http.Request` to make input handling simpler. | Method | Reads from | Example | | ----------------- | --------------- | ------------------------------------ | | `c.Query("key")` | Query string | `/search?q=mizu` | | `c.Form()` | Form body | `name=Alice&email=alice@example.com` | | `c.Param("name")` | Path parameters | `/hello/Alice` with `/hello/{name}` | * `Query` reads values from the URL. * `Form` parses key-value pairs from POST data. * `Param` extracts variables from the route path. Each method returns safe, plain strings. If a key is missing, you get an empty string. If parsing fails, `c.Form()` returns an error you can check. These helpers keep your code short and close to Go’s `net/http` design. ### Try something new You can add a small HTML form to test the `/submit` route. Create a file named `form.html`: ```html theme={null} Submit Example

Submit Form





``` Serve it with Mizu’s static file helper, then open it in your browser and submit the form. You will see the response from your `submit` handler. Now you know how to read data from query strings, forms, and path parameters in Mizu. Next, continue to [Cookies](./007-cookies) to learn how to store small pieces of data between requests. # Cookies Source: https://docs.go-mizu.dev/examples/007-cookies Learn how to manage cookies data. In this lesson, you will learn how to use cookies in Mizu to store and read small pieces of data between requests.
Cookies are a simple way to remember users, track sessions, or save preferences like language or theme. Mizu provides two helpers, `Ctx.Cookie()` and `Ctx.SetCookie()`, that make cookie handling easy. ### Code Create a file named `main.go` and add this code: ```go theme={null} package main import ( "log" "net/http" "time" "github.com/go-mizu/mizu" ) // Set a cookie named "username" func setCookie(c *mizu.Ctx) error { name := c.Query("name") if name == "" { name = "Guest" } c.SetCookie(&http.Cookie{ Name: "username", Value: name, Path: "/", HttpOnly: true, Expires: time.Now().Add(24 * time.Hour), }) return c.Text(http.StatusOK, "Cookie set for "+name) } // Read a cookie named "username" func getCookie(c *mizu.Ctx) error { cookie, err := c.Cookie("username") if err != nil { return c.Text(http.StatusNotFound, "No cookie found") } return c.Text(http.StatusOK, "Welcome back, "+cookie.Value) } // Delete the cookie by setting expiration to the past func clearCookie(c *mizu.Ctx) error { c.SetCookie(&http.Cookie{ Name: "username", Value: "", Path: "/", Expires: time.Unix(0, 0), MaxAge: -1, }) return c.Text(http.StatusOK, "Cookie cleared") } func main() { app := mizu.New() app.Get("/set", setCookie) app.Get("/get", getCookie) app.Get("/clear", clearCookie) if err := app.Listen(":8080"); err != nil { log.Fatal(err) } } ``` ### Run Start the server: ``` go run . ``` Then try the following examples: #### Set a cookie Visit [http://localhost:8080/set?name=Alice](http://localhost:8080/set?name=Alice) Response: `Cookie set for Alice` Your browser now stores a cookie named `username`. #### Get the cookie Visit [http://localhost:8080/get](http://localhost:8080/get) Response: `Welcome back, Alice` The value comes from the cookie you set earlier. #### Clear the cookie Visit [http://localhost:8080/clear](http://localhost:8080/clear) Response: `Cookie cleared` The browser cookie is now deleted. ### How it works | Method | Purpose | Example | | ------------------------------ | --------------------------------- | ------------------------------------------------------------------ | | `c.SetCookie(ck *http.Cookie)` | Sends a cookie to the client | Sets name, value, expiry, and options like `HttpOnly` or `Secure`. | | `c.Cookie("name")` | Reads a cookie sent by the client | Returns a `*http.Cookie` and an error if not found. | * Setting a cookie adds a `Set-Cookie` header to the HTTP response. * Reading a cookie checks the request headers for the matching cookie name. * Deleting a cookie works by sending a new cookie with an empty value and an expiration date in the past. This approach lets you manage sessions or preferences without needing a database or external storage. ### Try something new Add a handler that counts how many times a user has visited your site: ```go theme={null} func visitCount(c *mizu.Ctx) error { ck, _ := c.Cookie("visits") count := 0 if ck != nil { fmt.Sscanf(ck.Value, "%d", &count) } count++ c.SetCookie(&http.Cookie{ Name: "visits", Value: fmt.Sprintf("%d", count), Path: "/", Expires: time.Now().Add(24 * time.Hour), }) return c.Text(http.StatusOK, fmt.Sprintf("You have visited %d times", count)) } ``` Register it with `app.Get("/visits", visitCount)` and refresh the page several times. The count will increase with each visit. Cookies form the basis for session handling, login tracking, and simple user personalization. Next, continue to [Middleware Basics](./008-middleware-basics) to learn how to wrap your handlers with reusable logic like logging, timing, and authentication. # Middleware Basics Source: https://docs.go-mizu.dev/examples/008-middleware-basics Learn how to create and use middleware functions. In this lesson, you will learn what middleware is and how to use it in Mizu. Middleware is a function that runs before or after your main handler. It can log requests, check authentication, handle errors, or modify responses, all without changing your route logic. Mizu middleware follows Go’s `net/http` design, so you can use standard handlers or write your own in a simple, composable way. ### Code Create a file named `main.go` and add this code: ```go theme={null} package main import ( "log" "net/http" "time" "github.com/go-mizu/mizu" ) // Simple logging middleware func logger(next mizu.Handler) mizu.Handler { return func(c *mizu.Ctx) error { start := time.Now() err := next(c) c.Logger().Info("request completed", "method", c.Request().Method, "path", c.Request().URL.Path, "status", c.StatusCode(), "duration", time.Since(start), ) return err } } // Authentication middleware example func auth(next mizu.Handler) mizu.Handler { return func(c *mizu.Ctx) error { token := c.Query("token") if token != "secret" { return c.Text(http.StatusUnauthorized, "Unauthorized") } return next(c) } } // Protected route func dashboard(c *mizu.Ctx) error { return c.Text(http.StatusOK, "Welcome to your dashboard") } // Public route func home(c *mizu.Ctx) error { return c.Text(http.StatusOK, "Home page - no auth required") } func main() { app := mizu.New() // Global middleware app.Use(logger) // Public route app.Get("/", home) // Protected route with route-specific middleware app.With(auth).Get("/dashboard", dashboard) if err := app.Listen(":8080"); err != nil { log.Fatal(err) } } ``` ### Run Start your server: ``` go run . ``` Then open your browser and try these routes: * [http://localhost:8080/](http://localhost:8080/) Response: `Home page - no auth required` The logger middleware runs for this route and prints something like: `INFO request completed method=GET path=/ status=200 duration=1.2ms` * [http://localhost:8080/dashboard](http://localhost:8080/dashboard) Response: `Unauthorized` * Add a token to simulate an authenticated request: [http://localhost:8080/dashboard?token=secret](http://localhost:8080/dashboard?token=secret) Response: `Welcome to your dashboard` ### How it works Middleware in Mizu is a function that takes a `mizu.Handler` and returns another `mizu.Handler`. Each middleware can decide whether to continue to the next handler or stop early. | Type of Middleware | Description | Example | | ------------------------- | ------------------------------------------------- | --------------------------------------------- | | Global middleware | Runs for every route, used for logging or errors. | `app.Use(logger)` | | Route-specific middleware | Runs only on certain routes. | `app.With(auth).Get("/dashboard", dashboard)` | | Middleware chain | Wraps multiple layers around your handler. | Logging → Auth → Handler | Every middleware gets the same `*mizu.Ctx` object and can read or modify the request and response. The `next(c)` call runs the next function in the chain. ### Try something new Add a timing middleware to measure how long each request takes: ```go theme={null} func timing(next mizu.Handler) mizu.Handler { return func(c *mizu.Ctx) error { start := time.Now() err := next(c) elapsed := time.Since(start) return c.Text(http.StatusOK, "Took "+elapsed.String()) } } ``` Apply it to a specific route: ```go theme={null} app.With(timing).Get("/slow", func(c *mizu.Ctx) error { time.Sleep(2 * time.Second) return c.Text(http.StatusOK, "Finished slow task") }) ``` Middleware helps you keep your handlers clean while layering shared logic such as authentication, logging, metrics, and caching. Next, continue to [Error Handling and Recover](./009-error-handling-recover) to learn how to handle panics and send friendly error messages. # Error Handling and Recover Source: https://docs.go-mizu.dev/examples/009-error-handling-recover Learn how to handle and recover from errors. # Error Handling and Recover In this lesson, you will learn how Mizu automatically handles errors and panics, and how you can define a custom global error handler using `app.ErrorHandler`. Mizu recovers from panics internally, logs errors, and keeps your app running without crashing. You only need to register an error handler if you want to customize how errors are shown to users. ### Code Create a file named `main.go` and add this code: ```go theme={null} package main import ( "errors" "log" "net/http" "strings" "github.com/go-mizu/mizu" ) // Custom error handler for all routes func handleError(c *mizu.Ctx, err error) { c.Logger().Error("handler error", "error", err) if strings.Contains(err.Error(), "not found") { _ = c.Text(http.StatusNotFound, "Page not found") return } _ = c.Text(http.StatusInternalServerError, "Something went wrong: "+err.Error()) } // Handler that returns an error func mightFail(c *mizu.Ctx) error { return errors.New("something went wrong") } // Handler that panics func panicHandler(c *mizu.Ctx) error { panic("unexpected panic occurred") } // Normal route func home(c *mizu.Ctx) error { return c.Text(http.StatusOK, "Home page - all good") } func main() { app := mizu.New() // Set global error handler app.ErrorHandler(handleError) app.Get("/", home) app.Get("/fail", mightFail) app.Get("/panic", panicHandler) if err := app.Listen(":8080"); err != nil { log.Fatal(err) } } ``` ### Run Start the server: ``` go run . ``` Then open these routes: * [http://localhost:8080/](http://localhost:8080/)
Response: `Home page - all good` * [http://localhost:8080/fail](http://localhost:8080/fail)
Response: `Something went wrong: something went wrong` The error is returned by the handler and handled by your custom function. * [http://localhost:8080/panic](http://localhost:8080/panic)
Response: `Something went wrong: unexpected panic occurred` The panic is recovered automatically by Mizu and passed to your error handler. ### How it works Mizu wraps every handler with built-in recovery logic. When a handler returns an error or panics: 1. Mizu catches the error or recovered panic. 2. The error (or `*mizu.PanicError` for panics) is sent to your global error handler. 3. If no handler is defined, Mizu logs the issue and returns a plain 500 Internal Server Error. You can inspect the error type to customize responses. For example, to treat panics differently: ```go theme={null} func handleError(c *mizu.Ctx, err error) { switch err.(type) { case *mizu.PanicError: _ = c.Text(http.StatusInternalServerError, "Internal Server Error") default: _ = c.Text(http.StatusBadRequest, "Error: "+err.Error()) } } ``` This gives you full control over how your app reports problems while Mizu ensures that it never crashes unexpectedly. Continue to the next lesson: [Logging with slog](./010-logging-with-slog) to learn how to add structured, colorful, and context-aware logs to your Mizu app. # Logging with slog Source: https://docs.go-mizu.dev/examples/010-logging-with-slog Learn how to add structured logging with slog. # Logging with slog In this lesson, you will learn how to add structured logging to your Mizu applications using Go’s built-in `log/slog` package. Logs help you see what your app is doing, measure how long things take, and understand when something goes wrong. Mizu already includes a built-in request logger, so every request is automatically logged in a human-readable format by default. You can customize the log output, format, and level using your own `slog` setup. ### Code Create a file named `main.go` and add this code: ```go theme={null} package main import ( "fmt" "log/slog" "os" "time" "github.com/go-mizu/mizu" ) // Example route that adds custom log entries func home(c *mizu.Ctx) error { c.Logger().Info("user visited home", "method", c.Request().Method, "path", c.Request().URL.Path, "client_ip", c.ClientIP(), ) return c.Text(200, "Home page") } // Simulate a slow route func slow(c *mizu.Ctx) error { start := time.Now() time.Sleep(2 * time.Second) elapsed := time.Since(start) c.Logger().Info("slow request finished", "path", c.Request().URL.Path, "duration", elapsed.String(), ) return c.Text(200, "Finished slow operation") } // Simulate an error and log the details func fail(c *mizu.Ctx) error { err := doSomething() if err != nil { c.Logger().Error("operation failed", "error", err, "user_agent", c.Request().UserAgent(), "client_ip", c.ClientIP(), ) return c.Text(500, "Something went wrong") } return c.Text(200, "Success") } func doSomething() error { return fmt.Errorf("database connection lost") } func main() { app := mizu.New() // Mizu logs in plain text by default. // You can attach your own structured logger if you want. handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}) logger := slog.New(handler) app.SetLogger(logger) // Mizu automatically logs every request, // so you do not need to add another logging middleware. app.Get("/", home) app.Get("/slow", slow) app.Get("/fail", fail) if err := app.Listen(":8080"); err != nil { logger.Error("server failed", "error", err) } } ``` ### Run Start the server: ``` go run . ``` Then open these routes in your browser: * [http://localhost:8080/](http://localhost:8080/)
You will see: `Home page` And in your terminal: ``` INFO request completed method=GET path=/ status=200 duration=1.3ms INFO user visited home method=GET path=/ client_ip=127.0.0.1 ``` * [http://localhost:8080/slow](http://localhost:8080/slow)
Wait a few seconds, then you will see: `Finished slow operation` And in your terminal: ``` INFO request completed method=GET path=/slow status=200 duration=2.001s INFO slow request finished path=/slow duration=2.001s ``` * [http://localhost:8080/fail](http://localhost:8080/fail)
You will see: `Something went wrong` And in your terminal: ``` ERROR operation failed error=database connection lost user_agent=curl/8.1.0 client_ip=127.0.0.1 ``` By default, Mizu logs all requests using a readable text format. If you run your app in a terminal, it will even use color output if your environment supports it. ### How it works Mizu automatically adds a request logger middleware when your app starts. It logs details such as: * HTTP method * Path * Status code * Duration * Any errors returned by handlers When you set a custom `slog.Logger` using `app.SetLogger()`, all logs from Mizu and your handlers use that logger instance. | Feature | What it does | Example | | ----------------- | ------------------------------------ | -------------------------------------------------------- | | Built-in logs | Logs every request automatically | `INFO request completed method=GET path=/` | | `app.SetLogger()` | Use your own `slog` configuration | `app.SetLogger(logger)` | | `c.Logger()` | Log custom messages inside a handler | `c.Logger().Info("event", "path", c.Request().URL.Path)` | | `c.ClientIP()` | Get the visitor's IP for logs | Useful for tracking and debugging | ### Try something new You can switch to JSON output for production environments: ```go theme={null} handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}) logger := slog.New(handler) ``` That will print structured logs like this: ```json theme={null} { "time": "2025-11-04T10:30:00Z", "level": "INFO", "msg": "request completed", "method": "GET", "path": "/slow", "status": 200, "duration": "2.001s" } ``` You can also set `LevelDebug` to get more detailed logs during development. Logging gives you a clear picture of what your app is doing, helps you understand user behavior, and makes it easy to spot and fix problems. Continue to the next lesson: [Request IDs and Correlation](./011-request-ids-correlation) to learn how to attach unique IDs to each request for tracing across logs and services. # Overview Source: https://docs.go-mizu.dev/examples/overview Learn Mizu through practical, runnable examples. Each example focuses on one concept and includes complete, working code. All examples are available at [github.com/go-mizu/examples](https://github.com/go-mizu/examples): ``` git clone https://github.com/go-mizu/examples cd examples go run ./001-hello-mizu ``` ## Getting Started These examples cover the fundamentals of building web applications with Mizu: 1. [Hello, Mizu](./001-hello-mizu) - Your first web server 2. [Static Files](./002-static-files) - Serve HTML, CSS, and JavaScript 3. [Routing Basics](./003-routing-basics) - Define routes and handlers 4. [JSON Responses](./004-json-responses) - Return structured data 5. [Context and Lifecycle](./005-context-lifecycle) - Handle timeouts and cancellation 6. [Query, Form, and Path Parameters](./006-query-form-path) - Read request data 7. [Cookies](./007-cookies) - Store data between requests 8. [Middleware Basics](./008-middleware-basics) - Add logging, auth, and shared logic 9. [Error Handling and Recover](./009-error-handling-recover) - Handle errors and panics 10. [Logging with slog](./010-logging-with-slog) - Structured request logging ## What you will learn By working through these examples, you will understand how to: * Create routes and handlers * Read data from requests (path, query, form, JSON) * Send responses (text, JSON, HTML, files) * Use middleware for cross-cutting concerns * Handle errors consistently * Log requests and application events Start with [Hello, Mizu](./001-hello-mizu) and work through each example in order. # Framework Adapters Source: https://docs.go-mizu.dev/frontend/adapters Framework-specific adapters with optimized defaults. Framework adapters provide framework-specific defaults and optimizations for common frontend frameworks. ## Overview Adapters are convenience wrappers that apply framework-specific best practices: ```go theme={null} import "github.com/go-mizu/mizu/frontend/adapters" // Instead of manual configuration app.Use(adapters.React(frontend.Options{ Root: "./dist", })) ``` ## Available Adapters ### React ```go theme={null} func React(opts frontend.Options) mizu.Middleware ``` Optimized for React applications with Vite. **Defaults:** * Manifest: `.vite/manifest.json` * Output directory: `dist` **Example:** ```go theme={null} import "github.com/go-mizu/mizu/frontend/adapters" app.Use(adapters.React(frontend.Options{ Root: "./dist", })) ``` ### Vue ```go theme={null} func Vue(opts frontend.Options) mizu.Middleware ``` Optimized for Vue 3 applications with Vite. **Defaults:** * Manifest: `.vite/manifest.json` * Output directory: `dist` **Example:** ```go theme={null} app.Use(adapters.Vue(frontend.Options{ Root: "./dist", })) ``` ### Svelte ```go theme={null} func Svelte(opts frontend.Options) mizu.Middleware ``` Optimized for Svelte applications with Vite. **Defaults:** * Manifest: `.vite/manifest.json` * Output directory: `dist` **Example:** ```go theme={null} app.Use(adapters.Svelte(frontend.Options{ Root: "./dist", })) ``` ### SvelteKit ```go theme={null} func SvelteKit(opts frontend.Options) mizu.Middleware ``` Optimized for SvelteKit with static adapter. **Defaults:** * Output directory: `build` * Index: `index.html` **Example:** ```go theme={null} app.Use(adapters.SvelteKit(frontend.Options{ Root: "./build", })) ``` ### Solid ```go theme={null} func Solid(opts frontend.Options) mizu.Middleware ``` Optimized for Solid.js with Vite. **Defaults:** * Manifest: `.vite/manifest.json` * Output directory: `dist` **Example:** ```go theme={null} app.Use(adapters.Solid(frontend.Options{ Root: "./dist", })) ``` ### Astro ```go theme={null} func Astro(opts frontend.Options) mizu.Middleware ``` Optimized for Astro with static output. **Defaults:** * Output directory: `dist` * Index: `index.html` **Example:** ```go theme={null} app.Use(adapters.Astro(frontend.Options{ Root: "./dist", })) ``` ### Angular ```go theme={null} func Angular(opts frontend.Options) mizu.Middleware ``` Optimized for Angular applications. **Defaults:** * Output directory: `dist/app-name/browser` * Index: `index.html` **Example:** ```go theme={null} app.Use(adapters.Angular(frontend.Options{ Root: "./dist/my-app/browser", })) ``` ## Using Adapters ### Basic Usage ```go theme={null} package server import ( "github.com/go-mizu/mizu" "github.com/go-mizu/mizu/frontend" "github.com/go-mizu/mizu/frontend/adapters" ) func New() *mizu.App { app := mizu.New() // API routes app.Get("/api/users", handleUsers) // Frontend with adapter app.Use(adapters.React(frontend.Options{ Mode: frontend.ModeAuto, Root: "./dist", DevServer: "http://localhost:5173", IgnorePaths: []string{"/api"}, })) return app } ``` ### With Embedded FS ```go theme={null} //go:embed all:dist var distFS embed.FS func New() *mizu.App { app := mizu.New() dist, _ := fs.Sub(distFS, "dist") app.Use(adapters.Vue(frontend.Options{ FS: dist, DevServer: "http://localhost:5173", })) return app } ``` ### Custom Configuration Adapters still accept all standard options: ```go theme={null} app.Use(adapters.React(frontend.Options{ Mode: frontend.ModeProduction, Root: "./dist", Prefix: "/app", InjectEnv: []string{"API_URL"}, CacheControl: frontend.CacheConfig{ HashedAssets: 180 * 24 * time.Hour, }, })) ``` ## When to Use Adapters **Use adapters when:** * Using standard framework setup * Want framework-specific optimizations * Prefer convention over configuration **Use `frontend.WithOptions` when:** * Custom build configuration * Non-standard directory structure * Need full control over options ## Creating Custom Adapters You can create your own adapters: ```go theme={null} package myadapters import ( "github.com/go-mizu/mizu" "github.com/go-mizu/mizu/frontend" ) func Qwik(opts frontend.Options) mizu.Middleware { // Apply Qwik-specific defaults if opts.Root == "" { opts.Root = "./dist" } if opts.Index == "" { opts.Index = "index.html" } if opts.Manifest == "" { opts.Manifest = "q-manifest.json" } return frontend.WithOptions(opts) } ``` Use: ```go theme={null} import "yourmodule/myadapters" app.Use(myadapters.Qwik(frontend.Options{ Root: "./dist", })) ``` ## Adapter Comparison | Adapter | Build Tool | Output Dir | Manifest | Notes | | --------- | ----------- | ------------------ | --------------------- | ------------------- | | React | Vite | `dist` | `.vite/manifest.json` | Standard Vite setup | | Vue | Vite | `dist` | `.vite/manifest.json` | Standard Vite setup | | Svelte | Vite | `dist` | `.vite/manifest.json` | Standard Vite setup | | SvelteKit | SvelteKit | `build` | None | Static adapter | | Angular | Angular CLI | `dist/app/browser` | None | Production build | | Solid | Vite | `dist` | `.vite/manifest.json` | Standard Vite setup | | Astro | Astro | `dist` | None | Static output | ## Next Steps Complete API documentation All configuration options Framework-specific guides # Alpine Source: https://docs.go-mizu.dev/frontend/alpine Add lightweight interactivity with Alpine.js and server-rendered HTML. Alpine.js is a minimal JavaScript framework for adding interactivity to server-rendered HTML. Think of it as "jQuery for the modern web" or "Tailwind for JavaScript." ## Comparison with Other Approaches | Feature | Alpine | HTMX | Vue 3 | React | | --------------------------- | ------------------- | --------------- | ---------------- | ------------- | | **Bundle Size** | \~15 KB | \~14 KB | \~34 KB | \~44 KB | | **JavaScript Required** | Light | Minimal | Moderate | Heavy | | **Reactivity** | Proxy-based | None | Proxy-based | Virtual DOM | | **Build Step** | None | None | Optional | Required | | **Learning Curve** | Gentle | Gentle | Moderate | Steep | | **Server Dependency** | Medium | High | Low | Low | | **SEO** | Good | Excellent | Needs SSR | Needs SSR | | **Progressive Enhancement** | Good | Excellent | Poor | Poor | | **Best For** | Interactive widgets | CRUD apps | Interactive apps | Complex UIs | | **State Management** | Local + Stores | Server-side | Vuex/Pinia | Redux/Context | | **Syntax** | HTML attributes | HTML attributes | Template syntax | JSX | ## Quick Start ```bash theme={null} mizu new ./my-alpine-app --template frontend/alpine cd my-alpine-app make dev ``` ## Why Alpine.js? ### The Sweet Spot Alpine occupies the perfect middle ground between vanilla JavaScript and heavy frameworks. It gives you reactive components without the build step or complexity. **Vanilla JS:** ```js theme={null} document.getElementById('count').textContent = count++ ``` **Alpine:** ```html theme={null}
``` **React (requires build step):** ```jsx theme={null} const [count, setCount] = useState(0) return (
{count}
) ``` ### Key Benefits * **Tiny**: \~15kB minified and gzipped * **No Build Step**: Include via CDN and start using * **Declarative**: Write reactive code in HTML * **Progressive**: Add to existing HTML pages * **Familiar**: Syntax inspired by Vue and Angular * **Composable**: Works great with HTMX and other libraries * **Powerful**: Includes directives, magic properties, and plugins ### When Alpine Shines Alpine is perfect when you want: * Interactive widgets without a build step * Dropdowns, modals, tabs, accordions * Form validation and dynamic inputs * Progressive enhancement of server-rendered pages * To complement HTMX for client-side interactivity * Rapid development with minimal JavaScript ## Installation ### Via CDN ```html theme={null} ``` Or use a specific version: ```html theme={null} ``` The `defer` attribute ensures Alpine loads after the DOM is ready. ### Via npm ```bash theme={null} npm install alpinejs ``` Then import and initialize: ```js theme={null} import Alpine from 'alpinejs' window.Alpine = Alpine Alpine.start() ``` ### With a Bundler ```js theme={null} import Alpine from 'alpinejs' import collapse from '@alpinejs/collapse' import persist from '@alpinejs/persist' // Register plugins Alpine.plugin(collapse) Alpine.plugin(persist) // Start Alpine window.Alpine = Alpine Alpine.start() ``` ## Architecture ### Development Mode ``` ┌─────────────────────────────────────────────────────────────┐ │ Browser (Port 3000) │ │ │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ Server-Rendered HTML │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ Alpine.js initializes and attaches reactivity │ │ │ └────────────────────────────────────────────────────────┘ │ │ ▲ │ │ │ │ │ │ Alpine.js (via CDN) │ │ │ │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ Static Assets │ │ │ │ CSS, Images, Alpine.js library │ │ │ └────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Mizu Backend (Go Server) │ │ │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ Router │ │ │ │ │ │ │ │ GET / → Render full HTML page │ │ │ │ GET /api/users → Return JSON data │ │ │ │ POST /api/users → Process + return JSON │ │ │ └────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ View Engine │ │ │ │ │ │ │ │ • Templates (Go html/template) │ │ │ │ • Layouts (default.html) │ │ │ │ • Pages with Alpine directives │ │ │ │ • Hot reload in dev mode │ │ │ └────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ Business Logic │ │ │ │ │ │ │ │ • API Handlers (JSON responses) │ │ │ │ • Services (UserService, AuthService) │ │ │ │ • Database access │ │ │ └────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ Database │ │ │ │ PostgreSQL, SQLite, etc. │ │ │ └────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ Data Flow: 1. Server renders initial HTML with Alpine directives 2. Browser loads Alpine.js from CDN 3. Alpine initializes components (x-data elements) 4. User interactions trigger Alpine reactivity 5. For server data, Alpine fetches JSON via fetch/axios 6. Alpine updates DOM reactively based on state changes ``` ### Production Mode ``` ┌─────────────────────────────────────────────────────────────┐ │ CDN / Edge │ │ │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ Static Assets (Cached) │ │ │ │ • CSS, Images │ │ │ │ • Alpine.js library (cached at edge) │ │ │ │ • Versioned and fingerprinted │ │ │ └────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Load Balancer │ └─────────────────────────────────────────────────────────────┘ │ ┌───────────────┼───────────────┐ ▼ ▼ ▼ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ Mizu Server 1│ │ Mizu Server 2│ │ Mizu Server 3│ │ │ │ │ │ │ │ Single Binary│ │ Single Binary│ │ Single Binary│ │ • Templates │ │ • Templates │ │ • Templates │ │ • API Routes │ │ • API Routes │ │ • API Routes │ │ • JSON │ │ • JSON │ │ • JSON │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ │ └───────────────┼───────────────┘ ▼ ┌──────────────────┐ │ Database │ │ (with pooling) │ └──────────────────┘ ``` ## Core Directives ### x-data Declares a new Alpine component with reactive state: ```html theme={null}
Content

``` ### x-show Toggle visibility with CSS `display`: ```html theme={null}
I'm visible!
I fade in and out
``` ### x-if Conditionally add/remove element from DOM: ```html theme={null}
``` **x-show vs x-if:** * `x-show`: Fast toggle, element stays in DOM, uses CSS `display` * `x-if`: Slower, removes from DOM, better for heavy components ### x-for Loop over arrays: ```html theme={null}
``` ### x-model Two-way data binding: ```html theme={null}

You typed:

Age:

Thank you!

Selected:

Selected:

Country:

``` ### x-on (@) Event listeners: ```html theme={null}
Hover or click
Click outside to close
Press ESC to close
Scroll content
``` ### x-bind (:) Bind attributes: ```html theme={null}
Status
Multi-class
Styled
Link
``` ### x-text Set text content: ```html theme={null}

``` ### x-html Set HTML content (use with caution): ```html theme={null}
``` **Warning**: Only use `x-html` with trusted content to avoid XSS attacks. ### x-init Run code when component initializes: ```html theme={null}

``` ### x-effect Re-run code when dependencies change: ```html theme={null}

``` ### x-cloak Hide element until Alpine initializes: ```html theme={null}

``` Prevents flash of unstyled content (FOUC). ### x-ignore Prevent Alpine from initializing: ```html theme={null}

``` ## Magic Properties ### \$el Reference the current element: ```html theme={null}
Log this element
``` ### \$refs Reference elements marked with `x-ref`: ```html theme={null}
``` ### \$watch Watch for property changes: ```html theme={null}

``` ### \$dispatch Dispatch custom events: ```html theme={null}
Dispatch event
Listening...
Listener
``` ### \$nextTick Wait for DOM updates: ```html theme={null}
``` ### \$root Reference root element of component: ```html theme={null}
``` ### \$data Reference component data: ```html theme={null}
``` ### \$id Generate unique IDs: ```html theme={null}
``` ### \$store Access global stores (covered in Stores section). ## Advanced Patterns ### Reusable Components Extract components into functions: ```js theme={null} document.addEventListener('alpine:init', () => { Alpine.data('dropdown', () => ({ open: false, toggle() { this.open = !this.open }, close() { this.open = false } })) Alpine.data('counter', (initial = 0) => ({ count: initial, increment() { this.count++ }, decrement() { this.count-- } })) }) ``` Use in HTML: ```html theme={null}
``` ### Stores (Global State) Create global reactive stores: ```js theme={null} document.addEventListener('alpine:init', () => { Alpine.store('auth', { user: null, loggedIn: false, login(user) { this.user = user this.loggedIn = true }, logout() { this.user = null this.loggedIn = false } }) Alpine.store('cart', { items: [], add(item) { this.items.push(item) }, remove(index) { this.items.splice(index, 1) }, get total() { return this.items.reduce((sum, item) => sum + item.price, 0) } }) }) ``` Access in components: ```html theme={null}

Items:

Total: $

``` ### Transitions Built-in transition directives: ```html theme={null}
Fades in and out
Slower fade
Fast in, slow out
Scale transition
``` ### Fetching Data ```html theme={null}
Loading...
``` ### Form Handling ```html theme={null}

Message sent!

``` ## Common Components ### Dropdown Menu ```html theme={null} ``` ### Modal Dialog ```html theme={null}

Modal Title

Modal content goes here.

``` ### Tabs ```html theme={null}
Tab 1 content
Tab 2 content
Tab 3 content
``` ### Accordion ```html theme={null}
Content for item 1
Content for item 2
``` ### Tooltip ```html theme={null}
Tooltip text
``` ### Notification Toast ```html theme={null}
``` ### Autocomplete ```html theme={null}
``` ## With Mizu Backend ### API Integration Backend handler: ```go theme={null} func handleUsers(c *mizu.Ctx) error { users := []map[string]any{ {"id": 1, "name": "Alice", "email": "alice@example.com"}, {"id": 2, "name": "Bob", "email": "bob@example.com"}, } return c.JSON(200, users) } func handleCreateUser(c *mizu.Ctx) error { var user struct { Name string `json:"name"` Email string `json:"email"` } if err := c.BindJSON(&user); err != nil { return c.JSON(400, map[string]string{"error": "Invalid request"}) } // Validate if user.Name == "" || user.Email == "" { return c.JSON(400, map[string]string{"error": "Name and email required"}) } // Create user... newUser := map[string]any{ "id": 3, "name": user.Name, "email": user.Email, } return c.JSON(201, newUser) } ``` Alpine frontend: ```html theme={null}
Loading...
``` ## With HTMX Alpine and HTMX work great together. Use HTMX for server interactions and Alpine for client-side UI state: ```html theme={null}
Loading users...
Alice
``` ## Alpine Plugins ### Persist Persist state to localStorage: ```html theme={null}

Hello,

``` Values persist across page reloads. ### Collapse Smooth height transitions: ```html theme={null}

Content that expands and collapses smoothly

``` ### Focus Manage focus within elements: ```html theme={null}
``` ### Intersect Trigger when element enters viewport: ```html theme={null}
Appears when scrolled into view
``` ### Morph Morph DOM elements (useful with HTMX): ```html theme={null}

``` ## Complete Application Example Let's build a complete Todo app with Mizu backend and Alpine frontend. ### Backend `app/server/handlers.go`: ```go theme={null} package server import ( "sync" "time" "github.com/go-mizu/mizu" ) type Todo struct { ID int `json:"id"` Text string `json:"text"` Completed bool `json:"completed"` CreatedAt time.Time `json:"created_at"` } type TodoService struct { todos []*Todo nextID int mu sync.RWMutex } func NewTodoService() *TodoService { return &TodoService{ todos: []*Todo{}, nextID: 1, } } func (s *TodoService) List(filter string) []*Todo { s.mu.RLock() defer s.mu.RUnlock() if filter == "" { return s.todos } var result []*Todo for _, todo := range s.todos { if filter == "active" && !todo.Completed { result = append(result, todo) } else if filter == "completed" && todo.Completed { result = append(result, todo) } } return result } func (s *TodoService) Create(text string) *Todo { s.mu.Lock() defer s.mu.Unlock() todo := &Todo{ ID: s.nextID, Text: text, Completed: false, CreatedAt: time.Now(), } s.todos = append(s.todos, todo) s.nextID++ return todo } func (s *TodoService) Toggle(id int) (*Todo, bool) { s.mu.Lock() defer s.mu.Unlock() for _, todo := range s.todos { if todo.ID == id { todo.Completed = !todo.Completed return todo, true } } return nil, false } func (s *TodoService) Delete(id int) bool { s.mu.Lock() defer s.mu.Unlock() for i, todo := range s.todos { if todo.ID == id { s.todos = append(s.todos[:i], s.todos[i+1:]...) return true } } return false } func (h *Handlers) ListTodos(c *mizu.Ctx) error { filter := c.Query("filter") todos := h.todoService.List(filter) return c.JSON(200, todos) } func (h *Handlers) CreateTodo(c *mizu.Ctx) error { var req struct { Text string `json:"text"` } if err := c.BindJSON(&req); err != nil { return c.JSON(400, map[string]string{"error": "Invalid request"}) } if req.Text == "" { return c.JSON(400, map[string]string{"error": "Text is required"}) } todo := h.todoService.Create(req.Text) return c.JSON(201, todo) } func (h *Handlers) ToggleTodo(c *mizu.Ctx) error { id := c.ParamInt("id") todo, ok := h.todoService.Toggle(id) if !ok { return c.JSON(404, map[string]string{"error": "Todo not found"}) } return c.JSON(200, todo) } func (h *Handlers) DeleteTodo(c *mizu.Ctx) error { id := c.ParamInt("id") if !h.todoService.Delete(id) { return c.JSON(404, map[string]string{"error": "Todo not found"}) } return c.HTML(204, "") } ``` `app/server/routes.go`: ```go theme={null} app.Get("/api/todos", h.ListTodos) app.Post("/api/todos", h.CreateTodo) app.Patch("/api/todos/{id}/toggle", h.ToggleTodo) app.Delete("/api/todos/{id}", h.DeleteTodo) ``` ### Frontend `views/pages/home.html`: ```html theme={null}

Todo App

Loading todos...
``` ## Performance ### Lazy Initialization Defer Alpine initialization for better initial load: ```html theme={null} ``` ### Debouncing and Throttling Reduce unnecessary updates: ```html theme={null}
Scroll content
``` ### Virtual Scrolling For very long lists, use virtual scrolling (requires plugin or manual implementation). ### Minimize Re-renders Use `x-show` instead of `x-if` when toggling frequently: ```html theme={null}
Content
``` ## Security ### XSS Protection Never use `x-html` with user content: ```html theme={null}
``` ### CSRF Protection Include CSRF tokens in requests: ```html theme={null}
``` ### Sanitize User Input Always validate and sanitize on the server: ```go theme={null} func handleCreate(c *mizu.Ctx) error { var req struct { Text string `json:"text"` } if err := c.BindJSON(&req); err != nil { return c.JSON(400, map[string]string{"error": "Invalid request"}) } // Validate if req.Text == "" { return c.JSON(400, map[string]string{"error": "Text required"}) } // Sanitize text := html.EscapeString(req.Text) // Process... } ``` ## Testing ### Unit Tests ```js theme={null} import Alpine from 'alpinejs' import { expect, test } from 'vitest' test('counter increments', async () => { document.body.innerHTML = `
` Alpine.start() const button = document.querySelector('button') const span = document.querySelector('span') expect(span.textContent).toBe('0') button.click() await Alpine.nextTick() expect(span.textContent).toBe('1') }) ``` ### E2E Tests ```js theme={null} test('todo app works', async ({ page }) => { await page.goto('http://localhost:3000') // Add todo await page.fill('input[placeholder*="What needs"]', 'Buy milk') await page.click('button:has-text("Add")') // Check it appears await expect(page.locator('text=Buy milk')).toBeVisible() // Toggle completion await page.click('input[type="checkbox"]') await expect(page.locator('text=Buy milk')).toHaveClass(/line-through/) // Delete await page.click('button:has-text("Delete")') await page.click('button:has-text("OK")') // Confirm await expect(page.locator('text=Buy milk')).not.toBeVisible() }) ``` ## Troubleshooting ### Alpine Not Working Check: 1. Alpine script is loaded: `` 2. Using `defer` attribute 3. No JavaScript errors in console 4. `x-data` is on parent element ### Data Not Updating Check: 1. Data property is reactive (defined in `x-data`) 2. Modifying data correctly (`this.count++` not `count++`) 3. No typos in property names 4. Using Alpine's reactivity (not vanilla JS) ### Events Not Firing Check: 1. Event name is correct (`@click` not `@onclick`) 2. Element can receive events 3. No `@click.stop` preventing propagation 4. Handler function exists ### Transitions Not Working Check: 1. Element uses `x-show` not `x-if` (or use `x-transition` on template parent) 2. Tailwind classes available 3. No conflicting CSS ### Debug Mode Enable Alpine devtools: ```html theme={null} ``` Then use browser devtools to inspect `window.Alpine`. ## When to Use Alpine ### Perfect For * **Interactive Widgets**: Dropdowns, modals, tabs, accordions * **Form Enhancement**: Validation, dynamic inputs, autocomplete * **Progressive Enhancement**: Add interactivity to server-rendered pages * **Prototypes**: Rapid development without build tools * **Small to Medium Apps**: Todo apps, dashboards, admin panels * **With HTMX**: Client-side UI state + server interactions ### Not Ideal For * **Large SPAs**: Complex routing and state management * **Heavy Computation**: Better suited for backend or Web Workers * **Offline-First**: No built-in offline support * **Complex Data Flow**: Redux-like patterns harder to implement ### Hybrid Approach Use Alpine for UI state, backend for business logic: ```html theme={null}
``` ## Next Steps Combine with HTMX for server interactions Server-rendered templates with Mizu Official Alpine.js documentation Component library and examples Official examples and patterns Perfect CSS companion for Alpine # Angular Source: https://docs.go-mizu.dev/frontend/angular Build Angular applications with Mizu and TypeScript. Angular is a comprehensive framework from Google for building web applications. This guide shows you how to integrate Angular with Mizu as the backend. ## Why Angular? Angular is a full-featured, opinionated framework that provides everything you need to build large-scale applications. Unlike libraries like React or Vue, Angular is a complete platform with a rich ecosystem of official tools and patterns. **Key strengths:** * **Complete platform**: Router, HTTP, Forms, Testing all included * **TypeScript-first**: Built with and for TypeScript * **Dependency Injection**: Enterprise-grade DI system * **RxJS**: Powerful reactive programming * **Signals**: New reactive primitives (Angular 16+) * **CLI**: World-class tooling and scaffolding * **Consistency**: Strong opinions lead to consistent code * **Enterprise-ready**: Battle-tested at Google scale ### Angular vs Other Frameworks | Feature | Angular | React | Vue 3 | Svelte | | ------------------ | --------------------- | ------------- | ---------------- | --------------- | | **Bundle Size** | \~167 KB | \~44 KB | \~34 KB | \~3 KB | | **Learning Curve** | Steep | Moderate | Gentle | Gentle | | **Language** | TypeScript | JSX/TSX | SFC | SFC | | **Reactivity** | Zone.js + Signals | Virtual DOM | Proxy | Compiled | | **State Mgmt** | Services/NgRx/Signals | Redux/Zustand | Pinia | Stores | | **Forms** | Reactive/Template | Libraries | v-model | bind: | | **Routing** | Built-in | React Router | Vue Router | Libraries | | **HTTP** | Built-in (HttpClient) | fetch/axios | fetch/axios | fetch | | **CLI** | Angular CLI | CRA/Vite | Vite | Vite/SvelteKit | | **DI** | Built-in | Manual | Manual | Manual | | **Testing** | Jasmine/Karma | Jest/RTL | Vitest | Vitest | | **Best For** | Enterprise apps | Any size | Progressive apps | Small/fast apps | ### Angular + Mizu vs Standalone Angular | Aspect | Angular + Mizu | Standalone Angular | | --------------- | ------------------------ | --------------------------- | | **Backend** | Go (Mizu) | Node.js/Java/.NET | | **API** | Go handlers | Express/NestJS | | **Database** | Go libs | TypeORM/Prisma | | **Deployment** | Single binary | Separate frontend + backend | | **Type Safety** | TS frontend + Go backend | Full TypeScript stack | | **SSR** | No (SPA) | Angular Universal | ## Quick Start ```bash theme={null} mizu new ./my-angular-app --template frontend/angular cd my-angular-app make dev ``` Visit `http://localhost:4200` to see your app! ## Architecture ### Development Mode ``` ┌─────────────────────────────────────────────────────────────┐ │ Browser │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ Angular App (HMR enabled) │ │ │ │ ┌─────────┐ ┌─────────┐ ┌──────────┐ │ │ │ │ │ Router │→ │ Pages │→ │ HTTP Call│ │ │ │ │ └─────────┘ └─────────┘ └──────────┘ │ │ │ └───────────────────────────────────────────────────────┘ │ │ ↑ HMR WebSocket ↓ /api/users │ └─────────┼──────────────────────────┼────────────────────────┘ │ │ ┌─────────┼──────────────────────────┼────────────────────────┐ │ Angular Dev Server (:4200) │ │ │ │ ↓ (proxied) │ │ ┌────┴─────────┐ ┌──────────────┐ │ │ │ Webpack Dev │ │ Proxy to │ │ │ │ Server + HMR │ │ :3000 │ │ │ └──────────────┘ └──────────────┘ │ │ ↑ ↓ │ └─────────┼──────────────────────────┼────────────────────────┘ │ │ ┌─────────┼──────────────────────────┼────────────────────────┐ │ Mizu Server (:3000) │ │ │ ↓ │ │ ┌────────────┐ │ │ │ API Routes │ │ │ │ (Go) │ │ │ └────────────┘ │ └──────────────────────────────────────────────────────────────┘ ``` ### Production Mode ``` ┌─────────────────────────────────────────────────────────────┐ │ Browser │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ Angular App (compiled, minified, AOT) │ │ │ │ ┌─────────┐ ┌─────────┐ ┌──────────┐ │ │ │ │ │ Router │→ │ Pages │→ │ HTTP Call│ │ │ │ │ └─────────┘ └─────────┘ └──────────┘ │ │ │ └───────────────────────────────────────────────────────┘ │ │ ↑ index.html + bundles ↓ /api/users │ └─────────┼──────────────────────────┼────────────────────────┘ │ │ ┌─────────┼──────────────────────────┼────────────────────────┐ │ Mizu Server (single binary) │ │ │ │ ↓ │ │ ┌────┴─────────┐ ┌────────────┐ │ │ │ Embedded FS │ │ API Routes │ │ │ │ (dist/...) │ │ (Go) │ │ │ └──────────────┘ └────────────┘ │ │ ←──────────────────────────────────────────────────────── │ │ Static files served from Go binary (//go:embed) │ └──────────────────────────────────────────────────────────────┘ ``` ## Project Structure ``` my-angular-app/ ├── cmd/server/ │ └── main.go ├── app/server/ │ ├── app.go │ ├── config.go │ └── routes.go # API routes ├── frontend/ # Angular app │ ├── src/ │ │ ├── app/ │ │ │ ├── app.component.ts │ │ │ ├── app.config.ts │ │ │ ├── app.routes.ts │ │ │ ├── components/ │ │ │ ├── pages/ │ │ │ ├── services/ │ │ │ ├── guards/ │ │ │ ├── interceptors/ │ │ │ ├── pipes/ │ │ │ └── directives/ │ │ ├── index.html │ │ ├── main.ts │ │ └── styles.css │ ├── angular.json │ ├── package.json │ ├── tsconfig.json │ └── proxy.conf.json # Dev proxy config ├── dist/my-app/browser/ # Build output └── Makefile ``` ## Backend Setup ```go theme={null} package server import ( "embed" "io/fs" "github.com/go-mizu/mizu" "github.com/go-mizu/mizu/frontend" ) //go:embed all:../../dist/my-app/browser var distFS embed.FS func New(cfg *Config) *mizu.App { app := mizu.New() // API routes setupRoutes(app) // Angular outputs to dist/my-app/browser dist, _ := fs.Sub(distFS, "dist/my-app/browser") app.Use(frontend.WithOptions(frontend.Options{ Mode: frontend.ModeAuto, FS: dist, Root: "./dist/my-app/browser", DevServer: "http://localhost:" + cfg.DevPort, IgnorePaths: []string{"/api"}, })) return app } ``` **Important:** Angular's output path includes the project name: `dist/my-app/browser/` ### API Routes Example ```go theme={null} // app/server/routes.go package server import "github.com/go-mizu/mizu" type User struct { ID int `json:"id"` Name string `json:"name"` Email string `json:"email"` } func setupRoutes(app *mizu.App) { // GET /api/users app.GET("/api/users", func(c *mizu.Ctx) error { users := []User{ {ID: 1, Name: "Alice", Email: "alice@example.com"}, {ID: 2, Name: "Bob", Email: "bob@example.com"}, } return c.JSON(users) }) // POST /api/users app.POST("/api/users", func(c *mizu.Ctx) error { var user User if err := c.BodyParser(&user); err != nil { return err } user.ID = 3 // In real app, generate from DB return c.Status(201).JSON(user) }) // GET /api/users/:id app.GET("/api/users/:id", func(c *mizu.Ctx) error { id := c.Param("id") user := User{ID: 1, Name: "Alice", Email: "alice@example.com"} return c.JSON(user) }) // DELETE /api/users/:id app.DELETE("/api/users/:id", func(c *mizu.Ctx) error { return c.Status(204).Send(nil) }) } ``` ## Angular Configuration ### `frontend/angular.json` ```json theme={null} { "projects": { "my-app": { "architect": { "build": { "options": { "outputPath": "../dist/my-app", "index": "src/index.html", "browser": "src/main.ts", "polyfills": [ "zone.js" ] }, "configurations": { "production": { "optimization": true, "outputHashing": "all", "sourceMap": false, "namedChunks": false, "extractLicenses": true, "budgets": [ { "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" } ] } } }, "serve": { "options": { "port": 4200, "proxyConfig": "proxy.conf.json" } } } } } } ``` ### `frontend/proxy.conf.json` ```json theme={null} { "/api": { "target": "http://localhost:3000", "secure": false, "changeOrigin": true } } ``` This proxies `/api/*` requests to the Mizu backend during development. ## Frontend Setup ### `frontend/src/app/app.routes.ts` ```ts theme={null} import { Routes } from '@angular/router' import { HomeComponent } from './pages/home/home.component' import { AboutComponent } from './pages/about/about.component' export const routes: Routes = [ { path: '', component: HomeComponent }, { path: 'about', component: AboutComponent }, ] ``` ### `frontend/src/app/app.config.ts` ```ts theme={null} import { ApplicationConfig } from '@angular/core' import { provideRouter } from '@angular/router' import { provideHttpClient, withInterceptors } from '@angular/common/http' import { routes } from './app.routes' export const appConfig: ApplicationConfig = { providers: [ provideRouter(routes), provideHttpClient(), ] } ``` ### `frontend/src/app/app.component.ts` ```ts theme={null} import { Component } from '@angular/core' import { RouterOutlet, RouterLink } from '@angular/router' @Component({ selector: 'app-root', standalone: true, imports: [RouterOutlet, RouterLink], template: `
`, styles: [` nav { display: flex; gap: 1rem; padding: 1rem; background: #f5f5f5; } nav a { color: #dd0031; text-decoration: none; } nav a:hover { text-decoration: underline; } main { padding: 2rem; } `] }) export class AppComponent {} ``` ### `frontend/src/app/pages/home/home.component.ts` ```ts theme={null} import { Component, OnInit } from '@angular/core' import { CommonModule } from '@angular/common' import { HttpClient } from '@angular/common/http' interface User { id: number name: string email: string } @Component({ selector: 'app-home', standalone: true, imports: [CommonModule], template: `

Users

Loading...
Error: {{ error }}
  • {{ user.name }} ({{ user.email }})
` }) export class HomeComponent implements OnInit { users: User[] = [] loading = true error: string | null = null constructor(private http: HttpClient) {} ngOnInit() { this.http.get('/api/users') .subscribe({ next: (data) => { this.users = data this.loading = false }, error: (err) => { this.error = err.message this.loading = false } }) } } ``` ## Standalone Components Angular 14+ introduced standalone components, eliminating the need for NgModules for most apps. ### Creating Standalone Components ```ts theme={null} import { Component } from '@angular/core' import { CommonModule } from '@angular/common' @Component({ selector: 'app-user-card', standalone: true, // Mark as standalone imports: [CommonModule], // Import dependencies directly template: `

{{ user.name }}

{{ user.email }}

`, styles: [` .card { border: 1px solid #ddd; padding: 1rem; border-radius: 8px; } `] }) export class UserCardComponent { @Input() user!: User } ``` ### Component Communication ```ts theme={null} // Parent component import { Component } from '@angular/core' import { UserCardComponent } from './user-card/user-card.component' @Component({ selector: 'app-users', standalone: true, imports: [UserCardComponent], template: ` ` }) export class UsersComponent { users = [ { id: 1, name: 'Alice', email: 'alice@example.com' }, { id: 2, name: 'Bob', email: 'bob@example.com' } ] onDelete(userId: number) { this.users = this.users.filter(u => u.id !== userId) } } // Child component @Component({ selector: 'app-user-card', standalone: true, template: `

{{ user.name }}

{{ user.email }}

` }) export class UserCardComponent { @Input() user!: User @Output() delete = new EventEmitter() } ``` ## Services and Dependency Injection Angular's DI system is one of its most powerful features. ### Creating a Service ```ts theme={null} // services/user.service.ts import { Injectable } from '@angular/core' import { HttpClient } from '@angular/common/http' import { Observable } from 'rxjs' export interface User { id: number name: string email: string } @Injectable({ providedIn: 'root' // Singleton across the app }) export class UserService { private apiUrl = '/api/users' constructor(private http: HttpClient) {} getUsers(): Observable { return this.http.get(this.apiUrl) } getUser(id: number): Observable { return this.http.get(`${this.apiUrl}/${id}`) } createUser(user: Omit): Observable { return this.http.post(this.apiUrl, user) } updateUser(id: number, user: Partial): Observable { return this.http.patch(`${this.apiUrl}/${id}`, user) } deleteUser(id: number): Observable { return this.http.delete(`${this.apiUrl}/${id}`) } } ``` ### Using Services in Components ```ts theme={null} import { Component, OnInit } from '@angular/core' import { UserService, User } from '../../services/user.service' @Component({ selector: 'app-users', template: `

Users

  • {{ user.name }}
` }) export class UsersComponent implements OnInit { users: User[] = [] constructor(private userService: UserService) {} ngOnInit() { this.loadUsers() } loadUsers() { this.userService.getUsers() .subscribe(users => this.users = users) } deleteUser(id: number) { this.userService.deleteUser(id) .subscribe(() => { this.users = this.users.filter(u => u.id !== id) }) } addUser() { this.userService.createUser({ name: 'New User', email: 'new@example.com' }) .subscribe(user => { this.users.push(user) }) } } ``` ### Service Scopes ```ts theme={null} // Singleton (app-wide) @Injectable({ providedIn: 'root' }) export class SingletonService {} // Provided in specific component tree @Injectable() export class ScopedService {} // Use in component @Component({ selector: 'app-feature', providers: [ScopedService] // New instance for this component and its children }) export class FeatureComponent {} ``` ## Signals (Angular 16+) Signals are Angular's new reactive primitives for fine-grained reactivity. ### Basic Signals ```ts theme={null} import { Component, signal, computed, effect } from '@angular/core' @Component({ selector: 'app-counter', standalone: true, template: `

Count: {{ count() }}

Doubled: {{ doubled() }}

` }) export class CounterComponent { // Writable signal count = signal(0) // Computed signal (automatically updates) doubled = computed(() => this.count() * 2) // Effect (runs when dependencies change) constructor() { effect(() => { console.log('Count changed to:', this.count()) }) } increment() { this.count.update(n => n + 1) // Or: this.count.set(this.count() + 1) } decrement() { this.count.update(n => n - 1) } } ``` ### Signals in Services ```ts theme={null} import { Injectable, signal, computed } from '@angular/core' import { HttpClient } from '@angular/common/http' @Injectable({ providedIn: 'root' }) export class TodoService { // State as signals private todosSignal = signal([]) private loadingSignal = signal(false) // Public readonly signals todos = this.todosSignal.asReadonly() loading = this.loadingSignal.asReadonly() // Computed signals completedCount = computed(() => this.todosSignal().filter(t => t.done).length ) pendingCount = computed(() => this.todosSignal().filter(t => !t.done).length ) constructor(private http: HttpClient) {} loadTodos() { this.loadingSignal.set(true) this.http.get('/api/todos') .subscribe(todos => { this.todosSignal.set(todos) this.loadingSignal.set(false) }) } addTodo(title: string) { const todo: Todo = { id: Date.now(), title, done: false } this.todosSignal.update(todos => [...todos, todo]) this.http.post('/api/todos', todo).subscribe() } toggleTodo(id: number) { this.todosSignal.update(todos => todos.map(t => t.id === id ? { ...t, done: !t.done } : t) ) } } ``` ## RxJS and Observables Angular uses RxJS extensively for async operations. ### Common RxJS Operators ```ts theme={null} import { Component, OnInit } from '@angular/core' import { HttpClient } from '@angular/common/http' import { map, filter, catchError, retry, debounceTime, switchMap } from 'rxjs/operators' import { of, Subject } from 'rxjs' @Component({ selector: 'app-search', template: `
  • {{ user.name }}
` }) export class SearchComponent implements OnInit { search$ = new Subject() users: User[] = [] constructor(private http: HttpClient) {} ngOnInit() { // Debounce search input, switch to new search, handle errors this.search$.pipe( debounceTime(300), // Wait 300ms after typing stops filter(query => query.length > 2), // Only search if 3+ chars switchMap(query => // Cancel previous search, start new one this.http.get(`/api/users?q=${query}`).pipe( retry(2), // Retry failed requests twice catchError(() => of([])) // Return empty array on error ) ) ).subscribe(users => { this.users = users }) } } ``` ### Common Patterns ```ts theme={null} // Map data this.http.get('/api/users').pipe( map(users => users.map(u => ({ ...u, displayName: `${u.name} (${u.email})` }))) ) // Combine multiple requests import { forkJoin } from 'rxjs' forkJoin({ users: this.http.get('/api/users'), posts: this.http.get('/api/posts'), stats: this.http.get('/api/stats') }).subscribe(({ users, posts, stats }) => { // All requests completed }) // Sequential requests this.http.get('/api/user/1').pipe( switchMap(user => this.http.get(`/api/posts?userId=${user.id}`)) ).subscribe(posts => { // Posts for user 1 }) ``` ## Reactive Forms Angular's reactive forms provide fine-grained control over form state and validation. ### Basic Reactive Form ```ts theme={null} import { Component } from '@angular/core' import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms' import { CommonModule } from '@angular/common' @Component({ selector: 'app-user-form', standalone: true, imports: [CommonModule, ReactiveFormsModule], template: `
Name is required and must be at least 2 characters
Valid email is required
Age must be between 18 and 120
`, styles: [` .error { color: red; font-size: 0.875rem; } form > div { margin-bottom: 1rem; } `] }) export class UserFormComponent { userForm: FormGroup constructor(private fb: FormBuilder) { this.userForm = this.fb.group({ name: ['', [Validators.required, Validators.minLength(2)]], email: ['', [Validators.required, Validators.email]], age: ['', [Validators.required, Validators.min(18), Validators.max(120)]] }) } onSubmit() { if (this.userForm.valid) { console.log('Form data:', this.userForm.value) } } } ``` ### Advanced Forms with FormArray ```ts theme={null} import { Component } from '@angular/core' import { FormBuilder, FormArray, Validators } from '@angular/forms' @Component({ selector: 'app-dynamic-form', template: `
` }) export class DynamicFormComponent { form = this.fb.group({ items: this.fb.array([]) }) constructor(private fb: FormBuilder) { this.addItem() } get items() { return this.form.get('items') as FormArray } addItem() { this.items.push(this.fb.group({ name: ['', Validators.required], quantity: [1, [Validators.required, Validators.min(1)]] })) } removeItem(index: number) { this.items.removeAt(index) } onSubmit() { console.log(this.form.value) } } ``` ### Custom Validators ```ts theme={null} import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms' // Synchronous validator export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { const forbidden = nameRe.test(control.value) return forbidden ? { forbiddenName: { value: control.value } } : null } } // Async validator export function uniqueEmailValidator(userService: UserService): AsyncValidatorFn { return (control: AbstractControl): Observable => { return userService.checkEmailExists(control.value).pipe( map(exists => exists ? { emailTaken: true } : null) ) } } // Usage this.userForm = this.fb.group({ name: ['', [Validators.required, forbiddenNameValidator(/admin/i)]], email: ['', [Validators.required, Validators.email], [uniqueEmailValidator(this.userService)]] }) ``` ## Routing Angular Router provides powerful routing capabilities. ### Nested Routes ```ts theme={null} // app.routes.ts import { Routes } from '@angular/router' export const routes: Routes = [ { path: '', component: HomeComponent }, { path: 'dashboard', component: DashboardLayoutComponent, children: [ { path: '', component: DashboardHomeComponent }, { path: 'users', component: UsersComponent }, { path: 'users/:id', component: UserDetailComponent }, { path: 'settings', component: SettingsComponent } ] }, { path: '**', component: NotFoundComponent } // 404 ] ``` ```ts theme={null} // DashboardLayoutComponent @Component({ selector: 'app-dashboard-layout', template: ` ` }) export class DashboardLayoutComponent {} ``` ### Route Parameters ```ts theme={null} import { Component, OnInit } from '@angular/core' import { ActivatedRoute } from '@angular/router' @Component({ selector: 'app-user-detail', template: `

{{ user.name }}

{{ user.email }}

` }) export class UserDetailComponent implements OnInit { user: User | null = null constructor( private route: ActivatedRoute, private userService: UserService ) {} ngOnInit() { // Get route parameter const id = Number(this.route.snapshot.paramMap.get('id')) // Or subscribe to params (for when route changes but component is reused) this.route.paramMap.subscribe(params => { const id = Number(params.get('id')) this.loadUser(id) }) } loadUser(id: number) { this.userService.getUser(id).subscribe(user => { this.user = user }) } } ``` ### Programmatic Navigation ```ts theme={null} import { Component } from '@angular/core' import { Router } from '@angular/router' @Component({ selector: 'app-example', template: ` ` }) export class ExampleComponent { constructor(private router: Router) {} goToUser(id: number) { // Navigate to route this.router.navigate(['/users', id]) // Or with query params this.router.navigate(['/users'], { queryParams: { search: 'alice' } }) } goBack() { window.history.back() } } ``` ### Route Guards ```ts theme={null} // guards/auth.guard.ts import { inject } from '@angular/core' import { Router } from '@angular/router' import { AuthService } from '../services/auth.service' export const authGuard = () => { const authService = inject(AuthService) const router = inject(Router) if (authService.isAuthenticated()) { return true } router.navigate(['/login']) return false } ``` ```ts theme={null} // routes.ts export const routes: Routes = [ { path: 'login', component: LoginComponent }, { path: 'admin', component: AdminComponent, canActivate: [authGuard] // Protect route } ] ``` ### Route Resolvers ```ts theme={null} // resolvers/user.resolver.ts import { inject } from '@angular/core' import { ActivatedRouteSnapshot } from '@angular/router' import { UserService } from '../services/user.service' export const userResolver = (route: ActivatedRouteSnapshot) => { const userService = inject(UserService) const id = Number(route.paramMap.get('id')) return userService.getUser(id) } ``` ```ts theme={null} // routes.ts export const routes: Routes = [ { path: 'users/:id', component: UserDetailComponent, resolve: { user: userResolver } // Data loaded before component renders } ] ``` ```ts theme={null} // UserDetailComponent ngOnInit() { // Data already loaded this.user = this.route.snapshot.data['user'] } ``` ## Pipes Pipes transform data in templates. ### Built-in Pipes ```ts theme={null} @Component({ template: `

{{ today | date:'fullDate' }}

{{ price | currency:'USD':'symbol':'1.2-2' }}

{{ name | uppercase }}

{{ user | json }}

{{ user.name }}

  • {{ item }}
  • ` }) export class ExampleComponent { today = new Date() price = 1234.56 name = 'Alice' user = { name: 'Bob', email: 'bob@example.com' } user$ = this.http.get('/api/user/1') items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] } ``` ### Custom Pipes ```ts theme={null} import { Pipe, PipeTransform } from '@angular/core' @Pipe({ name: 'timeAgo', standalone: true }) export class TimeAgoPipe implements PipeTransform { transform(value: Date): string { const now = new Date() const diff = now.getTime() - value.getTime() const seconds = Math.floor(diff / 1000) const minutes = Math.floor(seconds / 60) const hours = Math.floor(minutes / 60) const days = Math.floor(hours / 24) if (days > 0) return `${days}d ago` if (hours > 0) return `${hours}h ago` if (minutes > 0) return `${minutes}m ago` return `${seconds}s ago` } } ``` ```ts theme={null} // Usage @Component({ imports: [TimeAgoPipe], template: `

    Posted {{ post.createdAt | timeAgo }}

    ` }) ``` ## Directives Directives modify element behavior. ### Structural Directives ```ts theme={null} // Custom structural directive import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core' @Directive({ selector: '[appRepeat]', standalone: true }) export class RepeatDirective { constructor( private templateRef: TemplateRef, private viewContainer: ViewContainerRef ) {} @Input() set appRepeat(times: number) { this.viewContainer.clear() for (let i = 0; i < times; i++) { this.viewContainer.createEmbeddedView(this.templateRef, { $implicit: i }) } } } ``` ```ts theme={null} // Usage @Component({ imports: [RepeatDirective], template: `
    Item {{ i }}
    ` }) ``` ### Attribute Directives ```ts theme={null} import { Directive, ElementRef, HostListener, Input } from '@angular/core' @Directive({ selector: '[appHighlight]', standalone: true }) export class HighlightDirective { @Input() appHighlight = 'yellow' constructor(private el: ElementRef) {} @HostListener('mouseenter') onMouseEnter() { this.highlight(this.appHighlight) } @HostListener('mouseleave') onMouseLeave() { this.highlight('') } private highlight(color: string) { this.el.nativeElement.style.backgroundColor = color } } ``` ```ts theme={null} // Usage @Component({ imports: [HighlightDirective], template: `

    Hover over me!

    ` }) ``` ## HTTP Interceptors Interceptors modify HTTP requests and responses globally. ```ts theme={null} // interceptors/auth.interceptor.ts import { HttpInterceptorFn } from '@angular/common/http' export const authInterceptor: HttpInterceptorFn = (req, next) => { const token = localStorage.getItem('auth_token') if (token) { // Clone and modify request req = req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }) } return next(req) } ``` ```ts theme={null} // interceptors/error.interceptor.ts import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http' import { catchError, throwError } from 'rxjs' import { inject } from '@angular/core' import { Router } from '@angular/router' export const errorInterceptor: HttpInterceptorFn = (req, next) => { const router = inject(Router) return next(req).pipe( catchError((error: HttpErrorResponse) => { if (error.status === 401) { // Redirect to login on 401 router.navigate(['/login']) } return throwError(() => error) }) ) } ``` ```ts theme={null} // app.config.ts export const appConfig: ApplicationConfig = { providers: [ provideHttpClient( withInterceptors([authInterceptor, errorInterceptor]) ) ] } ``` ## Lifecycle Hooks Angular components have a rich lifecycle. ```ts theme={null} import { Component, OnInit, OnChanges, DoCheck, AfterContentInit, AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy, SimpleChanges } from '@angular/core' @Component({ selector: 'app-lifecycle', template: `

    Lifecycle example

    ` }) export class LifecycleComponent implements OnChanges, OnInit, DoCheck, AfterContentInit, AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy { ngOnChanges(changes: SimpleChanges) { // Called before ngOnInit and when input properties change console.log('ngOnChanges', changes) } ngOnInit() { // Called once after first ngOnChanges // Good for: API calls, initialization console.log('ngOnInit') } ngDoCheck() { // Called during every change detection run // Use sparingly - performance impact console.log('ngDoCheck') } ngAfterContentInit() { // Called once after content (ng-content) initialized console.log('ngAfterContentInit') } ngAfterContentChecked() { // Called after content checked during change detection console.log('ngAfterContentChecked') } ngAfterViewInit() { // Called once after view initialized // Good for: DOM manipulation, accessing ViewChild console.log('ngAfterViewInit') } ngAfterViewChecked() { // Called after view checked during change detection console.log('ngAfterViewChecked') } ngOnDestroy() { // Called before component is destroyed // Good for: Cleanup, unsubscribe console.log('ngOnDestroy') } } ``` **Lifecycle order:** 1. `ngOnChanges()` - Input changes 2. `ngOnInit()` - Component initialized 3. `ngDoCheck()` - Change detection 4. `ngAfterContentInit()` - Content initialized 5. `ngAfterContentChecked()` - Content checked 6. `ngAfterViewInit()` - View initialized 7. `ngAfterViewChecked()` - View checked 8. `ngOnDestroy()` - Before destruction ## ViewChild and ContentChild Access child components and DOM elements. ```ts theme={null} import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core' @Component({ selector: 'app-parent', template: ` ` }) export class ParentComponent implements AfterViewInit { @ViewChild('searchInput') searchInput!: ElementRef @ViewChild('childComponent') childComponent!: ChildComponent ngAfterViewInit() { // ViewChild available after view init console.log(this.searchInput.nativeElement.value) } focusInput() { this.searchInput.nativeElement.focus() } callChild() { this.childComponent.someMethod() } } ``` ## Lazy Loading Load feature modules on demand to reduce initial bundle size. ```ts theme={null} // app.routes.ts export const routes: Routes = [ { path: '', component: HomeComponent }, { path: 'admin', loadComponent: () => import('./pages/admin/admin.component').then(m => m.AdminComponent) }, { path: 'dashboard', loadChildren: () => import('./features/dashboard/dashboard.routes').then(m => m.DASHBOARD_ROUTES) } ] ``` ```ts theme={null} // features/dashboard/dashboard.routes.ts import { Routes } from '@angular/router' export const DASHBOARD_ROUTES: Routes = [ { path: '', component: DashboardHomeComponent }, { path: 'users', component: DashboardUsersComponent } ] ``` ## Complete Real-World Example: Task Manager Let's build a complete task management application. ### Backend (Go) ```go theme={null} // app/server/routes.go package server import ( "sync" "time" "github.com/go-mizu/mizu" ) type Task struct { ID int `json:"id"` Title string `json:"title"` Description string `json:"description"` Status string `json:"status"` Priority string `json:"priority"` CreatedAt time.Time `json:"createdAt"` } var ( tasks = []Task{} nextID = 1 tasksMu sync.RWMutex ) func setupRoutes(app *mizu.App) { app.GET("/api/tasks", func(c *mizu.Ctx) error { tasksMu.RLock() defer tasksMu.RUnlock() return c.JSON(tasks) }) app.POST("/api/tasks", func(c *mizu.Ctx) error { var task Task if err := c.BodyParser(&task); err != nil { return c.Status(400).JSON(map[string]string{"error": "Invalid request"}) } tasksMu.Lock() task.ID = nextID task.CreatedAt = time.Now() nextID++ tasks = append(tasks, task) tasksMu.Unlock() return c.Status(201).JSON(task) }) app.PUT("/api/tasks/:id", func(c *mizu.Ctx) error { id := c.ParamInt("id") var updates Task if err := c.BodyParser(&updates); err != nil { return c.Status(400).JSON(map[string]string{"error": "Invalid request"}) } tasksMu.Lock() defer tasksMu.Unlock() for i, task := range tasks { if task.ID == id { updates.ID = id updates.CreatedAt = task.CreatedAt tasks[i] = updates return c.JSON(updates) } } return c.Status(404).JSON(map[string]string{"error": "Task not found"}) }) app.DELETE("/api/tasks/:id", func(c *mizu.Ctx) error { id := c.ParamInt("id") tasksMu.Lock() defer tasksMu.Unlock() for i, task := range tasks { if task.ID == id { tasks = append(tasks[:i], tasks[i+1:]...) return c.Status(204).Send(nil) } } return c.Status(404).JSON(map[string]string{"error": "Task not found"}) }) } ``` ### Frontend - Service ```ts theme={null} // services/task.service.ts import { Injectable, signal, computed } from '@angular/core' import { HttpClient } from '@angular/common/http' export interface Task { id: number title: string description: string status: 'pending' | 'in-progress' | 'completed' priority: 'low' | 'medium' | 'high' createdAt: Date } @Injectable({ providedIn: 'root' }) export class TaskService { private tasksSignal = signal([]) private loadingSignal = signal(false) private filterSignal = signal('all') tasks = this.tasksSignal.asReadonly() loading = this.loadingSignal.asReadonly() filter = this.filterSignal filteredTasks = computed(() => { const filter = this.filterSignal() const tasks = this.tasksSignal() if (filter === 'all') return tasks return tasks.filter(t => t.status === filter) }) constructor(private http: HttpClient) { this.loadTasks() } loadTasks() { this.loadingSignal.set(true) this.http.get('/api/tasks') .subscribe(tasks => { this.tasksSignal.set(tasks) this.loadingSignal.set(false) }) } addTask(task: Omit) { this.http.post('/api/tasks', task) .subscribe(newTask => { this.tasksSignal.update(tasks => [...tasks, newTask]) }) } updateTask(id: number, updates: Partial) { const task = this.tasksSignal().find(t => t.id === id) if (!task) return const updated = { ...task, ...updates } this.http.put(`/api/tasks/${id}`, updated) .subscribe(newTask => { this.tasksSignal.update(tasks => tasks.map(t => t.id === id ? newTask : t) ) }) } deleteTask(id: number) { this.http.delete(`/api/tasks/${id}`) .subscribe(() => { this.tasksSignal.update(tasks => tasks.filter(t => t.id !== id)) }) } setFilter(filter: string) { this.filterSignal.set(filter) } } ``` ### Frontend - Components ```ts theme={null} // components/task-form/task-form.component.ts import { Component } from '@angular/core' import { FormBuilder, Validators, ReactiveFormsModule } from '@angular/forms' import { TaskService } from '../../services/task.service' import { CommonModule } from '@angular/common' @Component({ selector: 'app-task-form', standalone: true, imports: [CommonModule, ReactiveFormsModule], template: `
    `, styles: [` .task-form { display: flex; flex-direction: column; gap: 1rem; padding: 1rem; background: #f5f5f5; border-radius: 8px; margin-bottom: 2rem; } input, textarea, select { padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px; } textarea { min-height: 80px; } button { background: #dd0031; color: white; padding: 0.75rem; border: none; border-radius: 4px; cursor: pointer; } button:hover:not(:disabled) { background: #c5002a; } button:disabled { opacity: 0.5; cursor: not-allowed; } `] }) export class TaskFormComponent { taskForm = this.fb.group({ title: ['', Validators.required], description: [''], priority: ['medium' as const] }) constructor( private fb: FormBuilder, private taskService: TaskService ) {} onSubmit() { if (this.taskForm.valid) { const task = { ...this.taskForm.value, status: 'pending' as const } this.taskService.addTask(task as any) this.taskForm.reset({ priority: 'medium' }) } } } ``` ```ts theme={null} // components/task-item/task-item.component.ts import { Component, Input } from '@angular/core' import { CommonModule } from '@angular/common' import { Task, TaskService } from '../../services/task.service' @Component({ selector: 'app-task-item', standalone: true, imports: [CommonModule], template: `

    {{ task.title }}

    {{ task.description }}

    {{ task.priority }}
    `, styles: [` .task-item { display: flex; justify-content: space-between; padding: 1rem; border: 1px solid #ddd; border-radius: 8px; margin-bottom: 1rem; } .task-item.high { border-left: 4px solid #ef4444; } .task-item.medium { border-left: 4px solid #f59e0b; } .task-item.low { border-left: 4px solid #10b981; } .task-item.completed { opacity: 0.6; } .task-item.completed h3 { text-decoration: line-through; } .priority-badge { display: inline-block; padding: 0.25rem 0.5rem; background: #e5e7eb; border-radius: 4px; font-size: 0.75rem; text-transform: uppercase; } .task-actions { display: flex; gap: 0.5rem; align-items: flex-start; } select { padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px; } .delete-btn { background: #ef4444; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; } .delete-btn:hover { background: #dc2626; } `] }) export class TaskItemComponent { @Input() task!: Task constructor(private taskService: TaskService) {} onStatusChange(event: Event) { const status = (event.target as HTMLSelectElement).value this.taskService.updateTask(this.task.id, { status: status as any }) } onDelete() { if (confirm('Delete this task?')) { this.taskService.deleteTask(this.task.id) } } } ``` ```ts theme={null} // pages/tasks/tasks.component.ts import { Component } from '@angular/core' import { CommonModule } from '@angular/common' import { TaskService } from '../../services/task.service' import { TaskFormComponent } from '../../components/task-form/task-form.component' import { TaskItemComponent } from '../../components/task-item/task-item.component' @Component({ selector: 'app-tasks', standalone: true, imports: [CommonModule, TaskFormComponent, TaskItemComponent], template: `

    Task Manager

    Loading tasks...
    No tasks found
    `, styles: [` .tasks-page { max-width: 800px; margin: 0 auto; padding: 2rem; } h1 { color: #dd0031; margin-bottom: 2rem; } .filters { display: flex; gap: 0.5rem; margin-bottom: 2rem; } .filters button { padding: 0.5rem 1rem; border: 1px solid #ddd; background: white; border-radius: 4px; cursor: pointer; text-transform: capitalize; } .filters button.active { background: #dd0031; color: white; border-color: #dd0031; } .loading, .empty { text-align: center; padding: 2rem; color: #666; } `] }) export class TasksComponent { filters = ['all', 'pending', 'in-progress', 'completed'] constructor(public taskService: TaskService) {} } ``` ## Development Workflow ```bash theme={null} # Terminal 1: Angular dev server cd frontend npm start # Runs on port 4200 # Terminal 2: Mizu backend go run cmd/server/main.go # Runs on port 3000 ``` Or: ```bash theme={null} make dev # Runs both in parallel ``` Visit `http://localhost:4200` during development. ## Building for Production ```bash theme={null} make build ``` This: 1. Builds Angular (`ng build --configuration=production`) 2. Builds Go binary with embedded frontend Run: ```bash theme={null} MIZU_ENV=production ./bin/server ``` ## Troubleshooting ### Build Path Issues **Problem**: Go can't find the Angular dist folder. **Solution**: Angular outputs to `dist/my-app/browser` (project name included): ```go theme={null} //go:embed all:../../dist/my-app/browser ``` ### Proxy Not Working **Problem**: API calls fail in development. **Solution**: Check `proxy.conf.json` is configured and referenced in `angular.json`: ```json theme={null} { "serve": { "options": { "proxyConfig": "proxy.conf.json" } } } ``` ### Zone.js Errors **Problem**: "Zone is not defined" error. **Solution**: Ensure `zone.js` is in polyfills: ```json theme={null} { "polyfills": ["zone.js"] } ``` ### Signal Not Updating **Problem**: Signal changes don't trigger updates. **Solution**: Use `.set()` or `.update()`, not direct assignment: ```ts theme={null} // ❌ Wrong mySignal() = newValue // ✅ Correct mySignal.set(newValue) mySignal.update(old => old + 1) ``` ## When to Choose Angular **Choose Angular if:** * You're building enterprise-scale applications * You want a complete, opinionated framework * You value TypeScript and strong typing * You need powerful dependency injection * Your team prefers structure and conventions * You want official solutions for common problems **Consider alternatives if:** * **React**: You need maximum flexibility or have a React ecosystem requirement * **Vue**: You want easier learning curve with similar features * **Svelte**: You need smallest bundle size and compile-time optimization * **HTMX/Alpine**: You prefer server-rendered HTML with minimal JavaScript ## Next Steps Try React instead Official Angular documentation Best practices for APIs Build and deploy # API Integration Source: https://docs.go-mizu.dev/frontend/api-integration Best practices for integrating frontend with Mizu backend APIs. Integrating your frontend with Mizu's backend is straightforward. This guide covers patterns, best practices, and common scenarios. ## Basic API Call ### Frontend ```tsx theme={null} async function fetchUsers() { const response = await fetch('/api/users') const users = await response.json() return users } ``` ### Backend ```go theme={null} app.Get("/api/users", func(c *mizu.Ctx) error { users := []map[string]any{ {"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}, } return c.JSON(200, users) }) ``` ## Request Methods ### GET ```tsx theme={null} const users = await fetch('/api/users') .then(r => r.json()) ``` ```go theme={null} app.Get("/api/users", handleGetUsers) ``` ### POST ```tsx theme={null} await fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Alice', email: 'alice@example.com' }) }) ``` ```go theme={null} app.Post("/api/users", func(c *mizu.Ctx) error { var user struct { Name string `json:"name"` Email string `json:"email"` } if err := c.BindJSON(&user); err != nil { return c.JSON(400, map[string]string{"error": "invalid request"}) } // Create user... return c.JSON(201, user) }) ``` ### PUT/DELETE ```tsx theme={null} // PUT await fetch(`/api/users/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updates) }) // DELETE await fetch(`/api/users/${id}`, { method: 'DELETE' }) ``` ## Error Handling ### Frontend ```tsx theme={null} async function fetchUsers() { try { const response = await fetch('/api/users') if (!response.ok) { const error = await response.json() throw new Error(error.message || 'Request failed') } return await response.json() } catch (error) { console.error('API error:', error) throw error } } ``` ### Backend ```go theme={null} app.Get("/api/users", func(c *mizu.Ctx) error { users, err := db.GetUsers() if err != nil { return c.JSON(500, map[string]string{ "error": "failed to fetch users", }) } return c.JSON(200, users) }) ``` ## Authentication ### JWT Token ```tsx theme={null} // Store token localStorage.setItem('auth_token', token) // Send with requests const response = await fetch('/api/protected', { headers: { 'Authorization': `Bearer ${localStorage.getItem('auth_token')}` } }) ``` ```go theme={null} app.Get("/api/protected", func(c *mizu.Ctx) error { token := c.Request().Header.Get("Authorization") if token == "" { return c.JSON(401, map[string]string{"error": "unauthorized"}) } // Verify token... return c.JSON(200, data) }) ``` Or use the JWT middleware: ```go theme={null} import "github.com/go-mizu/mizu/middlewares/jwt" app.Use(jwt.WithOptions(jwt.Options{ Secret: []byte("your-secret"), TokenLookup: "header:Authorization", })) ``` ### HTTP-Only Cookies ```go theme={null} app.Post("/api/login", func(c *mizu.Ctx) error { // Verify credentials... http.SetCookie(c.Writer(), &http.Cookie{ Name: "auth_token", Value: token, HttpOnly: true, Secure: true, SameSite: http.SameSiteStrictMode, }) return c.JSON(200, map[string]string{"message": "logged in"}) }) ``` Frontend doesn't need to handle the cookie - it's sent automatically. ## CORS in Development Mizu adds CORS headers automatically in dev mode: ``` Access-Control-Allow-Origin: * ``` For production, use the CORS middleware: ```go theme={null} import "github.com/go-mizu/mizu/middlewares/cors" app.Use(cors.WithOptions(cors.Options{ AllowedOrigins: []string{"https://yourdomain.com"}, AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"}, AllowedHeaders: []string{"Content-Type", "Authorization"}, })) ``` ## TypeScript Types Share types between frontend and backend: ```ts theme={null} // shared/types.ts export interface User { id: number name: string email: string } export interface CreateUserRequest { name: string email: string } ``` Frontend: ```tsx theme={null} import type { User, CreateUserRequest } from './shared/types' const users = await fetch('/api/users') .then(r => r.json()) as User[] const create = async (req: CreateUserRequest) => { return await fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(req) }).then(r => r.json()) as User } ``` ## API Client Create a reusable API client: ```ts theme={null} class API { private baseURL: string constructor(baseURL = '/api') { this.baseURL = baseURL } private async request( endpoint: string, options?: RequestInit ): Promise { const response = await fetch(`${this.baseURL}${endpoint}`, { ...options, headers: { 'Content-Type': 'application/json', ...options?.headers, }, }) if (!response.ok) { const error = await response.json() throw new Error(error.message) } return response.json() } get(endpoint: string) { return this.request(endpoint) } post(endpoint: string, data: any) { return this.request(endpoint, { method: 'POST', body: JSON.stringify(data), }) } put(endpoint: string, data: any) { return this.request(endpoint, { method: 'PUT', body: JSON.stringify(data), }) } delete(endpoint: string) { return this.request(endpoint, { method: 'DELETE', }) } } export const api = new API() ``` Use: ```tsx theme={null} import { api } from './api' const users = await api.get('/users') const user = await api.post('/users', { name: 'Alice', email: 'alice@example.com' }) ``` ## Loading States ```tsx theme={null} function Users() { const [users, setUsers] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) useEffect(() => { api.get('/users') .then(setUsers) .catch(err => setError(err.message)) .finally(() => setLoading(false)) }, []) if (loading) return
    Loading...
    if (error) return
    Error: {error}
    return (
      {users.map(user => (
    • {user.name}
    • ))}
    ) } ``` ## Pagination ### Backend ```go theme={null} app.Get("/api/users", func(c *mizu.Ctx) error { page := c.QueryInt("page", 1) limit := c.QueryInt("limit", 20) users, total := db.GetUsers(page, limit) return c.JSON(200, map[string]any{ "data": users, "page": page, "limit": limit, "total": total, }) }) ``` ### Frontend ```tsx theme={null} const [page, setPage] = useState(1) const limit = 20 const { data, total } = await api.get(`/users?page=${page}&limit=${limit}`) ``` ## File Upload ### Frontend ```tsx theme={null} async function uploadFile(file: File) { const formData = new FormData() formData.append('file', file) const response = await fetch('/api/upload', { method: 'POST', body: formData, // Don't set Content-Type header }) return response.json() } ``` ### Backend ```go theme={null} app.Post("/api/upload", func(c *mizu.Ctx) error { file, header, err := c.Request().FormFile("file") if err != nil { return c.JSON(400, map[string]string{"error": "no file"}) } defer file.Close() // Save file... return c.JSON(200, map[string]string{ "filename": header.Filename, }) }) ``` ## Next Steps Architecture decisions API security best practices CORS configuration JWT authentication # API Reference Source: https://docs.go-mizu.dev/frontend/api-reference Complete API reference for the frontend middleware. Complete reference for the frontend middleware API. ## Functions ### New Creates frontend middleware with default options. ```go theme={null} func New(root string) mizu.Middleware ``` **Parameters:** * `root` (string): Directory containing built frontend files **Returns:** `mizu.Middleware` **Example:** ```go theme={null} app.Use(frontend.New("./dist")) ``` ### Dev Creates development-only middleware that proxies to dev server. ```go theme={null} func Dev(devServerURL string) mizu.Middleware ``` **Parameters:** * `devServerURL` (string): URL of frontend dev server **Returns:** `mizu.Middleware` **Example:** ```go theme={null} app.Use(frontend.Dev("http://localhost:5173")) ``` ### WithFS Creates production middleware with embedded filesystem. ```go theme={null} func WithFS(fsys fs.FS) mizu.Middleware ``` **Parameters:** * `fsys` (fs.FS): Embedded filesystem containing frontend files **Returns:** `mizu.Middleware` **Example:** ```go theme={null} //go:embed all:dist var distFS embed.FS dist, _ := fs.Sub(distFS, "dist") app.Use(frontend.WithFS(dist)) ``` ### WithOptions Creates middleware with full configuration. ```go theme={null} func WithOptions(opts Options) mizu.Middleware ``` **Parameters:** * `opts` (Options): Configuration options **Returns:** `mizu.Middleware` **Example:** ```go theme={null} app.Use(frontend.WithOptions(frontend.Options{ Mode: frontend.ModeAuto, Root: "./dist", DevServer: "http://localhost:5173", IgnorePaths: []string{"/api"}, })) ``` ## Types ### Options Configuration for the frontend middleware. ```go theme={null} type Options struct { // Mode determines serving behavior Mode Mode // Production options Root string FS fs.FS Index string Prefix string IgnorePaths []string CacheControl CacheConfig // Development options DevServer string DevServerTimeout time.Duration ProxyWebSocket bool // Advanced options Manifest string InjectEnv []string InjectMeta map[string]string SecurityHeaders bool SourceMaps bool ServiceWorker string ErrorHandler func(*mizu.Ctx, error) error NotFoundHandler func(*mizu.Ctx) error } ``` **Fields:** **Mode:** * Type: `Mode` * Default: `ModeAuto` * Description: Controls how middleware operates **Root:** * Type: `string` * Default: `"dist"` * Description: Directory containing built frontend files **FS:** * Type: `fs.FS` * Default: `nil` * Description: Embedded filesystem (takes precedence over Root) **Index:** * Type: `string` * Default: `"index.html"` * Description: Fallback file for SPA routing **Prefix:** * Type: `string` * Default: `""` * Description: URL prefix for serving frontend **IgnorePaths:** * Type: `[]string` * Default: `[]string{"/api", "/health", "/metrics"}` * Description: Paths that bypass frontend middleware **CacheControl:** * Type: `CacheConfig` * Default: See CacheConfig defaults * Description: Caching behavior configuration **DevServer:** * Type: `string` * Default: `"http://localhost:5173"` * Description: URL of frontend dev server **DevServerTimeout:** * Type: `time.Duration` * Default: `30 * time.Second` * Description: Timeout for dev server requests **ProxyWebSocket:** * Type: `bool` * Default: `true` * Description: Enable WebSocket proxying for HMR **Manifest:** * Type: `string` * Default: `""` * Description: Path to build manifest file **InjectEnv:** * Type: `[]string` * Default: `nil` * Description: Environment variables to inject into HTML **InjectMeta:** * Type: `map[string]string` * Default: `nil` * Description: Meta tags to inject into HTML **SecurityHeaders:** * Type: `bool` * Default: `true` * Description: Enable default security headers **SourceMaps:** * Type: `bool` * Default: `false` * Description: Allow serving source map files **ServiceWorker:** * Type: `string` * Default: `""` * Description: Path to service worker file **ErrorHandler:** * Type: `func(*mizu.Ctx, error) error` * Default: `nil` * Description: Custom error handler **NotFoundHandler:** * Type: `func(*mizu.Ctx) error` * Default: `nil` * Description: Handler called before SPA fallback ### Mode Operating mode for the middleware. ```go theme={null} type Mode string const ( ModeDev Mode = "dev" ModeProduction Mode = "production" ModeAuto Mode = "auto" ) ``` **Values:** * `ModeDev`: Always proxy to dev server * `ModeProduction`: Always serve static files * `ModeAuto`: Auto-detect based on environment ### CacheConfig Caching behavior configuration. ```go theme={null} type CacheConfig struct { HashedAssets time.Duration UnhashedAssets time.Duration HTML time.Duration Patterns map[string]time.Duration } ``` **Fields:** **HashedAssets:** * Type: `time.Duration` * Default: `365 * 24 * time.Hour` (1 year) * Description: Cache duration for files with content hash **UnhashedAssets:** * Type: `time.Duration` * Default: `7 * 24 * time.Hour` (1 week) * Description: Cache duration for files without hash **HTML:** * Type: `time.Duration` * Default: `0` (no-cache) * Description: Cache duration for HTML files **Patterns:** * Type: `map[string]time.Duration` * Default: `nil` * Description: Custom cache durations for file patterns **Example:** ```go theme={null} CacheConfig{ HashedAssets: 365 * 24 * time.Hour, UnhashedAssets: 7 * 24 * time.Hour, HTML: 0, Patterns: map[string]time.Duration{ "*.woff2": 30 * 24 * time.Hour, "*.png": 14 * 24 * time.Hour, }, } ``` ## Manifest API ### LoadManifest Loads a build manifest from filesystem. ```go theme={null} func LoadManifest(fsys fs.FS, path string) (*Manifest, error) ``` **Parameters:** * `fsys` (fs.FS): Filesystem containing manifest * `path` (string): Path to manifest file **Returns:** * `*Manifest`: Loaded manifest * `error`: Error if loading fails **Example:** ```go theme={null} manifest, err := frontend.LoadManifest(distFS, ".vite/manifest.json") ``` ### Manifest Methods ```go theme={null} // Get output file for entry point func (m *Manifest) Entry(name string) string // Get URL for asset func (m *Manifest) Asset(path string) string // Get module preload paths func (m *Manifest) Preloads(entry string) []string // Get CSS files for entry func (m *Manifest) CSS(entry string) []string // Generate script tag func (m *Manifest) ScriptTag(entry string) template.HTML // Generate CSS tags func (m *Manifest) CSSTags(entry string) template.HTML // Generate preload tags func (m *Manifest) PreloadTags(entry string) template.HTML // Generate all tags (CSS + preloads + script) func (m *Manifest) EntryTags(entry string) template.HTML // Get template helper functions func (m *Manifest) ViewHelpers() template.FuncMap ``` ## Injection Functions ### InjectEnv Injects environment variables into HTML. ```go theme={null} func InjectEnv(html []byte, vars []string) []byte ``` **Parameters:** * `html` (\[]byte): HTML content * `vars` (\[]string): Environment variable names to inject **Returns:** Modified HTML with injected script ### InjectMeta Injects meta tags into HTML. ```go theme={null} func InjectMeta(html []byte, meta map[string]string) []byte ``` **Parameters:** * `html` (\[]byte): HTML content * `meta` (map\[string]string): Meta tags (name → content) **Returns:** Modified HTML with injected meta tags ### InjectPreload Injects preload link tags into HTML. ```go theme={null} func InjectPreload(html []byte, assets []PreloadAsset) []byte ``` **Parameters:** * `html` (\[]byte): HTML content * `assets` (\[]PreloadAsset): Assets to preload **Returns:** Modified HTML with preload tags ## Constants ### Default Values ```go theme={null} const ( DefaultRoot = "dist" DefaultIndex = "index.html" DefaultDevServer = "http://localhost:5173" DefaultDevTimeout = 30 * time.Second DefaultHashedCache = 365 * 24 * time.Hour // 1 year DefaultUnhashedCache = 7 * 24 * time.Hour // 1 week DefaultHTMLCache = 0 // no-cache ) ``` ### Default Ignore Paths ```go theme={null} var DefaultIgnorePaths = []string{"/api", "/health", "/metrics"} ``` ### Default Security Headers ```go theme={null} var DefaultSecurityHeaders = map[string]string{ "X-Content-Type-Options": "nosniff", "X-Frame-Options": "SAMEORIGIN", "X-XSS-Protection": "1; mode=block", "Referrer-Policy": "strict-origin-when-cross-origin", } ``` ## Next Steps Configuration guide Framework adapters Code examples # Building Source: https://docs.go-mizu.dev/frontend/building Build and optimize your frontend for production deployment. Building your frontend creates optimized, production-ready files that Mizu serves efficiently. ## Build Process ### Using Makefile ```bash theme={null} make build ``` This runs: 1. `cd frontend && npm run build` - Builds frontend 2. `go build -o ./bin/server cmd/server/main.go` - Builds Go binary with embedded frontend ### Manual Build ```bash theme={null} # Build frontend cd frontend npm run build # Build Go binary cd .. go build -o ./bin/server cmd/server/main.go ``` ## What Happens During Build ### 1. Frontend Build (Vite) ```bash theme={null} npm run build ``` **Process:** * Transpiles TypeScript to JavaScript * Bundles modules together * Minifies JavaScript and CSS * Optimizes images * Adds content hashes to filenames * Generates source maps * Creates build manifest **Output:** ``` dist/ ├── index.html ├── assets/ │ ├── main.abc123.js # Minified, hashed │ ├── main.def456.css # Minified, hashed │ └── vendor.xyz789.js # Code-split chunk └── .vite/ └── manifest.json # Build manifest ``` ### 2. Go Build with Embed ```go theme={null} //go:embed all:../../dist var distFS embed.FS ``` The Go compiler: * Embeds `dist/` into the binary * Creates self-contained executable * No external dependencies needed ## Build Configuration ### Vite Configuration ```ts theme={null} // vite.config.ts import { defineConfig } from 'vite' export default defineConfig({ build: { outDir: '../dist', // Output directory emptyOutDir: true, // Clean before build sourcemap: false, // Disable source maps in production minify: 'esbuild', // Minification method target: 'es2015', // Browser target rollupOptions: { output: { // Code splitting manualChunks: { 'react-vendor': ['react', 'react-dom'], 'router': ['react-router-dom'], }, // Asset naming with hashes entryFileNames: 'assets/[name].[hash].js', chunkFileNames: 'assets/[name].[hash].js', assetFileNames: 'assets/[name].[hash].[ext]', }, }, // Size warning threshold chunkSizeWarningLimit: 1000, // 1000 KB }, }) ``` ### Environment Variables ```bash theme={null} # .env.production VITE_API_URL=https://api.production.com VITE_ANALYTICS_ID=GA-123456 ``` ```ts theme={null} const apiUrl = import.meta.env.VITE_API_URL ``` ## Build Optimization ### 1. Code Splitting Split code into smaller chunks: ```ts theme={null} // React const About = lazy(() => import('./pages/About')) // Vue const About = defineAsyncComponent(() => import('./pages/About.vue')) ``` ### 2. Tree Shaking Import only what you need: ```ts theme={null} // ❌ Imports entire library import _ from 'lodash' // ✅ Imports only what's used import { debounce } from 'lodash-es' ``` ### 3. Minification Vite minifies automatically: ```js theme={null} // Before function calculateTotal(items) { return items.reduce((sum, item) => sum + item.price, 0) } // After const calculateTotal=e=>e.reduce((e,t)=>e+t.price,0) ``` ### 4. Asset Optimization ```bash theme={null} # Install image optimizer npm install -D vite-plugin-imagemin # Add to vite.config.ts import viteImagemin from 'vite-plugin-imagemin' export default { plugins: [ viteImagemin({ gifsicle: { optimizationLevel: 7 }, optipng: { optimizationLevel: 7 }, mozjpeg: { quality: 80 }, svgo: { plugins: [{ removeViewBox: false }] }, webp: { quality: 80 }, }), ], } ``` ## Multi-Environment Builds ### Environment Files ``` .env # All environments .env.development # Development only .env.production # Production only .env.staging # Staging only ``` ### Build Scripts ```json theme={null} { "scripts": { "build": "vite build", "build:staging": "vite build --mode staging", "build:production": "vite build --mode production" } } ``` ## Build Analysis ### Bundle Size ```bash theme={null} npm run build # Output shows sizes dist/assets/main.abc123.js 245.67 kB dist/assets/vendor.xyz789.js 156.32 kB ``` ### Visualize Bundle ```bash theme={null} npm install -D rollup-plugin-visualizer ``` ```ts theme={null} import { visualizer } from 'rollup-plugin-visualizer' export default { plugins: [ visualizer({ filename: 'stats.html' }) ] } ``` Run build and open `stats.html` to see bundle composition. ## CI/CD Integration ### GitHub Actions ```yaml theme={null} # .github/workflows/deploy.yml name: Deploy on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Node uses: actions/setup-node@v3 with: node-version: '18' - name: Setup Go uses: actions/setup-go@v4 with: go-version: '1.22' - name: Install dependencies run: cd frontend && npm ci - name: Build frontend run: cd frontend && npm run build - name: Build server run: go build -o ./bin/server cmd/server/main.go - name: Deploy run: ./deploy.sh ``` ## Troubleshooting ### Build Fails ```bash theme={null} # Clear cache and reinstall rm -rf node_modules package-lock.json npm install # Clear Vite cache rm -rf node_modules/.vite ``` ### Large Bundle Size 1. Check bundle analysis 2. Enable code splitting 3. Use dynamic imports 4. Remove unused dependencies ### Missing Assets Ensure assets are in `public/` or imported in code: ```ts theme={null} // ❌ Won't be included // // ✅ Will be included import logo from './logo.png' // ``` ## Next Steps Deploy your application Production optimization Caching strategies # Caching Source: https://docs.go-mizu.dev/frontend/caching Comprehensive caching strategies for optimal performance. Proper caching is crucial for frontend performance. Mizu implements intelligent caching based on asset types, content fingerprinting, and best practices. ## Overview The frontend middleware automatically applies caching based on three factors: 1. **File type** (HTML, JS, CSS, images, etc.) 2. **Content fingerprinting** (hash in filename) 3. **Custom patterns** (your configuration) ## Default Caching Strategy | Asset Type | Pattern | Cache Duration | Cache-Control | | --------------- | ------------- | -------------- | ------------------------------------- | | Hashed assets | `*.abc123.js` | 1 year | `public, max-age=31536000, immutable` | | Unhashed assets | `logo.png` | 1 week | `public, max-age=604800` | | HTML files | `*.html` | None | `no-cache, no-store, must-revalidate` | | Source maps | `*.map` | None | `no-cache` | ## How Content Hashing Works ### Build Tools Generate Hashes When you build your frontend, tools like Vite add content hashes to filenames: ``` Before build: main.js styles.css After build: main.a1b2c3d4.js ← Hash added styles.e5f6g7h8.css ← Hash added ``` The hash is based on file contents. If the file changes, the hash changes: ``` main.a1b2c3d4.js → main.x9y8z7w6.js ``` ### Why This Matters **Old approach (without hashing):** ```html theme={null} ``` Problem: Browsers cache `main.js`. When you update it, users might still see the old cached version. **New approach (with hashing):** ```html theme={null} ``` Benefit: When you update the file, the filename changes: ```html theme={null} ``` Browsers see a new filename and fetch the new file. The old file stays cached harmlessly. ## Asset Classification Mizu automatically classifies assets: ```go theme={null} // Hashed assets (detected by pattern) app.a1b2c3.js → 1 year cache, immutable vendor.xyz789.css → 1 year cache, immutable logo.abc123.png → 1 year cache, immutable // Unhashed assets logo.png → 1 week cache favicon.ico → 1 week cache // HTML files index.html → no-cache about.html → no-cache // Source maps app.js.map → no-cache (blocked by default) ``` ### Hash Detection Pattern Mizu looks for this pattern in filenames: ``` [._-][a-fA-F0-9]{6,}$ ``` Examples that match: * `main.a1b2c3.js` ✅ * `vendor-xyz789.css` ✅ * `chunk.ABC123DEF.js` ✅ * `logo_12ab34cd.png` ✅ Examples that don't match: * `main.js` ❌ (no hash) * `version-1.2.3.js` ❌ (not a hex hash) * `user-123.png` ❌ (too short) ## Custom Cache Configuration ### Override Defaults ```go theme={null} app.Use(frontend.WithOptions(frontend.Options{ Mode: frontend.ModeProduction, Root: "./dist", CacheControl: frontend.CacheConfig{ HashedAssets: 180 * 24 * time.Hour, // 6 months instead of 1 year UnhashedAssets: 24 * time.Hour, // 1 day instead of 1 week HTML: 0, // no-cache (default) }, })) ``` ### Pattern-Based Caching Cache specific file types differently: ```go theme={null} app.Use(frontend.WithOptions(frontend.Options{ Mode: frontend.ModeProduction, Root: "./dist", CacheControl: frontend.CacheConfig{ Patterns: map[string]time.Duration{ // Fonts: cache for 30 days "*.woff2": 30 * 24 * time.Hour, "*.woff": 30 * 24 * time.Hour, "*.ttf": 30 * 24 * time.Hour, // Images: cache for 14 days "*.jpg": 14 * 24 * time.Hour, "*.jpeg": 14 * 24 * time.Hour, "*.png": 14 * 24 * time.Hour, "*.svg": 14 * 24 * time.Hour, "*.webp": 14 * 24 * time.Hour, // Manifests and configs: no cache "manifest.json": 0, "robots.txt": 0, "sitemap.xml": 0, }, }, })) ``` **Pattern matching order:** 1. Check custom patterns first 2. If no match, check if file has hash 3. If hashed → use `HashedAssets` duration 4. If unhashed → use `UnhashedAssets` duration 5. If HTML → use `HTML` duration ### Environment-Specific Caching ```go theme={null} func getCacheConfig(env string) frontend.CacheConfig { if env == "production" { return frontend.CacheConfig{ HashedAssets: 365 * 24 * time.Hour, UnhashedAssets: 7 * 24 * time.Hour, } } // Shorter cache for staging return frontend.CacheConfig{ HashedAssets: 24 * time.Hour, UnhashedAssets: 1 * time.Hour, } } app.Use(frontend.WithOptions(frontend.Options{ Mode: frontend.ModeProduction, Root: "./dist", CacheControl: getCacheConfig(os.Getenv("ENV")), })) ``` ## Cache-Control Directives ### immutable Hashed assets get the `immutable` directive: ``` Cache-Control: public, max-age=31536000, immutable ``` **What it means:** * `public`: Can be cached by browsers and CDNs * `max-age=31536000`: Cache for 1 year (in seconds) * `immutable`: File will never change (safe to cache forever) Modern browsers skip revalidation for immutable resources. ### no-cache vs no-store HTML files get aggressive no-cache headers: ``` Cache-Control: no-cache, no-store, must-revalidate Pragma: no-cache Expires: 0 ``` **What each means:** * `no-cache`: Revalidate before using cached copy * `no-store`: Don't cache at all * `must-revalidate`: Obey the rules strictly * `Pragma: no-cache`: HTTP/1.0 compatibility * `Expires: 0`: Old-style expiration ## CDN Caching When using a CDN (CloudFlare, Fastly, etc.): ```go theme={null} app.Use(frontend.WithOptions(frontend.Options{ Mode: frontend.ModeProduction, Root: "./dist", CacheControl: frontend.CacheConfig{ // Aggressive caching for CDN HashedAssets: 365 * 24 * time.Hour, UnhashedAssets: 7 * 24 * time.Hour, Patterns: map[string]time.Duration{ // Cache fonts for a very long time "*.woff2": 365 * 24 * time.Hour, }, }, })) ``` **CDN benefits:** * Files cached at edge locations * Faster delivery to users * Reduced origin server load * Mizu's cache headers work automatically ## Best Practices ### 1. Always Use Asset Fingerprinting Ensure your build tool fingerprints assets: **Vite:** ```ts theme={null} // vite.config.ts export default { build: { rollupOptions: { output: { entryFileNames: 'assets/[name].[hash].js', chunkFileNames: 'assets/[name].[hash].js', assetFileNames: 'assets/[name].[hash].[ext]' } } } } ``` **Angular:** ```json theme={null} { "architect": { "build": { "configurations": { "production": { "outputHashing": "all" } } } } } ``` ### 2. Never Cache HTML Files Always set HTML caching to 0: ```go theme={null} CacheControl: frontend.CacheConfig{ HTML: 0, // Critical! } ``` **Why:** HTML files reference the hashed assets. If HTML is cached, users might request old asset filenames that no longer exist. ### 3. Use Longer Cache for Fonts Fonts rarely change: ```go theme={null} Patterns: map[string]time.Duration{ "*.woff2": 30 * 24 * time.Hour, // 30 days } ``` ### 4. Test Cache Headers Check headers with curl: ```bash theme={null} curl -I https://yourdomain.com/assets/main.abc123.js ``` Should show: ``` HTTP/2 200 cache-control: public, max-age=31536000, immutable ``` ### 5. Monitor Cache Hit Rates Use CDN analytics to track: * Cache hit rate (aim for >90%) * Bandwidth saved * Origin requests (should be minimal) ## Debugging Cache Issues ### Clear Browser Cache **Chrome:** ``` Cmd/Ctrl + Shift + R (hard refresh) ``` **Programmatically:** ```js theme={null} // In browser console caches.keys().then(names => { names.forEach(name => caches.delete(name)) }) ``` ### Verify Cache Headers ```bash theme={null} # Check a hashed asset curl -I localhost:3000/assets/main.abc123.js # Should show long cache cache-control: public, max-age=31536000, immutable ``` ```bash theme={null} # Check HTML curl -I localhost:3000/ # Should show no-cache cache-control: no-cache, no-store, must-revalidate ``` ### Common Issues **Problem:** Users see old version after deployment **Solution:** Check that: 1. HTML files have `cache-control: no-cache` 2. Asset filenames include content hash 3. Build generated new hashes **Problem:** Assets not caching **Solution:** Check that: 1. Filenames include hash pattern 2. Server is returning cache headers 3. HTTPS is enabled (required for some cache APIs) ## Next Steps Production optimization guide Security headers and best practices Build manifest integration Deploy to CDN # Configuration Source: https://docs.go-mizu.dev/frontend/configuration Complete reference for frontend middleware configuration options. The frontend middleware offers extensive configuration options to handle various deployment scenarios, frameworks, and requirements. ## Basic Configuration ### Minimal Setup The simplest configuration: ```go theme={null} app.Use(frontend.New("./dist")) ``` This uses sensible defaults: * Auto-detects mode based on `MIZU_ENV` * Serves from `./dist` in production * Proxies to `http://localhost:5173` in development * Ignores `/api`, `/health`, `/metrics` ### With Dev Server Specify both production and development: ```go theme={null} app.Use(frontend.WithOptions(frontend.Options{ Mode: frontend.ModeAuto, Root: "./dist", DevServer: "http://localhost:5173", })) ``` ### Embedded Filesystem Use embedded files for single-binary deployment: ```go theme={null} //go:embed all:dist var distFS embed.FS func setup() { dist, _ := fs.Sub(distFS, "dist") app.Use(frontend.WithFS(dist)) } ``` ## Options Reference ### Mode Controls how the middleware operates. ```go theme={null} type Mode string const ( ModeDev Mode = "dev" // Always proxy to dev server ModeProduction Mode = "production" // Always serve static files ModeAuto Mode = "auto" // Auto-detect from env ) ``` **Usage:** ```go theme={null} app.Use(frontend.WithOptions(frontend.Options{ Mode: frontend.ModeAuto, // Default })) ``` **Auto-detection logic:** Environment variable checked in order: 1. `MIZU_ENV` 2. `GO_ENV` 3. `ENV` If value is `"production"` or `"prod"` (case-insensitive) → Production mode Otherwise → Development mode ### Root Directory containing built frontend files (production only). ```go theme={null} app.Use(frontend.WithOptions(frontend.Options{ Root: "./dist", // Default })) ``` **Path resolution:** * Relative paths are relative to working directory * Absolute paths work as expected **Common paths:** * Vite: `"./dist"` * Angular: `"./dist/my-app/browser"` * Next.js: `"./out"` * Nuxt: `"./dist"` ### FS (Embedded Filesystem) Embedded filesystem for production builds (takes precedence over `Root`). ```go theme={null} //go:embed all:dist var distFS embed.FS func setup() { dist, _ := fs.Sub(distFS, "dist") app.Use(frontend.WithOptions(frontend.Options{ FS: dist, // Use embedded files })) } ``` **Why use `fs.Sub`?** The embed directive includes the directory name: ``` distFS contains: dist/ index.html assets/ ``` Using `fs.Sub` extracts the subdirectory: ``` dist contains: index.html assets/ ``` Now `index.html` is at the root level as expected. ### Index The fallback HTML file for SPA routing. ```go theme={null} app.Use(frontend.WithOptions(frontend.Options{ Index: "index.html", // Default })) ``` **When is it used?** * Request path is `/` or empty * Requested file doesn't exist (SPA fallback) * Requested path is a directory ### DevServer URL of the frontend development server. ```go theme={null} app.Use(frontend.WithOptions(frontend.Options{ DevServer: "http://localhost:5173", // Default (Vite) })) ``` **Common dev server URLs:** * Vite: `"http://localhost:5173"` * Angular: `"http://localhost:4200"` * Next.js: `"http://localhost:3000"` * Create React App: `"http://localhost:3000"` ### DevServerTimeout Timeout for requests to the dev server. ```go theme={null} app.Use(frontend.WithOptions(frontend.Options{ DevServerTimeout: 30 * time.Second, // Default })) ``` Increase for slow dev servers: ```go theme={null} app.Use(frontend.WithOptions(frontend.Options{ DevServerTimeout: 60 * time.Second, })) ``` ### ProxyWebSocket Enable WebSocket proxying for HMR. ```go theme={null} app.Use(frontend.WithOptions(frontend.Options{ ProxyWebSocket: true, // Default })) ``` Disable if you don't need HMR: ```go theme={null} app.Use(frontend.WithOptions(frontend.Options{ ProxyWebSocket: false, })) ``` ### Prefix URL prefix for serving the frontend. ```go theme={null} app.Use(frontend.WithOptions(frontend.Options{ Prefix: "/app", // Serve from /app/* })) ``` **Routing:** * `/app` → `index.html` * `/app/about` → `index.html` (SPA fallback) * `/app/assets/main.js` → `assets/main.js` * `/` → Not handled by frontend middleware **Frontend router configuration required:** ```tsx theme={null} // React Router ``` ```ts theme={null} // Vite config export default { base: '/app/', } ``` ### IgnorePaths Paths that bypass the frontend middleware and go to Go handlers. ```go theme={null} app.Use(frontend.WithOptions(frontend.Options{ IgnorePaths: []string{"/api", "/health", "/metrics"}, // Default })) ``` **Add custom paths:** ```go theme={null} app.Use(frontend.WithOptions(frontend.Options{ IgnorePaths: []string{"/api", "/auth", "/webhooks"}, })) ``` **Disable all:** ```go theme={null} app.Use(frontend.WithOptions(frontend.Options{ IgnorePaths: []string{}, // No automatic ignores })) ``` ### CacheControl Configure caching behavior for different asset types. ```go theme={null} app.Use(frontend.WithOptions(frontend.Options{ CacheControl: frontend.CacheConfig{ HashedAssets: 365 * 24 * time.Hour, // Default: 1 year UnhashedAssets: 7 * 24 * time.Hour, // Default: 1 week HTML: 0, // Default: no-cache Patterns: map[string]time.Duration{ "*.woff2": 30 * 24 * time.Hour, }, }, })) ``` **CacheConfig fields:** * **HashedAssets**: Files with content hash in name (e.g., `main.abc123.js`) * **UnhashedAssets**: Files without hash (e.g., `logo.png`) * **HTML**: HTML files (`.html`) * **Patterns**: Custom patterns override defaults See [Caching Strategy](/frontend/caching) for details. ### SecurityHeaders Enable automatic security headers. ```go theme={null} app.Use(frontend.WithOptions(frontend.Options{ SecurityHeaders: true, // Default })) ``` **Headers added:** ``` X-Content-Type-Options: nosniff X-Frame-Options: SAMEORIGIN X-XSS-Protection: 1; mode=block Referrer-Policy: strict-origin-when-cross-origin ``` **Disable if using helmet:** ```go theme={null} app.Use(helmet.Default()) app.Use(frontend.WithOptions(frontend.Options{ SecurityHeaders: false, // Avoid duplicates })) ``` ### SourceMaps Allow serving source map files (`.js.map`, `.css.map`). ```go theme={null} app.Use(frontend.WithOptions(frontend.Options{ SourceMaps: false, // Default (blocked in production) })) ``` **Enable for debugging:** ```go theme={null} app.Use(frontend.WithOptions(frontend.Options{ SourceMaps: true, })) ``` **Conditional source maps:** ```go theme={null} isProduction := os.Getenv("MIZU_ENV") == "production" app.Use(frontend.WithOptions(frontend.Options{ SourceMaps: !isProduction, // Only in development })) ``` ### Manifest Path to build manifest for asset mapping. ```go theme={null} app.Use(frontend.WithOptions(frontend.Options{ Manifest: ".vite/manifest.json", })) ``` The manifest is used for: * Asset fingerprinting * Module preloading * CSS injection * Template helpers See [Build Manifest](/frontend/manifest) for details. ### InjectEnv Inject server-side environment variables into the frontend. ```go theme={null} app.Use(frontend.WithOptions(frontend.Options{ InjectEnv: []string{"API_URL", "ANALYTICS_ID"}, })) ``` Variables are exposed as `window.__ENV__`: ```tsx theme={null} // Frontend code const apiUrl = window.__ENV__.API_URL; ``` **Security warning:** Only inject non-sensitive values. These are visible to users. See [Environment Injection](/frontend/env-injection) for details. ### InjectMeta Inject custom meta tags into HTML. ```go theme={null} app.Use(frontend.WithOptions(frontend.Options{ InjectMeta: map[string]string{ "description": "My awesome app", "keywords": "go, mizu, web", }, })) ``` Injects: ```html theme={null} ``` ### ServiceWorker Path to service worker file for PWA support. ```go theme={null} app.Use(frontend.WithOptions(frontend.Options{ ServiceWorker: "sw.js", })) ``` Proper headers are added for service worker scope. See [Service Workers](/frontend/service-workers) for details. ### ErrorHandler Custom error handler for frontend errors. ```go theme={null} app.Use(frontend.WithOptions(frontend.Options{ ErrorHandler: func(c *mizu.Ctx, err error) error { c.Logger().Error("frontend error", "error", err) return c.JSON(500, map[string]string{ "error": "Internal Server Error", }) }, })) ``` ### NotFoundHandler Custom handler called before SPA fallback. ```go theme={null} app.Use(frontend.WithOptions(frontend.Options{ NotFoundHandler: func(c *mizu.Ctx) error { // Log 404s c.Logger().Warn("file not found", "path", c.Request().URL.Path) // Return nil to continue to SPA fallback return nil // Or return error to skip fallback and respond // return c.JSON(404, map[string]string{"error": "not found"}) }, })) ``` ## Configuration Examples ### Complete Production Setup ```go theme={null} package server import ( "embed" "io/fs" "os" "time" "github.com/go-mizu/mizu" "github.com/go-mizu/mizu/frontend" ) //go:embed all:../../dist var distFS embed.FS func New() *mizu.App { app := mizu.New() // API routes first setupAPIRoutes(app) // Frontend configuration dist, _ := fs.Sub(distFS, "dist") app.Use(frontend.WithOptions(frontend.Options{ // Mode Mode: frontend.ModeProduction, // Files FS: dist, Index: "index.html", // Routing Prefix: "", IgnorePaths: []string{"/api"}, // Caching CacheControl: frontend.CacheConfig{ HashedAssets: 365 * 24 * time.Hour, UnhashedAssets: 7 * 24 * time.Hour, HTML: 0, Patterns: map[string]time.Duration{ "*.woff2": 30 * 24 * time.Hour, }, }, // Security SecurityHeaders: true, SourceMaps: false, // Environment InjectEnv: []string{"API_URL"}, InjectMeta: map[string]string{ "description": "My app", }, })) return app } ``` ### Development-Optimized Setup ```go theme={null} func New() *mizu.App { app := mizu.New() setupAPIRoutes(app) app.Use(frontend.WithOptions(frontend.Options{ Mode: frontend.ModeDev, DevServer: "http://localhost:5173", DevServerTimeout: 60 * time.Second, ProxyWebSocket: true, IgnorePaths: []string{"/api"}, })) return app } ``` ### Auto-Switching Setup ```go theme={null} func New() *mizu.App { app := mizu.New() setupAPIRoutes(app) dist, _ := fs.Sub(distFS, "dist") app.Use(frontend.WithOptions(frontend.Options{ // Auto-detect based on MIZU_ENV Mode: frontend.ModeAuto, // Production settings FS: dist, Root: "./dist", // Fallback if FS fails // Development settings DevServer: "http://localhost:5173", ProxyWebSocket: true, // Common settings IgnorePaths: []string{"/api"}, InjectEnv: []string{"API_URL"}, })) return app } ``` ### Multi-Environment Setup ```go theme={null} type Config struct { Environment string Port string DevPort string } func New(cfg *Config) *mizu.App { app := mizu.New() setupAPIRoutes(app) opts := frontend.Options{ Mode: frontend.ModeAuto, Root: "./dist", DevServer: "http://localhost:" + cfg.DevPort, IgnorePaths: []string{"/api"}, } // Environment-specific configuration switch cfg.Environment { case "production": dist, _ := fs.Sub(distFS, "dist") opts.FS = dist opts.SourceMaps = false opts.SecurityHeaders = true opts.InjectEnv = []string{"API_URL"} case "staging": opts.SourceMaps = true // Enable for debugging opts.SecurityHeaders = true opts.InjectEnv = []string{"API_URL", "DEBUG"} case "development": opts.Mode = frontend.ModeDev opts.DevServerTimeout = 60 * time.Second } app.Use(frontend.WithOptions(opts)) return app } ``` ## Environment Variables Configure via environment: ```bash theme={null} # Mode detection export MIZU_ENV=production # or development export GO_ENV=production # alternative export ENV=production # alternative # Custom ports export PORT=3000 export DEV_PORT=5173 # API URL injection export API_URL=https://api.example.com ``` Access in configuration: ```go theme={null} type Config struct { Env string Port string DevPort string APIURL string } func LoadConfig() *Config { return &Config{ Env: getEnv("MIZU_ENV", "development"), Port: getEnv("PORT", "3000"), DevPort: getEnv("DEV_PORT", "5173"), APIURL: getEnv("API_URL", "http://localhost:3000/api"), } } func getEnv(key, fallback string) string { if value := os.Getenv(key); value != "" { return value } return fallback } ``` ## Next Steps Learn about dev mode and HMR Optimize for production deployment Deep dive into caching Security best practices # Development Source: https://docs.go-mizu.dev/frontend/development How development mode works with hot reload and proxying. Development mode is designed to give you the best developer experience with instant feedback and minimal configuration. When you run your Mizu app in development, it acts as a reverse proxy to your frontend dev server. ## How Development Mode Works In development, Mizu proxies requests to your frontend dev server (usually Vite, webpack-dev-server, or similar): ``` Browser Request ↓ Mizu Server (http://localhost:3000) ↓ Is it /api/* ? ↙ ↘ Yes No ↓ ↓ Go API Proxy to Dev Server Handler (http://localhost:5173) ``` **The flow:** 1. Browser requests `http://localhost:3000/` 2. Mizu checks if the path starts with `/api` (or other ignore paths) 3. If yes → Handle with Go API routes 4. If no → Proxy to the frontend dev server 5. Frontend dev server responds with the latest code ## Enabling Development Mode ### Automatic Detection The simplest way is to use auto-detection: ```go theme={null} app.Use(frontend.WithOptions(frontend.Options{ Mode: frontend.ModeAuto, // Auto-detect mode Root: "./dist", // Production build folder DevServer: "http://localhost:5173", // Dev server URL })) ``` Set the environment variable to enable development: ```bash theme={null} # Development (default) MIZU_ENV=development go run cmd/server/main.go # Or don't set it at all - defaults to dev go run cmd/server/main.go ``` ### Explicit Development Mode You can explicitly set development mode: ```go theme={null} app.Use(frontend.Dev("http://localhost:5173")) ``` Or with options: ```go theme={null} app.Use(frontend.WithOptions(frontend.Options{ Mode: frontend.ModeDev, DevServer: "http://localhost:5173", })) ``` ## Hot Module Replacement (HMR) HMR is what makes modern frontend development so fast. When you save a file, the changes appear instantly in your browser without a full page reload. ### How HMR Works 1. You edit a React component 2. Vite detects the file change 3. Vite sends an update via WebSocket 4. The browser receives the update 5. Only the changed component re-renders 6. Your app state is preserved **Mizu's role:** * Proxies the WebSocket connection between browser and Vite * Preserves all HMR functionality * No configuration needed! ### What Gets Proxied Mizu proxies both HTTP and WebSocket connections: **HTTP Requests:** * `GET /` → Proxied to dev server * `GET /assets/main.js` → Proxied to dev server * `GET /src/App.tsx` → Proxied to dev server (during HMR) **WebSocket Connections:** * `ws://localhost:3000/` → Proxied to `ws://localhost:5173/` * Used for HMR updates * Vite's HMR protocol passes through unchanged ## Dev Server Configuration ### Default Ports Different dev servers use different default ports: | Framework | Tool | Default Port | | --------- | ----------- | ----------------- | | React | Vite | 5173 | | Vue | Vite | 5173 | | Svelte | Vite | 5173 | | Angular | Angular CLI | 4200 | | Next.js | Next.js | 3000 (conflicts!) | **Important:** If your frontend dev server runs on port 3000, change either the Mizu port or the frontend port to avoid conflicts. ### Configuring Dev Server URL Match your frontend dev server port: ```go theme={null} app.Use(frontend.WithOptions(frontend.Options{ Mode: frontend.ModeAuto, DevServer: "http://localhost:4200", // Angular dev server })) ``` ### Custom Dev Server Timeout If your dev server is slow to respond: ```go theme={null} app.Use(frontend.WithOptions(frontend.Options{ Mode: frontend.ModeDev, DevServer: "http://localhost:5173", DevServerTimeout: 60 * time.Second, // Default: 30s })) ``` ### Disabling WebSocket Proxying If you don't need HMR (unusual): ```go theme={null} app.Use(frontend.WithOptions(frontend.Options{ Mode: frontend.ModeDev, DevServer: "http://localhost:5173", ProxyWebSocket: false, // Disable WebSocket proxy })) ``` ## Ignore Paths By default, these paths bypass the proxy and go to your Go handlers: * `/api` - API routes * `/health` - Health check * `/metrics` - Metrics endpoint ```go theme={null} app.Get("/api/users", handleUsers) // Handled by Go app.Get("/health", handleHealth) // Handled by Go app.Get("/", ...) // Proxied to dev server app.Get("/about", ...) // Proxied to dev server ``` ### Custom Ignore Paths Add your own paths: ```go theme={null} app.Use(frontend.WithOptions(frontend.Options{ Mode: frontend.ModeDev, DevServer: "http://localhost:5173", IgnorePaths: []string{"/api", "/auth", "/webhooks"}, })) ``` Now `/auth` and `/webhooks` are handled by Go, not proxied. ### Disable Ignore Paths If you want to proxy everything except explicit routes: ```go theme={null} app.Use(frontend.WithOptions(frontend.Options{ Mode: frontend.ModeDev, DevServer: "http://localhost:5173", IgnorePaths: []string{}, // No automatic ignores })) // Define API routes BEFORE the frontend middleware app.Get("/api/users", handleUsers) // Frontend middleware comes after app.Use(frontend.Dev("http://localhost:5173")) ``` Remember: middleware order matters! Routes defined before the frontend middleware take precedence. ## CORS in Development Mizu automatically adds CORS headers in development mode: ``` Access-Control-Allow-Origin: * ``` This allows your frontend dev server to make requests to the Mizu API without CORS issues. **In production**, these permissive CORS headers are NOT added. Use the [CORS middleware](/middlewares/cors) for production CORS configuration. ## Error Handling When the dev server is not running or unreachable, Mizu shows a helpful error page: ``` ┌─────────────────────────────────────────┐ │ 🚨 Unable to connect to dev server │ │ │ │ http://localhost:5173 │ │ │ │ Make sure your frontend dev server │ │ is running: │ │ │ │ For Vite: npm run dev │ │ For Next.js: npm run dev │ │ │ │ [Auto-retrying in 2s...] │ └─────────────────────────────────────────┘ ``` The page automatically retries every 2 seconds, so once you start your dev server, the page loads. ## Complete Development Setup Here's a complete example: ### `app/server/app.go` ```go theme={null} package server import ( "github.com/go-mizu/mizu" "github.com/go-mizu/mizu/frontend" ) func New(cfg *Config) *mizu.App { app := mizu.New() // Add API routes BEFORE frontend middleware app.Get("/api/users", handleUsers) app.Post("/api/users", createUser) // Frontend middleware (auto-detects mode) app.Use(frontend.WithOptions(frontend.Options{ Mode: frontend.ModeAuto, Root: "./dist", DevServer: "http://localhost:" + cfg.DevPort, IgnorePaths: []string{"/api"}, })) return app } ``` ### `app/server/config.go` ```go theme={null} package server import "os" type Config struct { Port string DevPort string Env string } func LoadConfig() *Config { return &Config{ Port: getEnv("PORT", "3000"), DevPort: getEnv("DEV_PORT", "5173"), Env: getEnv("MIZU_ENV", "development"), } } func getEnv(key, fallback string) string { if value := os.Getenv(key); value != "" { return value } return fallback } ``` ### Running Development ```bash theme={null} # Terminal 1: Start frontend dev server cd frontend npm run dev # Terminal 2: Start Mizu server go run cmd/server/main.go ``` Or use the Makefile: ```bash theme={null} make dev # Runs both servers in parallel ``` ## Development Workflow A typical development session: 1. **Start both servers** ```bash theme={null} make dev ``` 2. **Open browser** ``` http://localhost:3000 ``` 3. **Make frontend changes** * Edit React/Vue/Svelte components * Changes appear instantly (HMR) * No refresh needed! 4. **Make backend changes** * Edit Go files * Stop server (Ctrl+C) * Restart server * Or use `air` for auto-reload 5. **Test API integration** * Call APIs from frontend * Check browser Network tab * Check server logs ## Debugging Tips ### Check Dev Server is Running ```bash theme={null} # Should respond with HTML curl http://localhost:5173 ``` ### Check Mizu is Proxying ```bash theme={null} # Should show the same HTML as above curl http://localhost:3000 ``` ### Enable Debug Logging Add the logger middleware to see all requests: ```go theme={null} import "github.com/go-mizu/mizu/middlewares/logger" app.Use(logger.New()) app.Use(frontend.Dev("http://localhost:5173")) ``` You'll see logs like: ``` GET / → 200 (12.3ms) [proxied] GET /api/users → 200 (2.1ms) [handler] GET /assets/main.js → 200 (5.2ms) [proxied] ``` ### HMR Not Working 1. **Check WebSocket is connected** * Open browser DevTools * Go to Network tab → WS (WebSockets) * Should see a connection to `ws://localhost:3000` 2. **Check Vite config allows external connections** ```ts theme={null} // vite.config.ts export default { server: { hmr: { clientPort: 3000, // Mizu's port } } } ``` 3. **Check CORS headers** * Network tab should show `Access-Control-Allow-Origin: *` ## Next Steps Learn how production mode optimizes performance Explore all configuration options Fix common development issues Best practices for calling backend APIs # Embedded Filesystems Source: https://docs.go-mizu.dev/frontend/embed Deploy single-binary applications with embedded frontend files. Embedding your frontend into the Go binary creates a single executable with no external dependencies. ## How It Works The `//go:embed` directive bundles files into the compiled binary: ```go theme={null} package server import ( "embed" "io/fs" "github.com/go-mizu/mizu" "github.com/go-mizu/mizu/frontend" ) //go:embed all:../../dist var distFS embed.FS func New() *mizu.App { app := mizu.New() // Extract 'dist' subdirectory dist, _ := fs.Sub(distFS, "dist") app.Use(frontend.WithFS(dist)) return app } ``` ## Benefits ### 1. Single Binary Deployment One file contains everything: ```bash theme={null} # Before (multiple files) myapp/ ├── server ├── dist/ │ ├── index.html │ └── assets/ │ └── ... # After (single file) server # Contains everything! ``` ### 2. No External Dependencies ```bash theme={null} # Just copy the binary scp ./bin/server user@server:/usr/local/bin/ # Run it ./server ``` ### 3. Simpler Deployment ```dockerfile theme={null} # Dockerfile FROM scratch COPY server /server CMD ["/server"] ``` Single layer, minimal size. ### 4. Immutable Deployments Frontend and backend versions always match. ## Directory Structure ### Before Embed ``` my-app/ ├── cmd/ │ └── server/ │ └── main.go ├── app/ │ └── server/ │ └── app.go ← Embed directive here ├── dist/ ← Built frontend │ ├── index.html │ └── assets/ └── go.mod ``` ### Embed Directive Location ```go theme={null} // app/server/app.go package server //go:embed all:../../dist ← Relative to this file var distFS embed.FS ``` The path is relative to the `.go` file containing the directive. ## Why Use fs.Sub? The embed directive includes the directory name: ```go theme={null} //go:embed all:../../dist var distFS embed.FS // distFS contains: // dist/ // index.html // assets/ ``` Use `fs.Sub` to remove the prefix: ```go theme={null} dist, _ := fs.Sub(distFS, "dist") // dist contains: // index.html // assets/ ``` Now `index.html` is at the root, as expected. ## Complete Example ```go theme={null} package server import ( "embed" "io/fs" "github.com/go-mizu/mizu" "github.com/go-mizu/mizu/frontend" ) //go:embed all:../../dist var distFS embed.FS func New() *mizu.App { app := mizu.New() // API routes app.Get("/api/users", handleUsers) // Embedded frontend dist, err := fs.Sub(distFS, "dist") if err != nil { panic("failed to load embedded frontend: " + err.Error()) } app.Use(frontend.WithFS(dist)) return app } ``` ## Build Process ```bash theme={null} # 1. Build frontend cd frontend npm run build # Creates dist/ # 2. Build Go binary (embeds dist/) cd .. go build -o ./bin/server cmd/server/main.go # 3. Deploy single binary scp ./bin/server user@server:/app/server ``` ## Binary Size Embedding increases binary size: ```bash theme={null} # Without embed server: 8 MB # With embed (typical React app) server: 12 MB # +4 MB for frontend # With large frontend server: 20 MB # +12 MB ``` **Tips to reduce size:** * Minify frontend * Compress assets * Remove unused code * Use code splitting ## Trade-offs ### Pros * ✅ Single file deployment * ✅ No file serving issues * ✅ Faster startup (no disk I/O) * ✅ Simpler Docker images * ✅ Version consistency ### Cons * ❌ Larger binary size * ❌ Must rebuild for frontend changes * ❌ Can't update frontend independently * ❌ Slower build times ## When to Use **Use embedded FS when:** * Deploying to production * Want simple deployment * Frontend changes infrequently * Binary size acceptable **Use file system when:** * Frequent frontend updates * Want to update without rebuild * Very large frontends * Need separate frontend deployment ## Hybrid Approach Use environment variable to switch: ```go theme={null} func getFrontendFS() fs.FS { // Development: use file system if os.Getenv("MIZU_ENV") != "production" { return os.DirFS("./dist") } // Production: use embedded FS dist, _ := fs.Sub(distFS, "dist") return dist } app.Use(frontend.WithFS(getFrontendFS())) ``` ## Multiple Embedded Directories Embed views and static separately: ```go theme={null} //go:embed all:../../dist var distFS embed.FS //go:embed all:../../views var viewsFS embed.FS //go:embed all:../../static var staticFS embed.FS func New() *mizu.App { app := mizu.New() // Views views, _ := fs.Sub(viewsFS, "views") v := view.New(view.Config{FS: views}) app.Use(v.Middleware()) // Static assets static, _ := fs.Sub(staticFS, "static") app.Static("/static", static) // Frontend SPA dist, _ := fs.Sub(distFS, "dist") app.Use(frontend.WithFS(dist)) return app } ``` ## Next Steps Build process and optimization Alternative deployment options Production best practices # Environment Injection Source: https://docs.go-mizu.dev/frontend/env-injection Inject server-side environment variables into your frontend. Environment injection allows you to pass server-side environment variables to your frontend at runtime, perfect for configuration that changes between environments. ## Basic Usage ### Backend Configuration ```go theme={null} app.Use(frontend.WithOptions(frontend.Options{ Mode: frontend.ModeProduction, Root: "./dist", InjectEnv: []string{"API_URL", "ANALYTICS_ID", "FEATURE_FLAGS"}, })) ``` ### Frontend Access Variables are available as `window.__ENV__`: ```tsx theme={null} // TypeScript declare global { interface Window { __ENV__?: Record } } const apiUrl = window.__ENV__?.API_URL || 'http://localhost:3000/api' const analyticsId = window.__ENV__?.ANALYTICS_ID ``` ```jsx theme={null} // JavaScript const apiUrl = window.__ENV__?.API_URL || '/api' ``` ## How It Works The middleware injects a script tag into your HTML: ```html theme={null} My App ``` The script is injected before ``, making variables available before your app loads. ## Use Cases ### API URLs ```go theme={null} InjectEnv: []string{"API_URL"} ``` ```tsx theme={null} const API_URL = window.__ENV__?.API_URL || '/api' export const fetchUsers = () => fetch(`${API_URL}/users`).then(r => r.json()) ``` ### Analytics ```go theme={null} InjectEnv: []string{"ANALYTICS_ID", "SENTRY_DSN"} ``` ```tsx theme={null} import { init } from '@sentry/react' if (window.__ENV__?.SENTRY_DSN) { init({ dsn: window.__ENV__.SENTRY_DSN }) } ``` ### Feature Flags ```go theme={null} InjectEnv: []string{"FEATURES"} ``` ```tsx theme={null} const features = JSON.parse(window.__ENV__?.FEATURES || '{}') function App() { return (
    {features.newUI && } {!features.newUI && }
    ) } ``` ### Multi-Environment Setup ```go theme={null} func getEnvVars(env string) []string { vars := []string{"API_URL"} if env == "production" { vars = append(vars, "ANALYTICS_ID", "SENTRY_DSN") } if env == "development" { vars = append(vars, "DEBUG", "MOCK_API") } return vars } app.Use(frontend.WithOptions(frontend.Options{ InjectEnv: getEnvVars(os.Getenv("ENV")), })) ``` ## Security Considerations ### Never Inject Secrets ```go theme={null} // ❌ DON'T inject sensitive data InjectEnv: []string{ "DATABASE_PASSWORD", // Visible to users! "SECRET_KEY", // Visible to users! "PRIVATE_API_KEY", // Visible to users! } ``` **Why:** All injected variables are visible in the HTML source and browser console. ### Only Inject Public Configuration ```go theme={null} // ✅ DO inject public config InjectEnv: []string{ "API_URL", // Public endpoint "ANALYTICS_ID", // Public tracking ID "APP_VERSION", // Public version "ENVIRONMENT", // Public environment name } ``` ### Prefixing Convention Follow the convention of prefixing public variables: ```bash theme={null} # .env file VITE_API_URL=https://api.example.com # Public (VITE_ prefix) VITE_ANALYTICS_ID=GA-123 # Public DATABASE_URL=postgresql://... # Private (no prefix) SECRET_KEY=abc123 # Private (no prefix) ``` ```go theme={null} // Only inject VITE_ prefixed vars InjectEnv: getPublicEnvVars(), ``` ```go theme={null} func getPublicEnvVars() []string { var publicVars []string for _, env := range os.Environ() { if strings.HasPrefix(env, "VITE_") { key := strings.Split(env, "=")[0] publicVars = append(publicVars, key) } } return publicVars } ``` ## Build-Time vs Runtime ### Build-Time (Vite/Webpack) ```js theme={null} // Build-time replacement const apiUrl = import.meta.env.VITE_API_URL ``` **Pros:** * Type-safe (TypeScript) * Tree-shaking works * Optimizations apply **Cons:** * Must rebuild for each environment * Can't change without rebuild ### Runtime (Mizu Injection) ```js theme={null} // Runtime injection const apiUrl = window.__ENV__?.API_URL ``` **Pros:** * Single build for all environments * Change without rebuild * Dynamic configuration **Cons:** * Not type-safe by default * Available after page load * Requires fallbacks ### Hybrid Approach Best of both worlds: ```ts theme={null} // Use build-time for development, runtime for production const apiUrl = window.__ENV__?.API_URL || // Runtime (production) import.meta.env.VITE_API_URL || // Build-time (development) 'http://localhost:3000/api' // Fallback ``` ## TypeScript Support Create type definitions: ```ts theme={null} // src/env.d.ts interface ImportMetaEnv { readonly VITE_API_URL: string readonly VITE_ANALYTICS_ID: string } interface ImportMeta { readonly env: ImportMetaEnv } // Runtime env declare global { interface Window { __ENV__?: { API_URL?: string ANALYTICS_ID?: string SENTRY_DSN?: string } } } export {} ``` Use safely: ```ts theme={null} const config = { apiUrl: window.__ENV__?.API_URL || '/api', analyticsId: window.__ENV__?.ANALYTICS_ID, } // Type-safe const url: string = config.apiUrl ``` ## Testing ### Mock in Tests ```tsx theme={null} // test-utils.tsx beforeEach(() => { window.__ENV__ = { API_URL: 'http://localhost:3001/api', ANALYTICS_ID: 'test-analytics', } }) afterEach(() => { delete window.__ENV__ }) ``` ### Environment-Specific Tests ```tsx theme={null} describe('Production config', () => { beforeEach(() => { window.__ENV__ = { API_URL: 'https://api.production.com', } }) it('uses production API', () => { expect(getApiUrl()).toBe('https://api.production.com') }) }) ``` ## Alternative: Meta Tags You can also inject config via meta tags: ```go theme={null} app.Use(frontend.WithOptions(frontend.Options{ InjectMeta: map[string]string{ "api-url": os.Getenv("API_URL"), "analytics-id": os.Getenv("ANALYTICS_ID"), }, })) ``` Read in frontend: ```ts theme={null} const apiUrl = document.querySelector('meta[name="api-url"]')?.getAttribute('content') ``` **When to use:** * SEO-relevant config * Prefer meta tags over script tags * Framework requires meta tags ## Next Steps All configuration options Security best practices Build and deployment guide # HTMX Source: https://docs.go-mizu.dev/frontend/htmx Build hypermedia-driven applications with HTMX and server-rendered HTML. HTMX is a different approach to building web applications. Instead of SPAs with heavy JavaScript, HTMX lets you build dynamic applications with HTML and minimal JavaScript by extending HTML with custom attributes. ## Comparison with Other Approaches | Feature | HTMX | React | Vue 3 | Alpine.js | | --------------------------- | ---------------- | ----------- | ---------------- | ----------------------- | | **Bundle Size** | \~14 KB | \~44 KB | \~34 KB | \~15 KB | | **JavaScript Required** | Minimal | Heavy | Moderate | Light | | **Server Round-trips** | Yes | No (SPA) | No (SPA) | No | | **SEO** | Excellent | Needs SSR | Needs SSR | Good | | **Learning Curve** | Gentle | Steep | Moderate | Gentle | | **Build Step** | Optional | Required | Required | None | | **Offline Support** | Limited | Excellent | Excellent | Limited | | **Server Dependency** | High | Low | Low | Medium | | **Best For** | CRUD apps, forms | Complex UIs | Interactive apps | Progressive enhancement | | **Backend Integration** | Seamless | API-based | API-based | API-based | | **Progressive Enhancement** | Excellent | Poor | Poor | Good | ## Quick Start Create a new HTMX project with the CLI: ```bash theme={null} mizu new ./my-htmx-app --template frontend/htmx cd my-htmx-app make dev ``` Visit `http://localhost:3000` to see your app! ## Why HTMX? ### The Hypermedia Approach HTMX embraces the original web model: server-rendered HTML with progressive enhancement. Instead of sending JSON over the wire and building UI in JavaScript, HTMX sends ready-to-render HTML fragments. **Traditional SPA approach:** ``` Client Request → JSON Response → JavaScript Renders UI ``` **HTMX approach:** ``` Client Request → HTML Response → Browser Renders UI ``` ### Key Benefits * **Tiny**: \~14kB minified and gzipped * **No Build Step**: Include via CDN or npm * **Server-First**: All logic on the server * **Progressive**: Works without JavaScript * **Accessible**: Built on web standards * **Simple**: Extends HTML with attributes * **SEO-Friendly**: Real HTML from server * **Fast Development**: No frontend/server coordination ### When HTMX Shines HTMX is perfect when you want: * Server-side rendering with dynamic interactions * Simple mental model (HTML in, HTML out) * No build toolchain * Progressive enhancement * Great SEO out of the box * Rapid development with familiar tools ## Installation ### Via CDN ```html theme={null} ``` Or use a specific version: ```html theme={null} ``` ### Via npm ```bash theme={null} npm install htmx.org ``` Then import in your JavaScript: ```js theme={null} import 'htmx.org' ``` Or with a bundler: ```js theme={null} import htmx from 'htmx.org' window.htmx = htmx ``` ## Architecture ### Development Mode ``` ┌─────────────────────────────────────────────────────────────┐ │ Browser (Port 3000) │ │ │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ HTML + HTMX │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ User clicks → HTMX sends GET /users │ │ │ │ Server responds with HTML fragment │ │ │ │ HTMX swaps content into #list │ │ │ └────────────────────────────────────────────────────────┘ │ │ ▲ │ │ │ HTTP Request │ │ │ (AJAX with HX-Request header) │ │ ▼ │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ Static Assets │ │ │ │ CSS, Images, HTMX library │ │ │ └────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Mizu Backend (Go Server) │ │ │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ Router │ │ │ │ │ │ │ │ GET / → Render full page │ │ │ │ GET /users → Render HTML fragment │ │ │ │ POST /users → Process + return HTML │ │ │ │ PUT /users/{id} → Update + return HTML │ │ │ │ DELETE /users/{id} → Remove + return empty │ │ │ └────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ View Engine │ │ │ │ │ │ │ │ • Templates (Go html/template) │ │ │ │ • Layouts (default.html) │ │ │ │ • Pages (home.html, users.html) │ │ │ │ • Partials (user-list.html, user-row.html) │ │ │ │ • Hot reload in dev mode │ │ │ └────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ Business Logic │ │ │ │ │ │ │ │ • Handlers (handleUsers, handleCreateUser) │ │ │ │ • Services (UserService, AuthService) │ │ │ │ • Validation │ │ │ │ • Database access │ │ │ └────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ Database │ │ │ │ PostgreSQL, SQLite, etc. │ │ │ └────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ``` ### Production Mode ``` ┌─────────────────────────────────────────────────────────────┐ │ CDN / Edge │ │ │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ Static Assets (Cached) │ │ │ │ • CSS, Images │ │ │ │ • HTMX library │ │ │ │ • Versioned and fingerprinted │ │ │ └────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Load Balancer │ └─────────────────────────────────────────────────────────────┘ │ ┌───────────────┼───────────────┐ ▼ ▼ ▼ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ Mizu Server 1│ │ Mizu Server 2│ │ Mizu Server 3│ │ │ │ │ │ │ │ Single Binary│ │ Single Binary│ │ Single Binary│ │ • Embedded │ │ • Embedded │ │ • Embedded │ │ Templates │ │ Templates │ │ Templates │ │ • Embedded │ │ • Embedded │ │ • Embedded │ │ Assets │ │ Assets │ │ Assets │ │ • Handlers │ │ • Handlers │ │ • Handlers │ │ • Middleware │ │ • Middleware │ │ • Middleware │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ │ └───────────────┼───────────────┘ ▼ ┌──────────────────┐ │ Database │ │ (with pooling) │ └──────────────────┘ ``` ## Project Structure ``` my-htmx-app/ ├── cmd/ │ └── server/ │ └── main.go # Entry point ├── app/ │ └── server/ │ ├── app.go # Mizu app setup │ ├── config.go # Configuration │ ├── routes.go # Routes │ └── handlers.go # Request handlers ├── views/ # HTML templates │ ├── embed.go # Embed directive │ ├── layouts/ │ │ └── default.html # Base layout │ ├── pages/ │ │ ├── home.html # Home page │ │ └── about.html # About page │ └── partials/ # Reusable components │ ├── user-list.html # User list fragment │ └── user-row.html # Single user row ├── static/ # CSS, JS, images │ ├── embed.go │ ├── css/ │ │ └── app.css │ └── js/ │ └── app.js ├── go.mod └── Makefile ``` ## Core Concepts ### HTMX Attributes HTMX extends HTML with attributes that enable AJAX, WebSockets, and Server-Sent Events directly in your markup. #### HTTP Methods ```html theme={null}
    ``` #### Targeting Control where the response gets inserted: ```html theme={null}
    Alice
    Refresh
    ``` #### Swap Strategies Control how content is swapped: ```html theme={null}
    Content
    Content
    Content
    Content
      ...
      ...
    Delete Me
    ``` #### Swap Modifiers Fine-tune swap behavior: ```html theme={null}
    Content
    Messages
    Content
    Content
    Content
    Content
    Content
    ``` #### Triggers Control what triggers the request: ```html theme={null}
    Results
    Loading...
    Load more...
    Checking...
    Load when visible
    ``` #### Trigger Filters Add conditions to triggers: ```html theme={null}
    Ctrl+Click to load
    Shift+Click to load
    Left-click only
    ``` ### Headers #### Request Headers HTMX automatically sends these headers: ```go theme={null} func handler(c *mizu.Ctx) error { // Check if this is an HTMX request isHTMX := c.Request().Header.Get("HX-Request") == "true" // Get the ID of the target element target := c.Request().Header.Get("HX-Target") // Get the ID of the triggered element trigger := c.Request().Header.Get("HX-Trigger") // Get the name attribute of the triggered element triggerName := c.Request().Header.Get("HX-Trigger-Name") // Get the current URL currentURL := c.Request().Header.Get("HX-Current-URL") // Check if this is a history restore request isHistory := c.Request().Header.Get("HX-History-Restore-Request") == "true" // Get the user response to hx-prompt prompt := c.Request().Header.Get("HX-Prompt") if isHTMX { // Return HTML fragment for HTMX requests return c.Render("partials/users", data) } // Return full page for direct navigation return c.Render("pages/users", data) } ``` #### Response Headers Control HTMX behavior from the server: ```go theme={null} func handler(c *mizu.Ctx) error { // Trigger a client-side redirect c.Writer().Header().Set("HX-Redirect", "/login") // Refresh the page c.Writer().Header().Set("HX-Refresh", "true") // Replace the URL in browser history c.Writer().Header().Set("HX-Replace-Url", "/users/page/2") // Push new URL to history c.Writer().Header().Set("HX-Push-Url", "/users/123") // Re-target the response c.Writer().Header().Set("HX-Retarget", "#different-element") // Change swap strategy c.Writer().Header().Set("HX-Reswap", "outerHTML") // Trigger client-side events c.Writer().Header().Set("HX-Trigger", "userCreated") // Trigger after swap c.Writer().Header().Set("HX-Trigger-After-Swap", "updateStats") // Trigger after settle c.Writer().Header().Set("HX-Trigger-After-Settle", "focusInput") // Trigger with JSON payload c.Writer().Header().Set("HX-Trigger", `{"showMessage": {"level": "info", "text": "User created"}}`) return c.Render("partials/user", user) } ``` ### Indicators Show loading states: ```html theme={null}
    Loading...
    ``` ### Validation Client-side validation still works: ```html theme={null}
    ``` Server-side validation: ```go theme={null} func handleCreateUser(c *mizu.Ctx) error { email := c.FormValue("email") // Validate if email == "" { c.Writer().Header().Set("HX-Retarget", "#error") c.Writer().Header().Set("HX-Reswap", "innerHTML") return c.HTML(400, `
    Email is required
    `) } if !isValidEmail(email) { c.Writer().Header().Set("HX-Retarget", "#error") return c.HTML(400, `
    Invalid email format
    `) } // Create user... return c.Render("partials/user-row", user) } ``` ```html theme={null}
    ``` ## Advanced Features ### Out-of-Band Swaps Update multiple parts of the page from a single response: ```html theme={null}
    ``` Server response: ```html theme={null}
    Alice
    Bob
    Loaded 2 users
    ``` Multiple out-of-band swaps: ```html theme={null}
    ...
    Users: 2
    Users loaded
    ``` ### History and Navigation Control browser history: ```html theme={null} View User ``` Handle history restoration: ```go theme={null} func handleUsers(c *mizu.Ctx) error { // Check if this is a history restore if c.Request().Header.Get("HX-History-Restore-Request") == "true" { // Return full page state return c.Render("pages/users", data) } // Normal HTMX request return c.Render("partials/user-list", data) } ``` ### Synchronization Prevent concurrent requests: ```html theme={null} ``` ### Confirmation Prompt before request: ```html theme={null} ``` ```go theme={null} func handleDelete(c *mizu.Ctx) error { // Get prompt response password := c.Request().Header.Get("HX-Prompt") if password != "admin" { return c.HTML(403, `
    Invalid password
    `) } // Delete user... return c.HTML(200, "") } ``` ### Boosting Progressively enhance regular links and forms: ```html theme={null}
    ``` With boost: * Links become AJAX requests that target `body` * Forms submit via AJAX * URLs are pushed to history * Works without JavaScript (progressive enhancement) ### Preserving Content Preserve elements during swaps: ```html theme={null}
    ``` ### Disable During Request Disable elements while request is in flight: ```html theme={null}
    ``` ### Request Parameters Include additional parameters: ```html theme={null}
    ``` ### Encoding Control request encoding: ```html theme={null}
    ``` ## Common Patterns ### Click to Load ```html theme={null}
    ``` ### Click to Edit View mode: ```html theme={null}
    Alice
    ``` Server returns edit form: ```html theme={null}
    ``` After save, server returns view mode again. ### Inline Delete ```html theme={null} Alice alice@example.com ``` Server returns empty response, HTMX removes the row. ### Active Search ```html theme={null} Searching...
    ``` Backend: ```go theme={null} func handleSearch(c *mizu.Ctx) error { query := c.Query("q") results := searchUsers(query) return c.Render("partials/search-results", map[string]any{ "Results": results, "Query": query, }) } ``` ### Infinite Scroll ```html theme={null}
    {{ range .Posts }}
    {{ .Title }}
    {{ end }} {{ if .HasMore }}
    Loading more...
    {{ end }}
    ``` Backend: ```go theme={null} func handlePosts(c *mizu.Ctx) error { page := c.QueryInt("page", 1) limit := 10 posts, total := getPosts(page, limit) hasMore := (page * limit) < total return c.Render("partials/posts", map[string]any{ "Posts": posts, "HasMore": hasMore, "NextPage": page + 1, }) } ``` ### Lazy Loading Load content when it becomes visible: ```html theme={null}
    Loading...
    ``` ### Polling Auto-refresh content: ```html theme={null}
    Loading stats...
    Loading stats...
    Stats here
    ``` ### Progress Bar ```html theme={null}
    ``` Backend: ```go theme={null} func handleJobStatus(c *mizu.Ctx) error { job := getJob() if job.Progress >= 100 { // Job complete, return final content return c.Render("partials/job-complete", job) } // Still in progress, return progress bar return c.Render("partials/job-progress", job) } ``` Template: ```html theme={null}
    {{ .Progress }}% complete
    ``` ### Bulk Operations ```html theme={null}
    {{ range .Users }} {{ end }}
    {{ .Name }} {{ .Email }}
    ``` Backend: ```go theme={null} func handleBulkDelete(c *mizu.Ctx) error { ids := c.Request().Form["ids"] for _, id := range ids { deleteUser(id) } // Return updated page users := getUsers() return c.Render("pages/users", map[string]any{ "Users": users, }) } ``` ### Dependent Selects ```html theme={null}
    ``` Backend: ```go theme={null} func handleStates(c *mizu.Ctx) error { country := c.Query("country") states := getStatesForCountry(country) return c.Render("partials/state-select", map[string]any{ "States": states, }) } ``` ### Typeahead / Autocomplete ```html theme={null}
    ``` Backend: ```go theme={null} func handleAutocomplete(c *mizu.Ctx) error { query := c.Query("q") if query == "" { return c.HTML(200, "") } suggestions := searchUsers(query, 5) return c.Render("partials/suggestions", map[string]any{ "Suggestions": suggestions, }) } ``` Template: ```html theme={null} {{ if .Suggestions }} {{ end }} ``` ### File Upload with Progress ```html theme={null}
    ``` ### Modal Dialogs ```html theme={null} ``` Backend returns: ```html theme={null} ``` ### Optimistic UI Show immediate feedback before server response: ```html theme={null} ``` If server request fails, HTMX will restore the original content. ### Pagination ```html theme={null}
    {{ range .Users }} {{ end }}
    {{ .Name }}
    ``` ### Tabs ```html theme={null}
    Loading...
    ``` ## HTMX Extensions ### Server-Sent Events (SSE) Real-time updates from server: ```html theme={null}
    ``` Backend (Go): ```go theme={null} func handleEvents(c *mizu.Ctx) error { c.Writer().Header().Set("Content-Type", "text/event-stream") c.Writer().Header().Set("Cache-Control", "no-cache") c.Writer().Header().Set("Connection", "keep-alive") flusher, ok := c.Writer().(http.Flusher) if !ok { return fmt.Errorf("streaming not supported") } for { select { case msg := <-messageChan: fmt.Fprintf(c.Writer(), "event: message\n") fmt.Fprintf(c.Writer(), "data:
    %s
    \n\n", msg) flusher.Flush() case <-c.Request().Context().Done(): return nil } } } ``` ### WebSockets ```html theme={null}
    ``` ### Preload Preload content on hover: ```html theme={null} View User ``` ### Loading States ```html theme={null} ``` ### Response Targets Target based on HTTP status: ```html theme={null}
    ``` ### Class Tools Manipulate classes: ```html theme={null}
    Content
    ``` ## Complete Application Example Let's build a complete Task Manager with Mizu backend and HTMX frontend. ### Backend Structure `app/server/app.go`: ```go theme={null} package server import ( "io/fs" "github.com/go-mizu/mizu" "github.com/go-mizu/mizu/view" "taskmanager/static" "taskmanager/views" ) func New(cfg *Config) *mizu.App { app := mizu.New() // Initialize view engine viewsFS, _ := fs.Sub(views.FS, ".") v := view.New(view.Config{ FS: viewsFS, Extension: ".html", DefaultLayout: "default", Development: cfg.Env == "development", }) if err := v.Load(); err != nil { panic("failed to load views: " + err.Error()) } app.Use(v.Middleware()) // Serve static assets staticFS, _ := fs.Sub(static.FS, ".") app.Static("/static", staticFS) // Initialize services taskService := NewTaskService() // Setup routes setupRoutes(app, taskService) return app } ``` `app/server/task_service.go`: ```go theme={null} package server import ( "sync" "time" ) type Task struct { ID int `json:"id"` Title string `json:"title"` Description string `json:"description"` Status string `json:"status"` // pending, in_progress, completed Priority string `json:"priority"` // low, medium, high CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } type TaskService struct { tasks map[int]*Task nextID int mu sync.RWMutex } func NewTaskService() *TaskService { return &TaskService{ tasks: make(map[int]*Task), nextID: 1, } } func (s *TaskService) List(filter string) []*Task { s.mu.RLock() defer s.mu.RUnlock() var result []*Task for _, task := range s.tasks { if filter == "" || task.Status == filter { result = append(result, task) } } return result } func (s *TaskService) Get(id int) (*Task, bool) { s.mu.RLock() defer s.mu.RUnlock() task, ok := s.tasks[id] return task, ok } func (s *TaskService) Create(title, description, status, priority string) *Task { s.mu.Lock() defer s.mu.Unlock() task := &Task{ ID: s.nextID, Title: title, Description: description, Status: status, Priority: priority, CreatedAt: time.Now(), UpdatedAt: time.Now(), } s.tasks[s.nextID] = task s.nextID++ return task } func (s *TaskService) Update(id int, title, description, status, priority string) (*Task, bool) { s.mu.Lock() defer s.mu.Unlock() task, ok := s.tasks[id] if !ok { return nil, false } task.Title = title task.Description = description task.Status = status task.Priority = priority task.UpdatedAt = time.Now() return task, true } func (s *TaskService) Delete(id int) bool { s.mu.Lock() defer s.mu.Unlock() if _, ok := s.tasks[id]; !ok { return false } delete(s.tasks, id) return true } func (s *TaskService) Stats() map[string]int { s.mu.RLock() defer s.mu.RUnlock() stats := map[string]int{ "total": len(s.tasks), "pending": 0, "in_progress": 0, "completed": 0, } for _, task := range s.tasks { stats[task.Status]++ } return stats } ``` `app/server/handlers.go`: ```go theme={null} package server import ( "github.com/go-mizu/mizu" ) type Handlers struct { taskService *TaskService } func NewHandlers(taskService *TaskService) *Handlers { return &Handlers{ taskService: taskService, } } func (h *Handlers) Home(c *mizu.Ctx) error { return c.Render("pages/home", map[string]any{ "Title": "Task Manager", }) } func (h *Handlers) ListTasks(c *mizu.Ctx) error { filter := c.Query("status") tasks := h.taskService.List(filter) // Check if HTMX request if c.Request().Header.Get("HX-Request") == "true" { return c.Render("partials/task-list", map[string]any{ "Tasks": tasks, "Filter": filter, }) } // Full page return c.Render("pages/tasks", map[string]any{ "Title": "Tasks", "Tasks": tasks, "Filter": filter, }) } func (h *Handlers) CreateTask(c *mizu.Ctx) error { title := c.FormValue("title") description := c.FormValue("description") status := c.FormValue("status") priority := c.FormValue("priority") // Validation if title == "" { c.Writer().Header().Set("HX-Retarget", "#error") c.Writer().Header().Set("HX-Reswap", "innerHTML") return c.HTML(400, `
    Title is required
    `) } task := h.taskService.Create(title, description, status, priority) // Trigger stats update c.Writer().Header().Set("HX-Trigger", "taskCreated") return c.Render("partials/task-row", map[string]any{ "Task": task, }) } func (h *Handlers) EditTask(c *mizu.Ctx) error { id := c.ParamInt("id") task, ok := h.taskService.Get(id) if !ok { return c.HTML(404, "Task not found") } return c.Render("partials/task-edit", map[string]any{ "Task": task, }) } func (h *Handlers) UpdateTask(c *mizu.Ctx) error { id := c.ParamInt("id") title := c.FormValue("title") description := c.FormValue("description") status := c.FormValue("status") priority := c.FormValue("priority") task, ok := h.taskService.Update(id, title, description, status, priority) if !ok { return c.HTML(404, "Task not found") } // Trigger stats update c.Writer().Header().Set("HX-Trigger", "taskUpdated") return c.Render("partials/task-row", map[string]any{ "Task": task, }) } func (h *Handlers) DeleteTask(c *mizu.Ctx) error { id := c.ParamInt("id") if !h.taskService.Delete(id) { return c.HTML(404, "Task not found") } // Trigger stats update c.Writer().Header().Set("HX-Trigger", "taskDeleted") // Return empty response (element will be removed) return c.HTML(200, "") } func (h *Handlers) Stats(c *mizu.Ctx) error { stats := h.taskService.Stats() return c.Render("partials/stats", map[string]any{ "Stats": stats, }) } ``` `app/server/routes.go`: ```go theme={null} package server import ( "github.com/go-mizu/mizu" ) func setupRoutes(app *mizu.App, taskService *TaskService) { h := NewHandlers(taskService) // Pages app.Get("/", h.Home) app.Get("/tasks", h.ListTasks) // Task operations app.Post("/tasks", h.CreateTask) app.Get("/tasks/{id}/edit", h.EditTask) app.Put("/tasks/{id}", h.UpdateTask) app.Delete("/tasks/{id}", h.DeleteTask) // Stats app.Get("/stats", h.Stats) } ``` ### Frontend Templates `views/layouts/default.html`: ```html theme={null} {{ .Title }} - Task Manager
    {{ embed }}
    ``` `views/pages/home.html`: ```html theme={null}

    Create New Task

    Loading tasks...
    ``` `views/partials/task-list.html`: ```html theme={null}
    {{ range .Tasks }} {{ template "partials/task-row" (dict "Task" .) }} {{ else }} {{ end }}
    Title Status Priority Actions
    No tasks found
    ``` `views/partials/task-row.html`: ```html theme={null}
    {{ .Task.Title }}
    {{ if .Task.Description }}
    {{ .Task.Description }}
    {{ end }} {{ if eq .Task.Status "pending" }} Pending {{ else if eq .Task.Status "in_progress" }} In Progress {{ else }} Completed {{ end }} {{ if eq .Task.Priority "low" }} Low {{ else if eq .Task.Priority "medium" }} Medium {{ else }} High {{ end }}
    ``` `views/partials/task-edit.html`: ```html theme={null}
    ``` Note: The template needs a helper function for single task rendering. Add this to your handlers: ```go theme={null} func (h *Handlers) GetTask(c *mizu.Ctx) error { id := c.ParamInt("id") task, ok := h.taskService.Get(id) if !ok { return c.HTML(404, "Task not found") } return c.Render("partials/task-row", map[string]any{ "Task": task, }) } ``` And add the route: ```go theme={null} app.Get("/tasks/{id}", h.GetTask) ``` `views/partials/stats.html`: ```html theme={null}
    Total: {{ .Stats.total }}
    {{ .Stats.pending }}
    {{ .Stats.in_progress }}
    {{ .Stats.completed }}
    ``` ## Error Handling ### Client-Side Errors ```html theme={null}
    ``` HTML5 validation runs before HTMX submits the form. ### Server-Side Errors Return appropriate HTTP status codes: ```go theme={null} func handleCreate(c *mizu.Ctx) error { email := c.FormValue("email") // Validation error (400) if !isValidEmail(email) { return c.HTML(400, `
    Invalid email
    `) } // Not found (404) if !userExists(email) { return c.HTML(404, `
    User not found
    `) } // Server error (500) if err := createUser(email); err != nil { return c.HTML(500, `
    Server error
    `) } // Success (200) return c.Render("partials/user", user) } ``` ### Global Error Handling ```html theme={null} ``` ## Security ### CSRF Protection Use CSRF middleware: ```go theme={null} import "github.com/go-mizu/mizu/middlewares/csrf" app.Use(csrf.New()) ``` Include token in forms: ```html theme={null}
    ``` ### XSS Protection Always escape user content in templates. Go's `html/template` does this automatically: ```html theme={null}
    {{ .UserInput }}
    {{ .TrustedHTML | safe }}
    ``` ### SQL Injection Use parameterized queries: ```go theme={null} // Good: parameterized db.Query("SELECT * FROM users WHERE email = ?", email) // Bad: string concatenation db.Query("SELECT * FROM users WHERE email = '" + email + "'") ``` ### Rate Limiting ```go theme={null} import "github.com/go-mizu/mizu/middlewares/ratelimit" app.Use(ratelimit.New(ratelimit.Config{ Max: 100, Duration: time.Minute, })) ``` ## Performance ### Caching Cache responses: ```go theme={null} import "github.com/go-mizu/mizu/middlewares/cache" app.Use(cache.New(cache.Config{ TTL: 5 * time.Minute, })) ``` Or use HTTP headers: ```go theme={null} func handler(c *mizu.Ctx) error { c.Writer().Header().Set("Cache-Control", "max-age=300") return c.Render("partials/data", data) } ``` ### Compression ```go theme={null} import "github.com/go-mizu/mizu/middlewares/compress" app.Use(compress.New()) ``` ### Lazy Loading Load content only when needed: ```html theme={null}
    Loading...
    ``` ### Debouncing Reduce server requests: ```html theme={null} ``` ## Testing ### Backend Tests ```go theme={null} func TestHandlers(t *testing.T) { app := setupTestApp() // Test GET req := httptest.NewRequest("GET", "/tasks", nil) req.Header.Set("HX-Request", "true") rec := httptest.NewRecorder() app.ServeHTTP(rec, req) assert.Equal(t, 200, rec.Code) assert.Contains(t, rec.Body.String(), "task-list") // Test POST form := url.Values{} form.Add("title", "Test Task") form.Add("status", "pending") req = httptest.NewRequest("POST", "/tasks", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("HX-Request", "true") rec = httptest.NewRecorder() app.ServeHTTP(rec, req) assert.Equal(t, 200, rec.Code) assert.Contains(t, rec.Body.String(), "Test Task") } ``` ### Frontend Tests Use tools like Playwright or Cypress: ```js theme={null} test('create task', async ({ page }) => { await page.goto('http://localhost:3000') await page.fill('[name="title"]', 'Test Task') await page.selectOption('[name="status"]', 'pending') await page.click('button[type="submit"]') // Wait for HTMX to complete await page.waitForSelector('text=Test Task') expect(await page.textContent('tbody')).toContain('Test Task') }) ``` ## Troubleshooting ### HTMX Not Working Check: 1. HTMX library is loaded: `` 2. Check browser console for errors 3. Verify server is returning HTML (not JSON) 4. Check `HX-Request` header is sent ### Content Not Updating Check: 1. `hx-target` points to existing element 2. `hx-swap` strategy is correct 3. Server response contains expected HTML 4. No JavaScript errors preventing swap ### Form Not Submitting Check: 1. Form has `hx-post` or similar attribute 2. Input fields have `name` attributes 3. Server endpoint exists and accepts POST 4. No validation errors preventing submit ### History Not Working Check: 1. Using `hx-push-url` or `hx-replace-url` 2. Handling history restore requests 3. URLs are valid ### Debug Mode Enable HTMX logging: ```html theme={null} ``` ## Combining with Alpine.js Alpine.js adds client-side reactivity to complement HTMX: ```html theme={null}
    Loading...
    ``` Example: Dropdown with HTMX actions: ```html theme={null}
    ``` ## When to Use HTMX ### Perfect For * **CRUD Applications**: Forms, lists, updates * **Admin Dashboards**: Data tables, filters, actions * **Content Sites**: Blogs, documentation, marketing * **Progressive Enhancement**: Start with plain HTML, add interactivity * **Server-Side Teams**: Developers comfortable with backend rendering * **SEO-Critical Apps**: Need server-rendered HTML * **Rapid Prototyping**: Quick feedback loop ### Not Ideal For * **Desktop-Like Apps**: Complex client-side state * **Real-Time Collaboration**: Multiple users editing simultaneously * **Offline-First**: Apps that work without network * **Heavy Client Logic**: Complex calculations or data processing * **Mobile Apps**: Need native feel with offline support ### Hybrid Approach Use HTMX for most of the app, add JavaScript where needed: ```html theme={null}
    Load Users
    ``` ## Next Steps Add client-side reactivity with Alpine.js Learn more about Mizu's template engine Compare with SPA approach Official HTMX documentation More patterns and examples Book on hypermedia-driven applications # Build Manifest Source: https://docs.go-mizu.dev/frontend/manifest Use build manifests for asset fingerprinting and module preloading. Build manifests map source files to their output files with content hashes. They enable proper asset loading, module preloading, and template integration. ## What is a Build Manifest? When Vite or Webpack builds your app, it creates a manifest file: ```json theme={null} { "src/main.tsx": { "file": "assets/main.abc123.js", "css": ["assets/main.def456.css"], "imports": ["assets/vendor.xyz789.js"] }, "src/App.tsx": { "file": "assets/App.ghi012.js" } } ``` This maps `src/main.tsx` → `assets/main.abc123.js`. ## Configuration ```go theme={null} import "github.com/go-mizu/mizu/frontend" opts := frontend.Options{ Root: "./dist", Manifest: ".vite/manifest.json", // Path to manifest } ``` ## Loading a Manifest ```go theme={null} import ( "embed" "io/fs" "github.com/go-mizu/mizu/frontend" ) //go:embed all:dist var distFS embed.FS func setup() { dist, _ := fs.Sub(distFS, "dist") // Load manifest manifest, err := frontend.LoadManifest(dist, ".vite/manifest.json") if err != nil { panic(err) } // Use in templates viewHelpers := manifest.ViewHelpers() // Register helpers with your view engine } ``` ## Manifest API ### Entry Points Get the output file for an entry: ```go theme={null} outputFile := manifest.Entry("src/main.tsx") // Returns: "assets/main.abc123.js" ``` ### Assets Get the URL for an asset: ```go theme={null} assetURL := manifest.Asset("logo.png") // Returns: "/assets/logo.abc123.png" ``` ### CSS Files Get CSS files for an entry: ```go theme={null} cssFiles := manifest.CSS("src/main.tsx") // Returns: ["assets/main.def456.css"] ``` ### Preloads Get module preloads: ```go theme={null} preloads := manifest.Preloads("src/main.tsx") // Returns: ["assets/vendor.xyz789.js", "assets/chunk.abc123.js"] ``` ## Template Integration ### View Helpers ```go theme={null} helpers := manifest.ViewHelpers() // Available functions: // - vite_entry: Complete tags (CSS + preloads + script) // - vite_asset: Asset URL // - vite_script: Script tag only // - vite_css: CSS tags only // - vite_preload: Preload tags only ``` ### In Go Templates ```html theme={null} {{ vite_entry "src/main.tsx" }}
    ``` Outputs: ```html theme={null} ``` ## Manual Tag Generation ### Script Tag ```go theme={null} scriptTag := manifest.ScriptTag("src/main.tsx") // Returns: ``` ### CSS Tags ```go theme={null} cssTags := manifest.CSSTags("src/main.tsx") // Returns: ``` ### Preload Tags ```go theme={null} preloadTags := manifest.PreloadTags("src/main.tsx") // Returns: ``` ### Entry Tags (All in One) ```go theme={null} allTags := manifest.EntryTags("src/main.tsx") // Returns: CSS + preloads + script in correct order ``` ## Vite Manifest Format Vite creates `.vite/manifest.json`: ```json theme={null} { "src/main.tsx": { "file": "assets/main.abc123.js", "src": "src/main.tsx", "isEntry": true, "css": ["assets/main.def456.css"], "imports": ["_vendor.xyz789.js"] }, "_vendor.xyz789.js": { "file": "assets/vendor.xyz789.js" } } ``` ## Webpack Manifest Format Webpack creates a simpler manifest: ```json theme={null} { "main.js": "assets/main.abc123.js", "main.css": "assets/main.def456.css", "logo.png": "assets/logo.abc123.png" } ``` Both formats work with Mizu's manifest loader. ## Benefits ### 1. Correct Asset Loading Always load the correct hashed file: ```html theme={null} {{ vite_entry "src/main.tsx" }} ``` ### 2. Module Preloading Improve performance by preloading dependencies: ```html theme={null} ``` Browser loads dependencies in parallel. ### 3. CSS Extraction Automatically include CSS: ```html theme={null} ``` ## Next Steps Caching strategies Production optimization # Minimal Setup Source: https://docs.go-mizu.dev/frontend/minimal-setup Manually set up frontend integration without using templates. This guide shows you how to manually integrate a frontend framework with Mizu without using the CLI templates. ## Prerequisites * Go 1.22+ * Node.js 18+ * Existing Mizu project or create new one ## React + Vite Setup ### 1. Create Vite Project ```bash theme={null} npm create vite@latest frontend -- --template react-ts cd frontend npm install ``` ### 2. Configure Vite ```ts theme={null} // frontend/vite.config.ts import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], server: { port: 5173, hmr: { clientPort: 3000, // Your Mizu server port }, }, build: { outDir: '../dist', emptyOutDir: true, }, }) ``` ### 3. Add Frontend Integration ```go theme={null} // main.go or app.go package main import ( "embed" "io/fs" "github.com/go-mizu/mizu" "github.com/go-mizu/mizu/frontend" ) //go:embed all:dist var distFS embed.FS func main() { app := mizu.New() // API routes app.Get("/api/hello", func(c *mizu.Ctx) error { return c.JSON(200, map[string]string{"message": "Hello!"}) }) // Frontend middleware dist, _ := fs.Sub(distFS, "dist") app.Use(frontend.WithOptions(frontend.Options{ Mode: frontend.ModeAuto, FS: dist, Root: "./dist", DevServer: "http://localhost:5173", IgnorePaths: []string{"/api"}, })) app.Listen(":3000") } ``` ### 4. Update package.json ```json theme={null} { "scripts": { "dev": "vite", "build": "tsc && vite build", "preview": "vite preview" } } ``` ### 5. Create Makefile (Optional) ```makefile theme={null} .PHONY: dev build clean dev: @echo "Starting development servers..." @(cd frontend && npm run dev) & \ go run main.go build: @echo "Building frontend..." cd frontend && npm run build @echo "Building backend..." go build -o ./bin/server clean: rm -rf dist frontend/node_modules ./bin ``` ## Vue + Vite Setup Follow same steps as React, but: ```bash theme={null} # Step 1: Create Vue project npm create vite@latest frontend -- --template vue-ts ``` Same Vite config, same backend setup. ## HTMX Setup ### 1. Create Directory Structure ```bash theme={null} mkdir -p views/{layouts,pages,partials} mkdir -p static/{css,js} ``` ### 2. Create Go Embed Files ```go theme={null} // views/embed.go package views import "embed" //go:embed layouts/* pages/* partials/* var FS embed.FS ``` ```go theme={null} // static/embed.go package static import "embed" //go:embed css/* js/* var FS embed.FS ``` ### 3. Setup App ```go theme={null} package main import ( "io/fs" "github.com/go-mizu/mizu" "github.com/go-mizu/mizu/view" "yourmodule/static" "yourmodule/views" ) func main() { app := mizu.New() // View engine viewsFS, _ := fs.Sub(views.FS, ".") v := view.New(view.Config{ FS: viewsFS, Extension: ".html", DefaultLayout: "default", }) app.Use(v.Middleware()) // Static files staticFS, _ := fs.Sub(static.FS, ".") app.Static("/static", staticFS) // Routes app.Get("/", func(c *mizu.Ctx) error { return c.Render("pages/home", nil) }) app.Listen(":3000") } ``` ### 4. Create Templates ```html theme={null} My App {{ embed }} ``` ```html theme={null}

    Welcome

    Loading...
    ``` ## Next.js Setup ### 1. Create Next.js App ```bash theme={null} npx create-next-app@latest frontend cd frontend ``` ### 2. Configure for Static Export ```js theme={null} // next.config.mjs /** @type {import('next').NextConfig} */ const nextConfig = { output: 'export', distDir: '../build', images: { unoptimized: true, }, } export default nextConfig ``` ### 3. Backend Setup ```go theme={null} //go:embed all:build var buildFS embed.FS func main() { app := mizu.New() app.Get("/api/hello", handleHello) build, _ := fs.Sub(buildFS, "build") app.Use(frontend.WithFS(build)) app.Listen(":3000") } ``` ## Common Additions ### Add Router ```bash theme={null} # React npm install react-router-dom # Vue npm install vue-router ``` ### Add State Management ```bash theme={null} # React npm install zustand # Vue npm install pinia ``` ### Add UI Library ```bash theme={null} # Any framework npm install -D tailwindcss postcss autoprefixer npx tailwindcss init -p ``` ## Troubleshooting ### HMR Not Working Add to Vite config: ```ts theme={null} server: { hmr: { clientPort: 3000, // Your Mizu port host: 'localhost', }, } ``` ### Assets Not Loading Ensure base URL is correct: ```ts theme={null} // vite.config.ts export default { base: '/', // Or '/app/' if using prefix } ``` ### Build Not Found Check embed path matches build output: ```go theme={null} //go:embed all:dist // Must match vite outDir var distFS embed.FS ``` ## Next Steps Use ready-made templates instead All configuration options Development workflow # Next Source: https://docs.go-mizu.dev/frontend/next Build Next.js applications with static export and Mizu backend. Next.js is a powerful React framework that offers server-side rendering, static site generation, file-based routing, and an exceptional developer experience. When using Next.js with Mizu, you'll leverage Next.js's **static export** mode to generate a fully static site, while Mizu handles your backend API and serves the static files. ## Why Next.js with Mizu? Next.js brings several advantages to your Mizu applications: **File-based routing** - Define routes by creating files in the `app/` directory. No need to configure a router manually. **React Server Components** - Write components that run during build time, reducing client-side JavaScript. **App Router** - Modern routing with layouts, loading states, error boundaries, and parallel routes. **Developer Experience** - Fast Refresh, TypeScript support, and excellent error messages out of the box. **Optimizations** - Automatic code splitting, image optimization (with configuration), and font optimization. ### Next.js with Mizu vs Standalone Next.js | Feature | Next.js + Mizu | Standalone Next.js | | ----------------- | ------------------------- | ------------------------ | | **Hosting** | Single Go binary | Node.js server or Vercel | | **Backend** | Go handlers | Next.js API routes | | **Deployment** | Any server with Go | Node.js required | | **SSR** | ❌ No (static export) | ✅ Yes | | **Static Export** | ✅ Yes | ✅ Yes | | **API Routes** | Go backend | JavaScript | | **Performance** | ⚡ Very fast (Go) | ⚡ Fast (Node.js) | | **Type Safety** | Backend: Go, Frontend: TS | Full-stack TypeScript | **When to use Next.js with Mizu:** * You want Next.js features (App Router, RSC, file-based routing) * You prefer Go for backend APIs * You want a single binary deployment * You're building a static-exportable site **When to use standalone Next.js:** * You need full SSR (server-side rendering at request time) * You want Next.js API routes * You prefer Node.js for everything * Your team is all JavaScript/TypeScript ## How Static Export Works Next.js's static export generates HTML files at build time: ``` Build Process ↓ ┌─────────────────────────────┐ │ Next.js analyzes routes │ └─────────────┬───────────────┘ ↓ ┌─────────────────────────────┐ │ Renders pages to HTML │ │ - Server Components run │ │ - Client Components bundle │ └─────────────┬───────────────┘ ↓ ┌─────────────────────────────┐ │ Outputs static files │ │ - build/ │ │ ├── index.html │ │ ├── about.html │ │ └── _next/ │ └─────────────────────────────┘ ``` **What happens at build time:** 1. Next.js crawls all routes starting from page files 2. Server Components execute and generate HTML 3. Client Components are bundled into JavaScript 4. Static HTML files are created for each route 5. Assets are optimized and fingerprinted **What happens at runtime:** 1. Mizu serves the pre-built HTML files 2. Browser loads HTML + JavaScript 3. React hydrates the page 4. Client-side navigation takes over 5. API calls go to Mizu backend (Go) ## Quick Start Create a new Next.js project with the CLI: ```bash theme={null} mizu new ./my-nextjs-app --template frontend/next cd my-nextjs-app make dev ``` Visit `http://localhost:3000` to see your app! ## Project Structure ``` my-nextjs-app/ ├── cmd/ │ └── server/ │ └── main.go # Go entry point ├── app/ │ └── server/ │ ├── app.go # Mizu app configuration │ ├── config.go # Server configuration │ └── routes.go # API routes (Go) ├── frontend/ # Next.js application │ ├── app/ # App Router (Next.js 13+) │ │ ├── layout.tsx # Root layout │ │ ├── page.tsx # Home page (/) │ │ ├── about/ │ │ │ └── page.tsx # About page (/about) │ │ └── users/ │ │ ├── page.tsx # Users list (/users) │ │ └── [id]/ │ │ └── page.tsx # User detail (/users/123) │ ├── components/ │ │ ├── Header.tsx │ │ └── Footer.tsx │ ├── public/ # Static assets │ │ └── images/ │ ├── next.config.mjs # Next.js configuration │ ├── package.json │ └── tsconfig.json ├── build/ # Built files (after npm run build) ├── go.mod └── Makefile ``` ## Configuration ### Next.js Configuration The key to using Next.js with Mizu is configuring static export: #### `frontend/next.config.mjs` ```js theme={null} /** @type {import('next').NextConfig} */ const nextConfig = { // Enable static export - this is crucial! output: 'export', // Output to 'build' directory (must match Mizu config) distDir: '../build', // Disable image optimization for static export images: { unoptimized: true }, // Optional: set base path if serving from subdirectory // basePath: '/app', // Optional: trailing slash behavior // trailingSlash: true, } export default nextConfig ``` **Configuration explained:** * **output: 'export'** - Tells Next.js to generate static HTML files instead of running a Node.js server * **distDir: '../build'** - Outputs files to `build/` (one level up from `frontend/`) * **images.unoptimized** - Disables Next.js Image Optimization API (requires Node.js server) ### Backend Configuration #### `app/server/app.go` ```go theme={null} package server import ( "embed" "io/fs" "github.com/go-mizu/mizu" "github.com/go-mizu/mizu/frontend" ) // Embed the Next.js build output //go:embed all:../../build var buildFS embed.FS func New(cfg *Config) *mizu.App { app := mizu.New() // API routes come first app.Get("/api/users", handleUsers) app.Post("/api/users", createUser) app.Get("/api/users/{id}", getUser) // Extract 'build' subdirectory from embedded FS build, _ := fs.Sub(buildFS, "build") // Frontend middleware (handles all non-API routes) app.Use(frontend.WithOptions(frontend.Options{ Mode: frontend.ModeAuto, // Auto-detect dev/prod FS: build, // Embedded build files Root: "./build", // Fallback to filesystem DevServer: "http://localhost:" + cfg.DevPort, // Next.js dev server IgnorePaths: []string{"/api"}, // Don't proxy /api to Next.js })) return app } ``` #### `app/server/routes.go` ```go theme={null} package server import "github.com/go-mizu/mizu" func setupRoutes(app *mizu.App) { // User API app.Get("/api/users", handleUsers) app.Post("/api/users", createUser) app.Get("/api/users/{id}", getUser) app.Put("/api/users/{id}", updateUser) app.Delete("/api/users/{id}", deleteUser) } func handleUsers(c *mizu.Ctx) error { users := []map[string]any{ {"id": 1, "name": "Alice", "email": "alice@example.com"}, {"id": 2, "name": "Bob", "email": "bob@example.com"}, {"id": 3, "name": "Charlie", "email": "charlie@example.com"}, } return c.JSON(200, users) } func createUser(c *mizu.Ctx) error { var user map[string]any if err := c.BodyJSON(&user); err != nil { return c.JSON(400, map[string]string{"error": "Invalid JSON"}) } // Add ID (in real app, use database) user["id"] = 4 return c.JSON(201, user) } func getUser(c *mizu.Ctx) error { id := c.Param("id") user := map[string]any{ "id": id, "name": "User " + id, "email": "user" + id + "@example.com", } return c.JSON(200, user) } ``` ## File-Based Routing Next.js uses file-based routing in the `app/` directory. Each folder represents a route segment, and special files define the UI. ### Route Files | File | Purpose | Required | | --------------- | --------------------------------------------------- | ---------- | | `layout.tsx` | Shared UI for a segment and its children | Yes (root) | | `page.tsx` | Unique UI for a route, makes it publicly accessible | Yes | | `loading.tsx` | Loading UI (Suspense boundary) | No | | `error.tsx` | Error UI (Error boundary) | No | | `not-found.tsx` | 404 UI | No | ### Example Routes ``` app/ ├── layout.tsx → Root layout (wraps everything) ├── page.tsx → Home page: / ├── about/ │ └── page.tsx → About: /about ├── blog/ │ ├── layout.tsx → Blog layout (wraps all blog pages) │ ├── page.tsx → Blog home: /blog │ └── [slug]/ │ └── page.tsx → Blog post: /blog/hello-world └── users/ ├── page.tsx → Users list: /users └── [id]/ └── page.tsx → User detail: /users/123 ``` ### Root Layout The root layout wraps your entire application: #### `frontend/app/layout.tsx` ```tsx theme={null} import type { Metadata } from 'next' import './globals.css' export const metadata: Metadata = { title: 'My Mizu App', description: 'Built with Next.js and Mizu', } export default function RootLayout({ children, }: { children: React.ReactNode }) { return (
    {children}

    © 2025 My Mizu App

    ) } ``` ### Dynamic Routes Use brackets for dynamic segments: #### `frontend/app/users/[id]/page.tsx` ```tsx theme={null} 'use client' import { useEffect, useState } from 'react' interface User { id: string name: string email: string } export default function UserPage({ params }: { params: { id: string } }) { const [user, setUser] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) useEffect(() => { fetch(`/api/users/${params.id}`) .then(res => { if (!res.ok) throw new Error('User not found') return res.json() }) .then(setUser) .catch(err => setError(err.message)) .finally(() => setLoading(false)) }, [params.id]) if (loading) return
    Loading user...
    if (error) return
    Error: {error}
    if (!user) return
    User not found
    return (

    {user.name}

    Email: {user.email}

    ID: {user.id}

    ) } ``` ## Server Components vs Client Components Next.js 13+ introduces React Server Components (RSC). Understanding the difference is crucial. ### Server Components (Default) Components are Server Components by default. They run **at build time** (in static export mode): ```tsx theme={null} // app/page.tsx - Server Component (no 'use client') export default function Home() { // This runs at BUILD time const buildTime = new Date().toISOString() return (

    Welcome

    Built at: {buildTime}

    ) } ``` **Server Component benefits:** * Zero JavaScript sent to browser for the component logic * Can read files, query databases (at build time) * Better performance - less client-side JavaScript **Server Component limitations:** * Cannot use hooks (useState, useEffect, etc.) * Cannot use browser APIs * Cannot handle user interactions directly ### Client Components Add `'use client'` directive for interactive components: ```tsx theme={null} 'use client' import { useState } from 'react' export default function Counter() { // This runs in the BROWSER const [count, setCount] = useState(0) return (

    Count: {count}

    ) } ``` **Client Component benefits:** * Can use hooks and state * Can handle user interactions * Can use browser APIs * Can use useEffect for side effects **Client Component limitations:** * JavaScript bundle sent to browser * Cannot use server-only features (fs, database) ### Composition Pattern Compose Server and Client Components: ```tsx theme={null} // app/page.tsx - Server Component import Counter from './Counter' // Client Component export default function Home() { const data = { message: "Hello from server" } return (

    Home Page

    {data.message}

    {/* Client component nested in server component */}
    ) } ``` ```tsx theme={null} // app/Counter.tsx - Client Component 'use client' import { useState } from 'react' export default function Counter() { const [count, setCount] = useState(0) return } ``` ## Data Fetching ### Fetching in Client Components Use `useEffect` and `fetch`: ```tsx theme={null} 'use client' import { useEffect, useState } from 'react' interface User { id: number name: string email: string } export default function Users() { const [users, setUsers] = useState([]) const [loading, setLoading] = useState(true) useEffect(() => { fetch('/api/users') .then(res => res.json()) .then(setUsers) .finally(() => setLoading(false)) }, []) if (loading) return
    Loading...
    return (
      {users.map(user => (
    • {user.name}
    • ))}
    ) } ``` ### Using React Query Install React Query for better data fetching: ```bash theme={null} cd frontend npm install @tanstack/react-query ``` Setup provider: ```tsx theme={null} // app/providers.tsx 'use client' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { useState } from 'react' export function Providers({ children }: { children: React.ReactNode }) { const [queryClient] = useState(() => new QueryClient()) return ( {children} ) } ``` ```tsx theme={null} // app/layout.tsx import { Providers } from './providers' export default function RootLayout({ children }) { return ( {children} ) } ``` Use in components: ```tsx theme={null} 'use client' import { useQuery } from '@tanstack/react-query' export default function Users() { const { data: users, isLoading, error } = useQuery({ queryKey: ['users'], queryFn: () => fetch('/api/users').then(res => res.json()) }) if (isLoading) return
    Loading...
    if (error) return
    Error: {error.message}
    return (
      {users.map(user =>
    • {user.name}
    • )}
    ) } ``` ## Metadata and SEO Next.js makes SEO easy with metadata: ### Static Metadata ```tsx theme={null} // app/page.tsx import type { Metadata } from 'next' export const metadata: Metadata = { title: 'Home - My App', description: 'Welcome to my app built with Mizu and Next.js', keywords: ['Next.js', 'Mizu', 'Go', 'React'], openGraph: { title: 'Home - My App', description: 'Welcome to my app', images: ['/og-image.png'], }, } export default function Home() { return

    Home

    } ``` ### Dynamic Metadata ```tsx theme={null} // app/users/[id]/page.tsx import type { Metadata } from 'next' export async function generateMetadata({ params }): Promise { return { title: `User ${params.id} - My App`, description: `Profile page for user ${params.id}`, } } export default function UserPage({ params }) { return

    User {params.id}

    } ``` ## Image Optimization Next.js's Image component requires a Node.js server for optimization. In static export mode, you have options: ### Option 1: Use Regular `` Tags ```tsx theme={null} export default function Logo() { return Logo } ``` ### Option 2: Use `next-image-export-optimizer` This package optimizes images at build time: ```bash theme={null} npm install next-image-export-optimizer ``` ```js theme={null} // next.config.mjs const nextConfig = { output: 'export', images: { loader: 'custom', imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], }, env: { nextImageExportOptimizer_imageFolderPath: "public/images", nextImageExportOptimizer_exportFolderPath: "build", nextImageExportOptimizer_quality: "75", }, } ``` ```tsx theme={null} import ExportedImage from 'next-image-export-optimizer' export default function Logo() { return ( ) } ``` ### Option 3: Optimize Manually Use tools like `sharp` or ImageMagick to pre-optimize images before adding them to `public/`. ## Development Workflow ### Starting Development ```bash theme={null} # Option 1: Use Makefile (recommended) make dev # Option 2: Manual (two terminals) # Terminal 1: Next.js dev server cd frontend npm run dev # Starts on http://localhost:3001 # Terminal 2: Mizu server go run cmd/server/main.go # Starts on http://localhost:3000 ``` **How it works in development:** 1. Next.js runs its dev server on port 3001 2. Mizu runs on port 3000 3. Requests to Mizu's port 3000 get proxied to Next.js (except `/api`) 4. You visit `http://localhost:3000` in your browser 5. Hot reload works through Mizu's proxy ### Making Changes **Frontend changes:** * Edit any file in `frontend/` * Next.js Fast Refresh updates browser instantly * No restart needed **Backend changes:** * Edit Go files * Restart Mizu server * Or use `air` for auto-reload: ```bash theme={null} # Install air go install github.com/cosmtrek/air@latest # Run with auto-reload air ``` ## Building for Production Build the complete application: ```bash theme={null} make build ``` This runs: 1. `cd frontend && npm run build` - Next.js static export 2. `go build -o bin/server cmd/server/main.go` - Go binary with embedded frontend ### Build Output ``` build/ ├── index.html # Home page ├── about.html # About page ├── users.html # Users list ├── users/ │ └── 123.html # Example user page ├── _next/ │ ├── static/ │ │ └── chunks/ # JavaScript bundles │ └── ... └── images/ # Static assets ``` ### Running in Production ```bash theme={null} MIZU_ENV=production ./bin/server ``` The binary contains: * Mizu web server * Your Go API handlers * Entire Next.js build embedded ## Limitations with Static Export When using `output: 'export'`, some Next.js features are unavailable: | Feature | Available | Notes | | ---------------------- | --------- | ---------------------------- | | File-based routing | ✅ Yes | Works perfectly | | Server Components | ✅ Yes | Run at build time | | Client Components | ✅ Yes | Full support | | Dynamic routes | ✅ Yes | Using \[brackets] | | Layouts | ✅ Yes | Full support | | Loading UI | ✅ Yes | Full support | | Error boundaries | ✅ Yes | Full support | | API Routes | ❌ No | Use Mizu Go handlers instead | | Server-Side Rendering | ❌ No | Only static export | | `getServerSideProps` | ❌ No | Use client-side fetching | | `revalidate` | ❌ No | No ISR in static export | | Image Optimization API | ❌ No | Use alternatives | | Middleware | ❌ No | Use Mizu middleware | | Server Actions | ❌ No | Use Mizu API routes | ### Working Around Limitations **Instead of API Routes:** ```tsx theme={null} // ❌ app/api/users/route.ts - Doesn't work in static export export async function GET() { return Response.json([]) } // ✅ Use Mizu Go handlers instead // app/server/routes.go func handleUsers(c *mizu.Ctx) error { return c.JSON(200, users) } ``` **Instead of SSR:** ```tsx theme={null} // ❌ Using getServerSideProps - Doesn't work export async function getServerSideProps() { const data = await fetch('...') return { props: { data } } } // ✅ Client-side fetching instead 'use client' export default function Page() { const [data, setData] = useState(null) useEffect(() => { fetch('/api/data').then(r => r.json()).then(setData) }, []) } ``` ## Troubleshooting ### Build Errors: "output: export" Issues **Error:** ``` Error: Page "/api/users" is incompatible with "output: export" ``` **Cause:** You have API routes in `app/api/` **Solution:** Remove `app/api/` directory. Use Mizu Go handlers instead. *** ### Hydration Mismatch Errors **Error:** ``` Warning: Text content did not match. Server: "..." Client: "..." ``` **Cause:** Server-rendered HTML doesn't match client-rendered HTML **Solution:** Ensure Server Components don't use time-dependent values: ```tsx theme={null} // ❌ Bad - time changes between build and runtime export default function Page() { return

    {new Date().toString()}

    } // ✅ Good - use client component for dynamic content 'use client' export default function Page() { const [time, setTime] = useState(new Date()) useEffect(() => { const timer = setInterval(() => setTime(new Date()), 1000) return () => clearInterval(timer) }, []) return

    {time.toString()}

    } ``` *** ### Images Not Loading **Error:** Images show broken icon **Cause:** Next.js Image component requires server **Solution:** Use one of the image optimization alternatives mentioned above. *** ### Can't Find Module **Error:** ``` Module not found: Can't resolve '@/components/Header' ``` **Cause:** Path alias not configured **Solution:** Check `tsconfig.json`: ```json theme={null} { "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["./app/*"] } } } ``` ## Real-World Example: Blog with Comments Here's a complete example showing Server Components, Client Components, and API integration: ### Backend ```go theme={null} // app/server/routes.go func setupRoutes(app *mizu.App) { app.Get("/api/posts", handlePosts) app.Get("/api/posts/{id}", getPost) app.Get("/api/posts/{id}/comments", getComments) app.Post("/api/posts/{id}/comments", createComment) } func handlePosts(c *mizu.Ctx) error { posts := []map[string]any{ {"id": 1, "title": "First Post", "slug": "first-post"}, {"id": 2, "title": "Second Post", "slug": "second-post"}, } return c.JSON(200, posts) } func getPost(c *mizu.Ctx) error { id := c.Param("id") post := map[string]any{ "id": id, "title": "Post " + id, "content": "This is the content of post " + id, } return c.JSON(200, post) } func getComments(c *mizu.Ctx) error { // Return comments for post comments := []map[string]any{ {"id": 1, "author": "Alice", "text": "Great post!"}, {"id": 2, "author": "Bob", "text": "Thanks for sharing"}, } return c.JSON(200, comments) } ``` ### Frontend ```tsx theme={null} // app/blog/[slug]/page.tsx import Comments from './Comments' export default function BlogPost({ params }: { params: { slug: string } }) { // Server Component - runs at build time return (

    Post: {params.slug}

    This is a blog post about {params.slug}

    {/* Client Component for interactivity */}
    ) } ``` ```tsx theme={null} // app/blog/[slug]/Comments.tsx 'use client' import { useState, useEffect } from 'react' export default function Comments({ slug }: { slug: string }) { const [comments, setComments] = useState([]) const [newComment, setNewComment] = useState('') const [author, setAuthor] = useState('') useEffect(() => { fetch(`/api/posts/${slug}/comments`) .then(r => r.json()) .then(setComments) }, [slug]) const handleSubmit = async (e) => { e.preventDefault() const response = await fetch(`/api/posts/${slug}/comments`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ author, text: newComment }), }) if (response.ok) { const comment = await response.json() setComments([...comments, comment]) setNewComment('') setAuthor('') } } return (

    Comments

      {comments.map((c: any) => (
    • {c.author}: {c.text}
    • ))}
    setAuthor(e.target.value)} placeholder="Your name" required />
    {{end}} ``` ## Step 4: Add CSS Create `static/css/style.css`: ```css theme={null} * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; background: #f5f5f5; } .container { max-width: 800px; margin: 0 auto; padding: 0 20px; } /* Navigation */ nav { background: #2c3e50; padding: 1rem 0; } nav .container { display: flex; justify-content: space-between; align-items: center; } nav .logo { color: white; font-size: 1.5rem; font-weight: bold; text-decoration: none; } nav .nav-links a { color: #ecf0f1; text-decoration: none; margin-left: 20px; } nav .nav-links a:hover { color: #3498db; } /* Main content */ main { padding: 2rem 0; min-height: calc(100vh - 150px); } h1 { margin-bottom: 1.5rem; color: #2c3e50; } /* Post cards */ .posts { display: grid; gap: 1.5rem; } .post-card { background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .post-card h2 { margin-bottom: 0.5rem; } .post-card h2 a { color: #2c3e50; text-decoration: none; } .post-card h2 a:hover { color: #3498db; } .meta { color: #7f8c8d; font-size: 0.9rem; margin-bottom: 0.5rem; } .read-more { color: #3498db; text-decoration: none; } /* Single post */ .post { background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .post .content { margin: 1.5rem 0; white-space: pre-wrap; } .back { color: #3498db; text-decoration: none; } /* Form */ .post-form { background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .form-group { margin-bottom: 1.5rem; } .form-group label { display: block; margin-bottom: 0.5rem; font-weight: 500; } .form-group input, .form-group textarea { width: 100%; padding: 0.75rem; border: 1px solid #ddd; border-radius: 4px; font-size: 1rem; } .form-group textarea { resize: vertical; } button { background: #3498db; color: white; padding: 0.75rem 1.5rem; border: none; border-radius: 4px; font-size: 1rem; cursor: pointer; } button:hover { background: #2980b9; } .error { background: #fee; color: #c00; padding: 1rem; border-radius: 4px; margin-bottom: 1rem; } /* Footer */ footer { background: #2c3e50; color: #ecf0f1; padding: 1rem 0; text-align: center; } ``` ## Step 5: Create the Server Create `main.go`: ```go theme={null} package main import ( "fmt" "html/template" "net/http" "sync" "time" "github.com/go-mizu/mizu" ) // Post model type Post struct { ID string Title string Content string Excerpt string CreatedAt time.Time } // Storage var ( posts = make(map[string]*Post) postsMu sync.RWMutex nextID = 1 ) // Templates var templates *template.Template func init() { templates = template.Must(template.ParseGlob("templates/*.html")) template.Must(templates.ParseGlob("templates/layouts/*.html")) } // Render helper func render(c *mizu.Ctx, name string, data map[string]any) error { c.Header().Set("Content-Type", "text/html; charset=utf-8") // Clone template to add layout t := template.Must(templates.Clone()) template.Must(t.ParseFiles("templates/layouts/main.html")) return t.ExecuteTemplate(c.Writer(), "main.html", data) } // Handlers func homePage(c *mizu.Ctx) error { postsMu.RLock() defer postsMu.RUnlock() list := make([]*Post, 0, len(posts)) for _, p := range posts { list = append(list, p) } return render(c, "home", map[string]any{ "Title": "Home", "Posts": list, }) } func showPost(c *mizu.Ctx) error { id := c.Param("id") postsMu.RLock() post, ok := posts[id] postsMu.RUnlock() if !ok { return c.Text(404, "Post not found") } return render(c, "post", map[string]any{ "Title": post.Title, "Post": post, }) } func newPostForm(c *mizu.Ctx) error { return render(c, "new-post", map[string]any{ "Title": "New Post", }) } func createPost(c *mizu.Ctx) error { title := c.FormValue("title") content := c.FormValue("content") if title == "" || content == "" { return render(c, "new-post", map[string]any{ "Title": "New Post", "Error": "Title and content are required", "Title": title, "Content": content, }) } postsMu.Lock() id := fmt.Sprintf("%d", nextID) nextID++ excerpt := content if len(excerpt) > 150 { excerpt = excerpt[:150] + "..." } post := &Post{ ID: id, Title: title, Content: content, Excerpt: excerpt, CreatedAt: time.Now(), } posts[id] = post postsMu.Unlock() return c.Redirect(302, "/posts/"+id) } func main() { app := mizu.New() // Static files app.Static("/static/", http.Dir("static")) // Routes app.Get("/", homePage) app.Get("/posts/new", newPostForm) app.Post("/posts", createPost) app.Get("/posts/{id}", showPost) fmt.Println("Server running at http://localhost:3000") app.Listen(":3000") } ``` ## Step 6: Run and Test ```bash theme={null} go run main.go ``` Open [http://localhost:3000](http://localhost:3000) in your browser. Try: 1. Click "New Post" to create a post 2. Fill in the form and submit 3. View the post 4. Go back to see it on the homepage ## Step 7: Add Sample Data Add some initial posts in `main()`: ```go theme={null} func main() { // Add sample posts posts["1"] = &Post{ ID: "1", Title: "Getting Started with Mizu", Content: "Mizu is a lightweight web framework for Go...", Excerpt: "Mizu is a lightweight web framework for Go...", CreatedAt: time.Now().Add(-24 * time.Hour), } posts["2"] = &Post{ ID: "2", Title: "Building APIs with Go", Content: "Learn how to build REST APIs using Go and Mizu...", Excerpt: "Learn how to build REST APIs using Go and Mizu...", CreatedAt: time.Now().Add(-48 * time.Hour), } nextID = 3 app := mizu.New() // ... rest of setup } ``` ## What You Learned * Using Go templates with layouts * Serving static files (CSS, images) * Handling form submissions * Redirecting after form submission * Template composition ## Project Structure ``` blog/ ├── main.go ├── go.mod ├── go.sum ├── templates/ │ ├── layouts/ │ │ └── main.html │ ├── home.html │ ├── post.html │ └── new-post.html └── static/ └── css/ └── style.css ``` ## Next Steps Add a React frontend. Advanced template features. # Installation Source: https://docs.go-mizu.dev/guides/installation Get started with Mizu in your Go project This guide covers all the ways to install and set up Mizu for your projects. ## Requirements Before installing Mizu, ensure you have: | Requirement | Version | Notes | | -------------------- | ------- | --------------------------------------- | | **Go** | 1.22+ | Required for enhanced ServeMux patterns | | **Operating System** | Any | Linux, macOS, Windows all supported | ### Checking Your Go Version ```bash theme={null} go version # Should output: go version go1.22.x (or higher) ``` If you need to install or upgrade Go, visit [go.dev/dl](https://go.dev/dl/). ## Installation Methods ### Method 1: As a Library (Recommended) Add Mizu to your existing Go project: ```bash theme={null} go get github.com/go-mizu/mizu ``` Create a simple `main.go`: ```go theme={null} package main import "github.com/go-mizu/mizu" func main() { app := mizu.New() app.Get("/", func(c *mizu.Ctx) error { return c.Text(200, "Hello, Mizu!") }) app.Listen(":3000") } ``` Run your application: ```bash theme={null} go run main.go ``` Visit [http://localhost:3000](http://localhost:3000) to see your app. ### Method 2: Using the CLI The Mizu CLI provides project scaffolding and development tools. #### Install the CLI ```bash theme={null} go install github.com/go-mizu/mizu/cmd/mizu@latest ``` Make sure `$GOPATH/bin` (or `$HOME/go/bin`) is in your PATH: ```bash theme={null} # Add to ~/.bashrc, ~/.zshrc, or equivalent export PATH="$PATH:$(go env GOPATH)/bin" ``` #### Verify Installation ```bash theme={null} mizu version ``` #### Create a New Project ```bash theme={null} # Create a minimal project mizu new myapp --template minimal # Or an API project mizu new myapi --template api # Or a full web application mizu new myweb --template web ``` #### Run in Development Mode ```bash theme={null} cd myapp mizu dev ``` This starts your app with hot reload enabled. ### Method 3: Using Templates Start with a pre-configured project structure: ```bash theme={null} # Minimal - Basic server setup mizu new myapp --template minimal # API - REST API with validation mizu new myapp --template api # Contract - Contract-based services mizu new myapp --template contract # Web - Server-rendered website mizu new myapp --template web # Live - Real-time features mizu new myapp --template live # Sync - State synchronization mizu new myapp --template sync ``` ## Your First Program Here's a complete "Hello World" example: ```go theme={null} package main import ( "log" "github.com/go-mizu/mizu" ) func main() { // Create a new Mizu app app := mizu.New() // Define a route app.Get("/", func(c *mizu.Ctx) error { return c.JSON(200, map[string]string{ "message": "Hello, Mizu!", "status": "running", }) }) // Start the server log.Println("Server starting on http://localhost:3000") if err := app.Listen(":3000"); err != nil { log.Fatal(err) } } ``` ### Running the Program ```bash theme={null} # Initialize go module (if new project) go mod init myapp # Download dependencies go mod tidy # Run the application go run main.go ``` ### Testing the API ```bash theme={null} curl http://localhost:3000 # {"message":"Hello, Mizu!","status":"running"} ``` ## IDE Setup ### VS Code 1. Install the [Go extension](https://marketplace.visualstudio.com/items?itemName=golang.go) 2. Open your project folder 3. VS Code will prompt to install Go tools - accept all Recommended settings (`.vscode/settings.json`): ```json theme={null} { "go.useLanguageServer": true, "go.formatTool": "goimports", "editor.formatOnSave": true, "[go]": { "editor.defaultFormatter": "golang.go" } } ``` ### GoLand 1. Open your project as a Go project 2. GoLand automatically detects go.mod 3. Wait for indexing to complete Recommended settings: * Enable "Format on Save" * Enable "Optimize imports on Save" ### Other Editors For Vim/Neovim, use [gopls](https://github.com/golang/tools/tree/master/gopls) with your preferred LSP client. For Emacs, use [lsp-mode](https://emacs-lsp.github.io/lsp-mode/) with gopls. ## Adding Middlewares Middlewares are installed as separate packages: ```bash theme={null} # Add specific middlewares go get github.com/go-mizu/mizu/middlewares/cors go get github.com/go-mizu/mizu/middlewares/logger go get github.com/go-mizu/mizu/middlewares/recover ``` Use them in your app: ```go theme={null} import ( "github.com/go-mizu/mizu" "github.com/go-mizu/mizu/middlewares/cors" "github.com/go-mizu/mizu/middlewares/logger" "github.com/go-mizu/mizu/middlewares/recover" ) func main() { app := mizu.New() // Add middlewares app.Use(recover.New()) // Panic recovery app.Use(logger.New()) // Request logging app.Use(cors.New()) // CORS support app.Get("/", handler) app.Listen(":3000") } ``` ## Project Structure Recommended structure for larger applications: ``` myapp/ ├── cmd/ │ └── server/ │ └── main.go # Entry point ├── internal/ │ ├── handlers/ # HTTP handlers │ │ ├── users.go │ │ └── posts.go │ ├── middleware/ # Custom middleware │ │ └── auth.go │ ├── models/ # Data models │ │ └── user.go │ └── services/ # Business logic │ └── user_service.go ├── pkg/ # Public packages ├── go.mod ├── go.sum └── README.md ``` ## Verifying Installation Run these checks to ensure everything is set up correctly: ### 1. Check Go Version ```bash theme={null} go version # go version go1.22.x ... ``` ### 2. Check Mizu Import ```bash theme={null} go list -m github.com/go-mizu/mizu # github.com/go-mizu/mizu v1.x.x ``` ### 3. Run a Test Server ```go theme={null} package main import ( "fmt" "github.com/go-mizu/mizu" ) func main() { app := mizu.New() app.Get("/health", func(c *mizu.Ctx) error { return c.JSON(200, map[string]string{ "status": "healthy", "framework": "mizu", }) }) fmt.Println("Test server running on :3000") app.Listen(":3000") } ``` ```bash theme={null} # In another terminal curl http://localhost:3000/health # {"framework":"mizu","status":"healthy"} ``` ## Troubleshooting ### "go: module not found" Ensure you have Go 1.22+ and run: ```bash theme={null} go mod tidy ``` ### "command not found: mizu" Add Go's bin directory to your PATH: ```bash theme={null} export PATH="$PATH:$(go env GOPATH)/bin" ``` ### Port Already in Use Change the port in your app: ```go theme={null} app.Listen(":8080") // Use a different port ``` Or find and kill the process using port 3000: ```bash theme={null} # On macOS/Linux lsof -i :3000 kill -9 # On Windows netstat -ano | findstr :3000 taskkill /PID /F ``` ## Next Steps Build your first complete application. Learn about App, Router, Handler, and Context. Explore the Mizu component ecosystem. # Project Structure Source: https://docs.go-mizu.dev/guides/project-structure Organize your Mizu project for maintainability and scale. This guide presents recommended patterns for structuring Mizu projects, from simple scripts to production applications. ## Simple projects For small projects or prototypes, everything can live in a single file: ``` myapp/ ├── main.go ├── go.mod └── go.sum ``` ```go theme={null} // main.go package main import "github.com/go-mizu/mizu" func main() { app := mizu.New() app.Get("/", func(c *mizu.Ctx) error { return c.JSON(200, map[string]string{"status": "ok"}) }) app.Listen(":3000") } ``` This is fine for scripts, tools, and learning. No ceremony needed. ## Standard layout For larger applications, separate concerns into packages: ``` myapp/ ├── main.go # Entry point ├── go.mod ├── go.sum ├── handlers/ # HTTP handlers │ ├── users.go │ └── posts.go ├── services/ # Business logic │ ├── user_service.go │ └── post_service.go ├── models/ # Data structures │ ├── user.go │ └── post.go ├── middleware/ # Custom middleware │ └── auth.go └── config/ # Configuration └── config.go ``` ### main.go The entry point sets up the app and routes: ```go theme={null} package main import ( "myapp/config" "myapp/handlers" "myapp/middleware" "github.com/go-mizu/mizu" ) func main() { cfg := config.Load() app := mizu.New() // Global middleware app.Use(middleware.RequestID()) // Public routes app.Get("/health", handlers.Health) // API routes api := app.Group("/api") api.Get("/users", handlers.ListUsers) api.Get("/users/{id}", handlers.GetUser) api.Post("/users", handlers.CreateUser) // Protected routes protected := api.With(middleware.RequireAuth()) protected.Put("/users/{id}", handlers.UpdateUser) protected.Delete("/users/{id}", handlers.DeleteUser) app.Listen(":" + cfg.Port) } ``` ### handlers/users.go Handlers focus on HTTP concerns: ```go theme={null} package handlers import ( "myapp/models" "myapp/services" "github.com/go-mizu/mizu" ) func ListUsers(c *mizu.Ctx) error { users, err := services.Users.List(c.Context()) if err != nil { return err } return c.JSON(200, users) } func GetUser(c *mizu.Ctx) error { id := c.Param("id") user, err := services.Users.Get(c.Context(), id) if err != nil { return err } return c.JSON(200, user) } func CreateUser(c *mizu.Ctx) error { var input models.CreateUserInput if err := c.BindJSON(&input, 1<<20); err != nil { return err } user, err := services.Users.Create(c.Context(), input) if err != nil { return err } return c.JSON(201, user) } ``` ### services/user\_service.go Services contain business logic: ```go theme={null} package services import ( "context" "myapp/models" ) var Users = &UserService{} type UserService struct { // Add dependencies here (DB, cache, etc.) } func (s *UserService) List(ctx context.Context) ([]models.User, error) { // Business logic here return []models.User{}, nil } func (s *UserService) Get(ctx context.Context, id string) (*models.User, error) { // Business logic here return &models.User{ID: id}, nil } func (s *UserService) Create(ctx context.Context, input models.CreateUserInput) (*models.User, error) { // Validation, database insert, etc. return &models.User{ID: "new", Name: input.Name}, nil } ``` ### models/user.go Models define data structures: ```go theme={null} package models import "time" type User struct { ID string `json:"id"` Name string `json:"name"` Email string `json:"email"` CreatedAt time.Time `json:"createdAt"` } type CreateUserInput struct { Name string `json:"name"` Email string `json:"email"` } ``` ## Clean architecture For complex applications, use a layered architecture: ``` myapp/ ├── cmd/ │ └── server/ │ └── main.go # Entry point ├── internal/ │ ├── app/ │ │ └── app.go # App setup │ ├── handler/ # HTTP handlers │ │ ├── handler.go │ │ └── user.go │ ├── service/ # Business logic │ │ └── user.go │ ├── repository/ # Data access │ │ └── user.go │ └── domain/ # Core types │ └── user.go ├── pkg/ # Public packages │ └── validate/ │ └── validate.go └── go.mod ``` ### Benefits | Layer | Responsibility | Dependencies | | -------------- | --------------------- | ------------------ | | **cmd** | Start the app | internal, pkg | | **handler** | HTTP request/response | service | | **service** | Business rules | repository, domain | | **repository** | Database operations | domain | | **domain** | Core types | none | ### Dependency injection Pass dependencies explicitly: ```go theme={null} // internal/app/app.go package app import ( "database/sql" "myapp/internal/handler" "myapp/internal/repository" "myapp/internal/service" "github.com/go-mizu/mizu" ) func New(db *sql.DB) *mizu.App { // Build dependency graph userRepo := repository.NewUserRepository(db) userService := service.NewUserService(userRepo) userHandler := handler.NewUserHandler(userService) // Create app app := mizu.New() // Register routes app.Get("/users", userHandler.List) app.Get("/users/{id}", userHandler.Get) app.Post("/users", userHandler.Create) return app } ``` ## Static files and templates Include static files and templates: ``` myapp/ ├── main.go ├── static/ # Static assets │ ├── css/ │ ├── js/ │ └── images/ ├── templates/ # HTML templates │ ├── layout.html │ └── pages/ │ ├── home.html │ └── about.html └── embed.go # Embed directive ``` ### embed.go ```go theme={null} package main import "embed" //go:embed static/* var StaticFS embed.FS //go:embed templates/* var TemplateFS embed.FS ``` ### main.go ```go theme={null} package main import ( "io/fs" "net/http" "github.com/go-mizu/mizu" ) func main() { app := mizu.New() // Serve static files static, _ := fs.Sub(StaticFS, "static") app.Static("/static/", http.FS(static)) // Your routes app.Get("/", homeHandler) app.Listen(":3000") } ``` ## Configuration Use environment variables with sensible defaults: ```go theme={null} // config/config.go package config import "os" type Config struct { Port string DatabaseURL string Debug bool } func Load() *Config { return &Config{ Port: getEnv("PORT", "3000"), DatabaseURL: getEnv("DATABASE_URL", ""), Debug: getEnv("DEBUG", "false") == "true", } } func getEnv(key, fallback string) string { if value := os.Getenv(key); value != "" { return value } return fallback } ``` ## Testing Keep tests next to the code they test: ``` handlers/ ├── users.go ├── users_test.go ├── posts.go └── posts_test.go ``` Or use a separate test directory for integration tests: ``` myapp/ ├── handlers/ ├── services/ └── tests/ ├── integration/ │ └── api_test.go └── e2e/ └── full_flow_test.go ``` ## Best practices 1. **Keep main.go small** - Only setup and wiring 2. **Handlers are thin** - Extract business logic to services 3. **One package per concern** - Don't mix handlers with models 4. **Use interfaces at boundaries** - Makes testing easier 5. **Prefer explicit over implicit** - Pass dependencies, don't use globals 6. **Group related routes** - Use `app.Group()` for organization ## Anti-patterns to avoid * **God packages** - `utils`, `helpers`, `common` * **Circular imports** - Usually means wrong package boundaries * **Business logic in handlers** - Keep handlers focused on HTTP * **Global state** - Makes testing difficult ## Next steps Write tests for your handlers. Deploy to production. # Quick Start Source: https://docs.go-mizu.dev/guides/quick-start Build your first Mizu app in 5 minutes. This guide gets you from zero to running app as fast as possible. You'll create a simple JSON API with one route. ## Prerequisites * Go 1.22 or later installed ([download](https://go.dev/dl/)) * A terminal and text editor Verify Go is installed: ```bash theme={null} go version # go version go1.22.0 darwin/arm64 ``` ## Step 1: Create a project ```bash theme={null} mkdir myapp && cd myapp go mod init myapp ``` ## Step 2: Install Mizu ```bash theme={null} go get github.com/go-mizu/mizu ``` ## Step 3: Create main.go Create a file named `main.go`: ```go theme={null} package main import "github.com/go-mizu/mizu" func main() { app := mizu.New() app.Get("/", func(c *mizu.Ctx) error { return c.JSON(200, map[string]string{ "message": "Hello, Mizu!", }) }) app.Listen(":3000") } ``` ## Step 4: Run it ```bash theme={null} go run main.go ``` You'll see: ``` INFO server starting addr=:3000 ``` ## Step 5: Test it Open a new terminal and run: ```bash theme={null} curl http://localhost:3000 ``` You'll get: ```json theme={null} {"message":"Hello, Mizu!"} ``` Press `Ctrl+C` in the first terminal to stop the server. ## What just happened? 1. **`mizu.New()`** created an app with a router and default middleware 2. **`app.Get("/", ...)`** registered a handler for GET requests to "/" 3. **`c.JSON(200, ...)`** sent a JSON response with status 200 4. **`app.Listen(":3000")`** started the HTTP server ## Add more routes Expand your `main.go`: ```go theme={null} package main import "github.com/go-mizu/mizu" func main() { app := mizu.New() // Homepage app.Get("/", func(c *mizu.Ctx) error { return c.JSON(200, map[string]string{ "message": "Hello, Mizu!", }) }) // Get user by ID app.Get("/users/{id}", func(c *mizu.Ctx) error { id := c.Param("id") return c.JSON(200, map[string]string{ "id": id, "name": "User " + id, }) }) // Create a user app.Post("/users", func(c *mizu.Ctx) error { var input struct { Name string `json:"name"` } if err := c.BindJSON(&input, 1<<20); err != nil { return c.JSON(400, map[string]string{"error": err.Error()}) } return c.JSON(201, map[string]string{ "id": "123", "name": input.Name, }) }) app.Listen(":3000") } ``` Test the new routes: ```bash theme={null} # Get a user curl http://localhost:3000/users/42 # Create a user curl -X POST http://localhost:3000/users \ -H "Content-Type: application/json" \ -d '{"name": "Alice"}' ``` ## Project structure For larger apps, organize code into packages: ``` myapp/ ├── main.go # Entry point ├── go.mod ├── go.sum ├── handlers/ # HTTP handlers │ └── users.go ├── services/ # Business logic │ └── user.go └── models/ # Data types └── user.go ``` ## What's next? Learn how Mizu components work together. Create a complete REST API tutorial. Best practices for organizing your code. Deploy your app to production. # Testing Source: https://docs.go-mizu.dev/guides/testing Write effective tests for your Mizu handlers and middleware. Go's standard `testing` package and `net/http/httptest` work seamlessly with Mizu. This guide shows patterns for testing handlers, middleware, and complete applications. ## Testing handlers ### Basic handler test ```go theme={null} package handlers_test import ( "net/http/httptest" "testing" "github.com/go-mizu/mizu" ) func TestHelloHandler(t *testing.T) { // Create app with handler app := mizu.New() app.Get("/hello", func(c *mizu.Ctx) error { return c.Text(200, "Hello, World!") }) // Create test request req := httptest.NewRequest("GET", "/hello", nil) rec := httptest.NewRecorder() // Execute request app.ServeHTTP(rec, req) // Assert response if rec.Code != 200 { t.Errorf("expected status 200, got %d", rec.Code) } if rec.Body.String() != "Hello, World!" { t.Errorf("unexpected body: %s", rec.Body.String()) } } ``` ### Testing JSON responses ```go theme={null} func TestGetUser(t *testing.T) { app := mizu.New() app.Get("/users/{id}", func(c *mizu.Ctx) error { id := c.Param("id") return c.JSON(200, map[string]string{ "id": id, "name": "Test User", }) }) req := httptest.NewRequest("GET", "/users/123", nil) rec := httptest.NewRecorder() app.ServeHTTP(rec, req) if rec.Code != 200 { t.Fatalf("expected 200, got %d", rec.Code) } // Parse JSON response var response map[string]string if err := json.Unmarshal(rec.Body.Bytes(), &response); err != nil { t.Fatalf("failed to parse JSON: %v", err) } if response["id"] != "123" { t.Errorf("expected id=123, got %s", response["id"]) } } ``` ### Testing POST requests with JSON body ```go theme={null} func TestCreateUser(t *testing.T) { app := mizu.New() app.Post("/users", func(c *mizu.Ctx) error { var input struct { Name string `json:"name"` Email string `json:"email"` } if err := c.BindJSON(&input, 1<<20); err != nil { return c.JSON(400, map[string]string{"error": err.Error()}) } return c.JSON(201, map[string]string{ "id": "new-id", "name": input.Name, "email": input.Email, }) }) // Create request with JSON body body := strings.NewReader(`{"name": "Alice", "email": "alice@example.com"}`) req := httptest.NewRequest("POST", "/users", body) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() app.ServeHTTP(rec, req) if rec.Code != 201 { t.Fatalf("expected 201, got %d: %s", rec.Code, rec.Body.String()) } } ``` ## Table-driven tests Use table-driven tests for thorough coverage: ```go theme={null} func TestUserEndpoints(t *testing.T) { app := setupTestApp() tests := []struct { name string method string path string body string wantStatus int wantBody string }{ { name: "get existing user", method: "GET", path: "/users/1", wantStatus: 200, wantBody: `"id":"1"`, }, { name: "get non-existent user", method: "GET", path: "/users/999", wantStatus: 404, wantBody: `"error"`, }, { name: "create user with valid data", method: "POST", path: "/users", body: `{"name":"Bob","email":"bob@test.com"}`, wantStatus: 201, }, { name: "create user with missing name", method: "POST", path: "/users", body: `{"email":"test@test.com"}`, wantStatus: 400, wantBody: `"error"`, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var body io.Reader if tt.body != "" { body = strings.NewReader(tt.body) } req := httptest.NewRequest(tt.method, tt.path, body) if tt.body != "" { req.Header.Set("Content-Type", "application/json") } rec := httptest.NewRecorder() app.ServeHTTP(rec, req) if rec.Code != tt.wantStatus { t.Errorf("status = %d, want %d", rec.Code, tt.wantStatus) } if tt.wantBody != "" && !strings.Contains(rec.Body.String(), tt.wantBody) { t.Errorf("body = %s, want to contain %s", rec.Body.String(), tt.wantBody) } }) } } ``` ## Testing middleware ### Test middleware in isolation ```go theme={null} func TestAuthMiddleware(t *testing.T) { authMiddleware := func(next mizu.Handler) mizu.Handler { return func(c *mizu.Ctx) error { token := c.Request().Header.Get("Authorization") if token == "" { return c.JSON(401, map[string]string{"error": "unauthorized"}) } return next(c) } } tests := []struct { name string authHeader string wantStatus int }{ {"with valid token", "Bearer token123", 200}, {"without token", "", 401}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { app := mizu.New() app.Use(authMiddleware) app.Get("/protected", func(c *mizu.Ctx) error { return c.Text(200, "OK") }) req := httptest.NewRequest("GET", "/protected", nil) if tt.authHeader != "" { req.Header.Set("Authorization", tt.authHeader) } rec := httptest.NewRecorder() app.ServeHTTP(rec, req) if rec.Code != tt.wantStatus { t.Errorf("status = %d, want %d", rec.Code, tt.wantStatus) } }) } } ``` ## Testing with dependencies ### Use interfaces for mocking ```go theme={null} // Define interface type UserRepository interface { Get(ctx context.Context, id string) (*User, error) Create(ctx context.Context, user *User) error } // Handler with dependency type UserHandler struct { repo UserRepository } func (h *UserHandler) GetUser(c *mizu.Ctx) error { user, err := h.repo.Get(c.Context(), c.Param("id")) if err != nil { return err } return c.JSON(200, user) } // Mock for testing type mockUserRepo struct { users map[string]*User } func (m *mockUserRepo) Get(ctx context.Context, id string) (*User, error) { if user, ok := m.users[id]; ok { return user, nil } return nil, errors.New("not found") } func (m *mockUserRepo) Create(ctx context.Context, user *User) error { m.users[user.ID] = user return nil } // Test with mock func TestGetUserWithMock(t *testing.T) { repo := &mockUserRepo{ users: map[string]*User{ "1": {ID: "1", Name: "Alice"}, }, } handler := &UserHandler{repo: repo} app := mizu.New() app.Get("/users/{id}", handler.GetUser) req := httptest.NewRequest("GET", "/users/1", nil) rec := httptest.NewRecorder() app.ServeHTTP(rec, req) if rec.Code != 200 { t.Errorf("expected 200, got %d", rec.Code) } } ``` ## Testing error handling ```go theme={null} func TestErrorHandler(t *testing.T) { app := mizu.New() // Custom error handler app.ErrorHandler(func(c *mizu.Ctx, err error) { var httpErr *HTTPError if errors.As(err, &httpErr) { c.JSON(httpErr.Code, map[string]string{"error": httpErr.Message}) } else { c.JSON(500, map[string]string{"error": "internal error"}) } }) // Handler that returns error app.Get("/fail", func(c *mizu.Ctx) error { return &HTTPError{Code: 400, Message: "bad request"} }) req := httptest.NewRequest("GET", "/fail", nil) rec := httptest.NewRecorder() app.ServeHTTP(rec, req) if rec.Code != 400 { t.Errorf("expected 400, got %d", rec.Code) } if !strings.Contains(rec.Body.String(), "bad request") { t.Errorf("expected error message in body: %s", rec.Body.String()) } } ``` ## Test helpers Create reusable test helpers: ```go theme={null} // testutil/testutil.go package testutil import ( "encoding/json" "net/http/httptest" "strings" "testing" "github.com/go-mizu/mizu" ) type TestClient struct { app *mizu.App t *testing.T } func NewClient(t *testing.T, app *mizu.App) *TestClient { return &TestClient{app: app, t: t} } func (c *TestClient) Get(path string) *httptest.ResponseRecorder { req := httptest.NewRequest("GET", path, nil) rec := httptest.NewRecorder() c.app.ServeHTTP(rec, req) return rec } func (c *TestClient) PostJSON(path string, body any) *httptest.ResponseRecorder { data, _ := json.Marshal(body) req := httptest.NewRequest("POST", path, strings.NewReader(string(data))) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() c.app.ServeHTTP(rec, req) return rec } func (c *TestClient) AssertStatus(rec *httptest.ResponseRecorder, want int) { if rec.Code != want { c.t.Errorf("status = %d, want %d", rec.Code, want) } } func (c *TestClient) ParseJSON(rec *httptest.ResponseRecorder, v any) { if err := json.Unmarshal(rec.Body.Bytes(), v); err != nil { c.t.Fatalf("failed to parse JSON: %v", err) } } ``` Usage: ```go theme={null} func TestWithHelper(t *testing.T) { app := setupApp() client := testutil.NewClient(t, app) rec := client.Get("/users/1") client.AssertStatus(rec, 200) var user User client.ParseJSON(rec, &user) if user.ID != "1" { t.Errorf("expected id=1, got %s", user.ID) } } ``` ## Running tests ```bash theme={null} # Run all tests go test ./... # Run with verbose output go test -v ./... # Run specific test go test -v -run TestGetUser ./handlers # Run with coverage go test -cover ./... # Generate coverage report go test -coverprofile=coverage.out ./... go tool cover -html=coverage.out ``` ## Best practices 1. **Test behavior, not implementation** - Focus on what the handler does 2. **Use table-driven tests** - Cover edge cases systematically 3. **Keep tests isolated** - Each test creates its own app instance 4. **Use interfaces for dependencies** - Makes mocking easy 5. **Test error paths** - Ensure proper error responses 6. **Use meaningful test names** - Describe what's being tested ## Next steps Deploy your tested app. Organize code for testability. # Welcome to Mizu Source: https://docs.go-mizu.dev/guides/welcome A lightweight, composable web framework for Go Mizu Logo # Welcome to Mizu Mizu is a lightweight, composable web framework for Go that builds on the standard library's `net/http` package. Like water (水, "mizu" in Japanese), it flows naturally with Go's conventions while adding just enough structure to make web development enjoyable. ## Get Started in 30 Seconds ```go theme={null} package main import "github.com/go-mizu/mizu" func main() { app := mizu.New() app.Get("/", func(c *mizu.Ctx) error { return c.JSON(200, map[string]string{ "message": "Hello, Mizu!", }) }) app.Listen(":3000") } ``` ```bash theme={null} go get github.com/go-mizu/mizu go run main.go # Visit http://localhost:3000 ``` ## Why Mizu? No magic, no reflection, no code generation. Mizu uses Go 1.22+ ServeMux patterns and standard interfaces. Production-ready middlewares for authentication, caching, rate limiting, and more. Thin abstractions over the standard library. Learn it once, use it everywhere. ## Next Steps Set up Mizu in your project. Build your first app in 5 minutes. Learn how App, Router, Handler, and Context work together. Step-by-step guides to build real applications. ## What Can You Build? | Use Case | Description | | ------------------ | ----------------------------------------------------- | | **REST APIs** | Build type-safe APIs with automatic validation | | **Websites** | Server-rendered pages with templates and static files | | **Real-time Apps** | SSE support for live updates | | **Microservices** | Lightweight services with health checks | | **Full-Stack** | Go backend + React/Vue/Svelte frontend | ## Requirements * **Go 1.22** or later (for enhanced ServeMux patterns) * Any operating system (Linux, macOS, Windows) ## Join the Community Chat with the community and get help. Star the repo, report issues, contribute code. # Adaptive Source: https://docs.go-mizu.dev/middlewares/adaptive Adaptive rate limiting middleware that adjusts based on system load. ## Overview The `adaptive` middleware dynamically adjusts rate limits based on system metrics like latency, error rate, and resource utilization. Use it when you need: * Auto-scaling rate limits * Load-based throttling * Self-tuning protection ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/adaptive" ``` ## Quick Start ```go theme={null} app := mizu.New() // Adaptive limiting based on latency app.Use(adaptive.New(adaptive.Options{ MinRate: 10, MaxRate: 1000, TargetLatency: 100 * time.Millisecond, })) ``` ## Configuration ### Options | Option | Type | Default | Description | | ----------------- | --------------- | ------- | ------------------------ | | `MinRate` | `int` | `10` | Minimum requests/sec | | `MaxRate` | `int` | `1000` | Maximum requests/sec | | `TargetLatency` | `time.Duration` | `100ms` | Target response time | | `TargetErrorRate` | `float64` | `0.01` | Target error rate (1%) | | `Window` | `time.Duration` | `10s` | Measurement window | | `AdjustInterval` | `time.Duration` | `1s` | Rate adjustment interval | ## Examples ### Latency-Based ```go theme={null} app.Use(adaptive.New(adaptive.Options{ MinRate: 50, MaxRate: 500, TargetLatency: 200 * time.Millisecond, })) ``` ### Error Rate-Based ```go theme={null} app.Use(adaptive.New(adaptive.Options{ MinRate: 10, MaxRate: 1000, TargetErrorRate: 0.05, // 5% error rate })) ``` ### Combined Metrics ```go theme={null} app.Use(adaptive.New(adaptive.Options{ MinRate: 50, MaxRate: 500, TargetLatency: 100 * time.Millisecond, TargetErrorRate: 0.01, Window: 30 * time.Second, AdjustInterval: 5 * time.Second, })) ``` ## How It Works 1. Measure latency and error rate in sliding window 2. If metrics exceed targets, decrease rate 3. If metrics are good, gradually increase rate 4. Rate stays within min/max bounds ## API Reference ### Functions ```go theme={null} // New creates adaptive rate limiter func New(opts Options) mizu.Middleware // GetCurrentRate returns current rate limit func GetCurrentRate(c *mizu.Ctx) int ``` ## Technical Details ### Architecture The adaptive middleware uses a token bucket algorithm with dynamic rate adjustment: * **Token Bucket**: Implements a refillable token bucket where tokens are added at the current rate per second * **Atomic Operations**: Uses `sync/atomic` for lock-free access to metrics (request count, error count, latency) * **Background Adjustment**: Runs a goroutine that periodically adjusts the rate based on collected metrics ### Rate Adjustment Algorithm The middleware adjusts rates based on three conditions: 1. **High Error Rate**: If error rate > threshold, reduce rate by 20% 2. **High Latency**: If average latency > target, reduce rate by 10% 3. **Good Performance**: If latency \< target/2 AND error rate \< threshold/2, increase rate by 10% All adjustments are clamped to the configured `MinRate` and `MaxRate` bounds. ### Metrics Collection The middleware tracks: * **Request Count**: Total requests in current adjustment interval * **Error Count**: Requests that returned errors * **Total Latency**: Cumulative response time in nanoseconds * **Current Rate**: Dynamically adjusted rate limit Metrics are atomically swapped and reset on each adjustment interval to prevent data races. ### Token Refill Mechanism Tokens are refilled based on elapsed time: ``` refill = (elapsed_nanoseconds * current_rate) / 1_second_in_nanoseconds ``` Tokens are capped at the current rate to prevent unbounded accumulation. ## Best Practices * Start with conservative limits * Set reasonable target latency * Monitor rate adjustments * Use with circuit breaker for full protection ## Testing The adaptive middleware includes comprehensive test coverage for all features and edge cases: | Test Case | Description | Expected Behavior | | --------------------------------------- | -------------------------------- | -------------------------------------------------------------------- | | `TestNew` | Basic middleware creation | Middleware processes request successfully | | `TestLimiter_RateLimited` | Rate limiting enforcement | First request succeeds, immediate second request is blocked with 429 | | `TestLimiter_CurrentRate` | Current rate retrieval | Returns the configured initial rate | | `TestLimiter_Stats` | Statistics retrieval | Returns current rate, min/max bounds, and request/error counts | | `TestSimple` | Simple preset configuration | Middleware works with default simple settings | | `TestHighThroughput` | High throughput preset | Handles 100 concurrent requests with some successes | | `TestConservative` | Conservative preset | Middleware works with conservative settings | | `TestRetryAfterHeader` | Retry-After header on rate limit | Returns "Retry-After: 1" header when rate limited | | `TestCustomErrorHandler` | Custom error handler | Uses custom JSON error response instead of default | | `TestNewLimiter_Defaults` | Default value handling | Negative values are replaced with sensible defaults | | `TestLimiter_Adjust` | Adjustment with no requests | Rate remains unchanged when no requests are made | | `TestLimiter_AdjustHighErrorRate` | High error rate adjustment | Rate decreases when error rate exceeds threshold | | `TestLimiter_AdjustHighLatency` | High latency adjustment | Rate decreases when latency exceeds target | | `TestLimiter_AdjustLowLatencyAndErrors` | Good performance adjustment | Rate increases when latency is low and errors are minimal | | `TestLimiter_AdjustClampToMin` | Minimum rate clamping | Rate cannot decrease below configured minimum | | `TestLimiter_AdjustClampToMax` | Maximum rate clamping | Rate cannot increase above configured maximum | | `TestLimiter_AllowTokenRefill` | Token refill mechanism | Tokens are refilled over time after exhaustion | | `TestLimiter_AllowTokenCap` | Token accumulation limit | Tokens are capped at current rate, not unlimited | | `TestLimiter_ErrorCounting` | Error metric tracking | Requests are counted correctly in statistics | ## Related Middlewares * [ratelimit](/middlewares/ratelimit) - Fixed rate limiting * [circuitbreaker](/middlewares/circuitbreaker) - Circuit breaker * [bulkhead](/middlewares/bulkhead) - Bulkhead pattern # Audit Source: https://docs.go-mizu.dev/middlewares/audit Audit logging middleware for tracking and recording all HTTP requests. ## Overview The `audit` middleware captures detailed information about every HTTP request for compliance, debugging, and security monitoring. It provides flexible handlers for storing audit logs. Use it when you need: * Compliance audit trails * Security event logging * Request debugging and analysis ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/audit" ``` ## Quick Start ```go theme={null} app := mizu.New() // Log all requests app.Use(audit.New(func(entry *audit.Entry) { log.Printf("%s %s -> %d", entry.Method, entry.Path, entry.Status) })) ``` ## Configuration ### Options | Option | Type | Default | Description | | -------------------- | ----------------------------------- | -------- | ------------------------- | | `Handler` | `func(*Entry)` | Required | Callback for each entry | | `Skip` | `func(*mizu.Ctx) bool` | `nil` | Skip logging for requests | | `IncludeRequestBody` | `bool` | `false` | Include request body | | `MaxBodySize` | `int` | `1024` | Max body size to capture | | `Metadata` | `func(*mizu.Ctx) map[string]string` | `nil` | Custom metadata | ### Entry Fields | Field | Type | Description | | ------------- | ------------------- | ------------------------- | | `Timestamp` | `time.Time` | Request time | | `Method` | `string` | HTTP method | | `Path` | `string` | Request path | | `Query` | `string` | Query string | | `Status` | `int` | Response status | | `Latency` | `time.Duration` | Request duration | | `IP` | `string` | Client IP | | `UserAgent` | `string` | User-Agent header | | `RequestID` | `string` | X-Request-ID header | | `RequestBody` | `string` | Request body (if enabled) | | `Metadata` | `map[string]string` | Custom metadata | ## Examples ### Basic Logging ```go theme={null} app.Use(audit.New(func(entry *audit.Entry) { fmt.Printf("[%s] %s %s %d %v\n", entry.Timestamp.Format(time.RFC3339), entry.Method, entry.Path, entry.Status, entry.Latency, ) })) ``` ### With Request Body ```go theme={null} app.Use(audit.WithOptions(audit.Options{ Handler: func(entry *audit.Entry) { log.Printf("Body: %s", entry.RequestBody) }, IncludeRequestBody: true, MaxBodySize: 4096, })) ``` ### Skip Health Checks ```go theme={null} app.Use(audit.WithOptions(audit.Options{ Handler: logEntry, Skip: func(c *mizu.Ctx) bool { return c.Request().URL.Path == "/health" }, })) ``` ### With Custom Metadata ```go theme={null} app.Use(audit.WithOptions(audit.Options{ Handler: logEntry, Metadata: func(c *mizu.Ctx) map[string]string { return map[string]string{ "version": "1.0", "env": os.Getenv("ENV"), "user_id": getUserID(c), } }, })) ``` ### Channel Handler (Async) ```go theme={null} ch := make(chan *audit.Entry, 100) // Consumer goroutine go func() { for entry := range ch { // Process entries asynchronously saveToDatabase(entry) } }() app.Use(audit.New(audit.ChannelHandler(ch))) ``` ### Buffered Handler (Batch) ```go theme={null} handler := audit.NewBufferedHandler( 100, // Batch size time.Minute, // Flush interval func(entries []*audit.Entry) { // Process batch bulkInsert(entries) }, ) defer handler.Close() app.Use(audit.New(handler.Handler())) ``` ## API Reference ### Functions ```go theme={null} // New creates audit middleware with handler func New(handler func(*Entry)) mizu.Middleware // WithOptions creates middleware with configuration func WithOptions(opts Options) mizu.Middleware // ChannelHandler creates async handler func ChannelHandler(ch chan *Entry) func(*Entry) // NewBufferedHandler creates batching handler func NewBufferedHandler(maxSize int, interval time.Duration, flush func([]*Entry)) *BufferedHandler ``` ### BufferedHandler Methods ```go theme={null} func (h *BufferedHandler) Handler() func(*Entry) func (h *BufferedHandler) Flush() func (h *BufferedHandler) Close() ``` ## Technical Details ### Implementation Architecture The audit middleware is built around a flexible handler pattern that processes audit entries containing comprehensive request/response metadata. #### Core Components **Entry Structure**: The `Entry` type captures all relevant information about an HTTP request/response cycle: * Request metadata (timestamp, method, path, query, remote address, user agent) * Request identification (request ID from configurable header) * Optional request body capture with size limits * Response status and latency measurement * Error information if handler returns an error * Custom metadata extension point **Response Writer Wrapping**: The middleware uses a custom `auditResponseWriter` that wraps the standard `http.ResponseWriter` to capture the response status code. This wrapper intercepts `WriteHeader` and `Write` calls to record the status, defaulting to 200 OK if not explicitly set. **Request Body Capture**: When enabled, the middleware reads the request body up to `MaxBodySize` using `io.LimitReader`, then restores it using `io.MultiReader` so downstream handlers can still access it. This ensures body capture doesn't interfere with normal request processing. **Timing Measurement**: Latency is calculated by recording the start time before calling the next handler and computing the elapsed duration after it returns. This provides accurate end-to-end request processing time. #### Handler Patterns **Synchronous Handler**: The basic handler function receives each entry immediately and processes it in the request path. Suitable for logging to stdout or fast operations. **Channel Handler**: Sends entries to a buffered channel for asynchronous processing. Uses a non-blocking select with default case to drop entries if the channel is full, preventing request blocking. **Buffered Handler**: Collects entries in memory and flushes them in batches either when reaching `maxSize` or on a timer interval. Includes proper mutex protection for concurrent access and a background goroutine for periodic flushing. ### Configuration Defaults * `RequestIDHeader`: "X-Request-ID" * `MaxBodySize`: 1024 bytes * `Handler`: Defaults to encoding JSON to `io.Discard` if not specified * `IncludeRequestBody`: false (opt-in for performance and privacy) ## Best Practices * Use async handlers for high-traffic applications * Set reasonable `MaxBodySize` to prevent memory issues * Skip logging for health checks and metrics endpoints * Include request IDs for correlation with other logs * Buffer writes for database storage ## Testing The audit middleware includes comprehensive test coverage for all functionality: | Test Case | Description | Expected Behavior | | ----------------------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------- | | `TestNew` | Basic middleware creation with handler | Captures all entry fields (method, path, query, user agent, request ID, status, latency) correctly | | `TestWithOptions_RequestBody` | Request body capture with size limit | Reads and stores request body up to MaxBodySize, body remains readable by handler | | `TestWithOptions_Skip` | Conditional logging with skip function | Skips audit logging for matched requests (/health), logs others normally | | `TestWithOptions_Metadata` | Custom metadata injection | Adds custom key-value pairs to entry metadata via callback function | | `TestWithOptions_Error` | Error handling and status capture | Records error status codes (500) correctly when handler returns error | | `TestChannelHandler` | Async channel-based handler | Sends entries to channel without blocking request processing | | `TestBufferedHandler` | Batch processing with size trigger | Buffers entries and flushes when batch size (3) is reached | | `TestBufferedHandler_Flush` | Manual flush operation | Immediately flushes buffered entries when Flush() is called | | `TestBufferedHandler_Handler` | Handler function retrieval | Returns valid handler function for middleware integration | ## Related Middlewares * [logger](/middlewares/logger) - Simple request logging * [requestid](/middlewares/requestid) - Request ID generation * [timing](/middlewares/timing) - Performance timing # Basic Auth Source: https://docs.go-mizu.dev/middlewares/basicauth HTTP Basic Authentication middleware for protecting routes with username/password. ## Overview The `basicauth` middleware provides HTTP Basic Authentication, a simple authentication scheme built into the HTTP protocol. It prompts users for a username and password, which are sent with each request. Use it when you need: * Simple password protection for admin areas * Quick authentication for internal tools * Development/staging environment protection ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/basicauth" ``` ## Quick Start ```go theme={null} app := mizu.New() // Define credentials credentials := map[string]string{ "admin": "secret", "user": "password", } // Protect all routes app.Use(basicauth.New(credentials)) // Or protect specific routes app.Get("/admin", adminHandler, basicauth.New(credentials)) ``` ## Configuration ### Options | Option | Type | Default | Description | | -------------- | ----------------------- | -------------- | -------------------------------------------- | | `Realm` | `string` | `"Restricted"` | Authentication realm shown in browser prompt | | `Validator` | `ValidatorFunc` | - | Custom function to validate credentials | | `ErrorHandler` | `func(*mizu.Ctx) error` | - | Custom handler for auth failures | ### ValidatorFunc ```go theme={null} type ValidatorFunc func(username, password string) bool ``` ## Examples ### Static Credentials ```go theme={null} credentials := basicauth.Accounts{ "admin": "secret123", "api": "token456", } app.Use(basicauth.New(credentials)) ``` ### Custom Realm ```go theme={null} app.Use(basicauth.WithRealm("Admin Area", map[string]string{ "admin": "secret", })) ``` ### Custom Validator Validate against a database or external service: ```go theme={null} app.Use(basicauth.WithValidator(func(user, pass string) bool { // Query database dbUser, err := db.GetUser(user) if err != nil { return false } // Verify password (use proper hashing!) return bcrypt.CompareHashAndPassword( []byte(dbUser.PasswordHash), []byte(pass), ) == nil })) ``` ### Custom Error Handler ```go theme={null} app.Use(basicauth.WithOptions(basicauth.Options{ Validator: func(user, pass string) bool { return user == "admin" && pass == "secret" }, ErrorHandler: func(c *mizu.Ctx) error { return c.JSON(401, map[string]string{ "error": "Invalid credentials", }) }, })) ``` ### Route-Specific Protection ```go theme={null} // Public routes app.Get("/", publicHandler) app.Get("/about", aboutHandler) // Protected admin routes adminAuth := basicauth.New(map[string]string{"admin": "secret"}) app.Get("/admin", adminDashboard, adminAuth) app.Get("/admin/users", adminUsers, adminAuth) ``` ### Group Protection ```go theme={null} admin := app.Group("/admin") admin.Use(basicauth.New(map[string]string{"admin": "secret"})) admin.Get("/", adminDashboard) admin.Get("/users", adminUsers) admin.Post("/users", createUser) ``` ## API Reference ### Functions ```go theme={null} // New creates middleware with static credentials func New(credentials map[string]string) mizu.Middleware // WithValidator creates middleware with custom validator func WithValidator(fn ValidatorFunc) mizu.Middleware // WithRealm creates middleware with custom realm func WithRealm(realm string, credentials map[string]string) mizu.Middleware // WithOptions creates middleware with full configuration func WithOptions(opts Options) mizu.Middleware ``` ### Types ```go theme={null} // Accounts is a convenience type for credentials type Accounts map[string]string ``` ## Technical Details The basicauth middleware implements HTTP Basic Authentication according to RFC 7617. Here are the key implementation details: ### Authentication Flow 1. **Header Extraction**: Extracts the `Authorization` header from the incoming request 2. **Scheme Validation**: Verifies the header starts with "Basic " prefix 3. **Base64 Decoding**: Decodes the base64-encoded credentials using `base64.StdEncoding.DecodeString()` 4. **Credential Parsing**: Splits the decoded string on the first colon (`:`) to separate username and password 5. **Validation**: Calls the configured validator function to verify credentials 6. **Response**: Either proceeds to the next handler or returns 401 Unauthorized with `WWW-Authenticate` header ### Security Features **Constant-Time Comparison**: The built-in validator uses `secureCompare()` function to prevent timing attacks: ```go theme={null} func secureCompare(a, b string) bool { // Hash both strings to ensure constant time comparison // even for strings of different lengths aHash := sha256.Sum256([]byte(a)) bHash := sha256.Sum256([]byte(b)) return subtle.ConstantTimeCompare(aHash[:], bHash[:]) == 1 } ``` This approach: * Hashes both strings using SHA-256 to normalize lengths * Uses `crypto/subtle.ConstantTimeCompare()` to prevent timing side-channels * Protects against timing attacks that could leak password information **WWW-Authenticate Header**: On authentication failure, the middleware sets the `WWW-Authenticate` header with the configured realm, prompting the browser to display a login dialog. ### Error Handling The middleware returns 401 Unauthorized in these cases: * Missing `Authorization` header * Invalid authorization scheme (not "Basic") * Invalid base64 encoding * Malformed credentials (no colon separator) * Validator function returns false Custom error handlers can be provided via `Options.ErrorHandler` to customize the response format (e.g., JSON instead of plain text). ## Security Considerations 1. **Always use HTTPS** - Basic auth sends credentials base64-encoded (not encrypted) 2. **Use strong passwords** - Avoid dictionary words and short passwords 3. **Hash stored passwords** - Never store plain-text passwords in your validator 4. **Consider rate limiting** - Combine with `ratelimit` to prevent brute force attacks 5. **Timing attacks** - The built-in validator uses constant-time comparison ## Best Practices * Use Basic Auth for simple internal tools, not production user-facing auth * Combine with HTTPS redirect middleware * Add rate limiting to prevent brute force attacks * Use Bearer auth or session-based auth for APIs ## Testing The basicauth middleware includes comprehensive test coverage. Below are all test cases: | Test Case | Description | Expected Behavior | | ----------------------------------------------- | ------------------------------------------------ | ---------------------------------------------------------------- | | **TestNew/allows valid credentials** | Valid username and password provided | Returns 200 OK and allows request through | | **TestNew/rejects invalid password** | Valid username with incorrect password | Returns 401 Unauthorized | | **TestNew/rejects unknown user** | Non-existent username provided | Returns 401 Unauthorized | | **TestNew/rejects missing auth** | No Authorization header present | Returns 401 Unauthorized with WWW-Authenticate header | | **TestWithValidator** | Custom validator function with valid credentials | Returns 200 OK when validator returns true | | **TestWithRealm** | Custom realm configuration | Returns 401 with WWW-Authenticate header containing custom realm | | **TestWithOptions\_ErrorHandler** | Custom error handler on auth failure | Calls custom error handler returning JSON response | | **TestWithOptions\_InvalidAuth/not basic** | Authorization header with Bearer scheme | Returns 401 Unauthorized | | **TestWithOptions\_InvalidAuth/invalid base64** | Malformed base64 in Authorization header | Returns 401 Unauthorized | | **TestWithOptions\_InvalidAuth/no colon** | Base64 credentials without colon separator | Returns 401 Unauthorized | | **TestWithOptions\_Panics** | Creating middleware without validator | Panics with error message | | **TestSecureCompare/password\_password** | Comparing identical strings | Returns true | | **TestSecureCompare/password\_different** | Comparing different strings | Returns false | | **TestSecureCompare/short\_longpassword** | Comparing strings of different lengths | Returns false | | **TestSecureCompare/empty\_empty** | Comparing empty strings | Returns true | ## Related Middlewares * [bearerauth](/middlewares/bearerauth) - Token-based authentication * [keyauth](/middlewares/keyauth) - API key authentication * [secure](/middlewares/secure) - HTTPS enforcement * [ratelimit](/middlewares/ratelimit) - Rate limiting # Bearer Auth Source: https://docs.go-mizu.dev/middlewares/bearerauth Bearer token authentication middleware for API protection. ## Overview The `bearerauth` middleware validates Bearer tokens from the `Authorization` header. It's commonly used for API authentication with JWTs, OAuth tokens, or custom tokens. Use it when you need: * API authentication with tokens * JWT validation * OAuth 2.0 Bearer token protection * Custom token authentication ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/bearerauth" ``` ## Quick Start ```go theme={null} app := mizu.New() // Simple token validation app.Use(bearerauth.New(func(token string) bool { return token == "my-secret-token" })) ``` ## Configuration ### Options | Option | Type | Default | Description | | ---------------------- | ------------------------------ | ----------------- | ---------------------------- | | `Validator` | `TokenValidator` | - | Validates the token | | `ValidatorWithContext` | `TokenValidatorWithContext` | - | Validates and returns claims | | `Header` | `string` | `"Authorization"` | Header to read token from | | `AuthScheme` | `string` | `"Bearer"` | Expected auth scheme prefix | | `ErrorHandler` | `func(*mizu.Ctx, error) error` | - | Custom error handler | ### Validator Types ```go theme={null} // Simple validator type TokenValidator func(token string) bool // Validator that returns claims type TokenValidatorWithContext func(token string) (claims any, valid bool) ``` ## Examples ### Simple Token Validation ```go theme={null} validTokens := map[string]bool{ "token-1": true, "token-2": true, } app.Use(bearerauth.New(func(token string) bool { return validTokens[token] })) ``` ### JWT Validation with Claims ```go theme={null} type UserClaims struct { UserID string Username string Role string } app.Use(bearerauth.WithOptions(bearerauth.Options{ ValidatorWithContext: func(token string) (any, bool) { // Parse and validate JWT claims, err := parseJWT(token) if err != nil { return nil, false } return &UserClaims{ UserID: claims["sub"].(string), Username: claims["username"].(string), Role: claims["role"].(string), }, true }, })) // Access claims in handler func protectedHandler(c *mizu.Ctx) error { claims, ok := bearerauth.Claims[*UserClaims](c) if !ok { return c.Text(401, "Unauthorized") } return c.JSON(200, map[string]string{ "user": claims.Username, "role": claims.Role, }) } ``` ### Custom Header ```go theme={null} // Use X-API-Token instead of Authorization app.Use(bearerauth.WithHeader("X-API-Token", func(token string) bool { return validateToken(token) })) ``` ### Custom Auth Scheme ```go theme={null} // Use "Token" instead of "Bearer" app.Use(bearerauth.WithOptions(bearerauth.Options{ AuthScheme: "Token", Validator: func(token string) bool { return validateToken(token) }, })) ``` ### Custom Error Handler ```go theme={null} app.Use(bearerauth.WithOptions(bearerauth.Options{ Validator: validateToken, ErrorHandler: func(c *mizu.Ctx, err error) error { switch err { case bearerauth.ErrTokenMissing: return c.JSON(401, map[string]string{ "error": "Authentication required", }) case bearerauth.ErrTokenInvalid: return c.JSON(403, map[string]string{ "error": "Invalid token", }) default: return c.JSON(401, map[string]string{ "error": err.Error(), }) } }, })) ``` ### Database Token Lookup ```go theme={null} app.Use(bearerauth.WithOptions(bearerauth.Options{ ValidatorWithContext: func(token string) (any, bool) { // Look up token in database session, err := db.GetSession(token) if err != nil || session.IsExpired() { return nil, false } // Return user info as claims return &UserInfo{ ID: session.UserID, Email: session.UserEmail, }, true }, })) ``` ## API Reference ### Functions ```go theme={null} // New creates middleware with simple validator func New(validator TokenValidator) mizu.Middleware // WithHeader creates middleware reading from custom header func WithHeader(header string, validator TokenValidator) mizu.Middleware // WithOptions creates middleware with full configuration func WithOptions(opts Options) mizu.Middleware ``` ### Extracting Data ```go theme={null} // FromContext extracts token or claims func FromContext(c *mizu.Ctx) any // Token extracts the token string func Token(c *mizu.Ctx) string // Claims extracts typed claims (generic) func Claims[T any](c *mizu.Ctx) (T, bool) ``` ### Error Types ```go theme={null} var ( ErrTokenMissing // Token not found in request ErrTokenInvalid // Token validation failed ErrInvalidScheme // Auth scheme doesn't match ) ``` ## Technical Details ### Implementation Overview The bearerauth middleware implements Bearer token authentication following RFC 6750 standards. It extracts and validates tokens from HTTP headers, storing the token or associated claims in the request context for downstream handlers. ### Core Components 1. **Token Extraction**: The middleware reads the configured header (default: "Authorization") and extracts the token after the auth scheme prefix (default: "Bearer "). 2. **Validation Flow**: * Checks if the authorization header exists * Verifies the auth scheme matches the configured scheme * Extracts the token after removing the scheme prefix * Invokes either `Validator` or `ValidatorWithContext` to validate the token * Stores the token or claims in the request context using a private context key 3. **Context Storage**: The middleware uses a private `contextKey` struct to store authentication data in the request context. This prevents key collisions with other middleware or application code. 4. **Error Handling**: Three error types are defined: * `ErrTokenMissing`: Returned when no token is found (401 Unauthorized) * `ErrInvalidScheme`: Returned when auth scheme doesn't match (403 Forbidden) * `ErrTokenInvalid`: Returned when validation fails (403 Forbidden) ### Validator Types * **TokenValidator**: Simple boolean validation for basic token checking * **TokenValidatorWithContext**: Returns claims along with validation result, enabling stateful authentication with user data ### Data Retrieval Three helper functions extract authentication data from context: * `FromContext(c)`: Returns raw token or claims (type `any`) * `Token(c)`: Returns token string (only when using `Validator`) * `Claims[T](c)`: Type-safe claims extraction (when using `ValidatorWithContext`) ## Security Considerations 1. **Token Security** - Use cryptographically secure tokens 2. **HTTPS Required** - Always use HTTPS to protect tokens in transit 3. **Token Expiration** - Implement token expiration 4. **Secure Storage** - Store tokens securely on the client 5. **Revocation** - Implement token revocation for logout/security ## Best Practices * Use short-lived tokens with refresh tokens for better security * Validate token claims (expiration, issuer, audience) * Log authentication failures for security monitoring * Consider rate limiting failed attempts ## Testing The bearerauth middleware includes comprehensive test coverage for all authentication scenarios: | Test Case | Description | Expected Behavior | | ---------------------------------------------------- | ---------------------------------------------------- | ----------------------------------------------------------- | | `TestNew/allows_valid_token` | Valid Bearer token in Authorization header | Returns 200 OK and allows access to protected resource | | `TestNew/rejects_invalid_token` | Invalid Bearer token provided | Returns 403 Forbidden | | `TestNew/rejects_missing_token` | No Authorization header present | Returns 401 Unauthorized | | `TestNew/rejects_wrong_scheme` | Authorization header with wrong scheme (e.g., Basic) | Returns 403 Forbidden | | `TestWithHeader` | Custom header name (X-Api-Token) with valid token | Returns 200 OK when token is valid | | `TestWithOptions_ValidatorWithContext` | ValidatorWithContext returns user claims | Claims are stored in context and accessible via FromContext | | `TestWithOptions_ErrorHandler` | Custom error handler for authentication failures | Custom error handler is invoked with proper error | | `TestWithOptions_CustomScheme/accepts_custom_scheme` | Custom auth scheme "Token" instead of "Bearer" | Returns 200 OK for matching scheme | | `TestWithOptions_CustomScheme/rejects_bearer_scheme` | Bearer scheme when Token scheme is configured | Returns 403 Forbidden for mismatched scheme | | `TestToken` | Token extraction using Token() helper | Token string is correctly extracted from context | | `TestWithOptions_Panics` | Creating middleware without validator | Panics with appropriate message | | `TestClaims` | Type-safe claims extraction using Claims[T]() | Claims are properly typed and accessible | | `TestErrors` | Error message formatting | Error constants return correct messages | ## Related Middlewares * [basicauth](/middlewares/basicauth) - Username/password authentication * [keyauth](/middlewares/keyauth) - API key authentication * [csrf](/middlewares/csrf) - CSRF protection # Body Close Source: https://docs.go-mizu.dev/middlewares/bodyclose Auto body close middleware to ensure request bodies are properly closed. ## Overview The `bodyclose` middleware ensures request bodies are properly closed after processing, preventing resource leaks. Use it when you need: * Prevent connection leaks * Ensure proper resource cleanup * Handle abandoned requests ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/bodyclose" ``` ## Quick Start ```go theme={null} app := mizu.New() // Auto-close request bodies app.Use(bodyclose.New()) ``` ## Examples ### Basic Usage ```go theme={null} app.Use(bodyclose.New()) ``` ### With Drain ```go theme={null} // Drain body before closing (for keep-alive) app.Use(bodyclose.WithDrain()) ``` ## API Reference ### Functions ```go theme={null} // New creates body close middleware with default options func New() mizu.Middleware // WithOptions creates bodyclose middleware with custom options func WithOptions(opts Options) mizu.Middleware // Drain creates middleware that drains request bodies func Drain() mizu.Middleware // NoDrain creates middleware that closes without draining func NoDrain() mizu.Middleware ``` ### Options ```go theme={null} type Options struct { // DrainBody drains the body before closing. // Default: true. DrainBody bool // MaxDrain is the maximum bytes to drain. // Default: 8KB. MaxDrain int64 } ``` ## Technical Details The bodyclose middleware operates by wrapping the request handler with a deferred cleanup function that ensures proper resource management: ### Implementation Strategy 1. **Nil Body Check**: Immediately passes control to the next handler if the request body is nil, avoiding unnecessary overhead 2. **Deferred Cleanup**: Uses Go's `defer` statement to guarantee body cleanup regardless of handler outcome (success, error, or panic) 3. **Optional Draining**: When `DrainBody` is enabled, reads and discards up to `MaxDrain` bytes before closing to enable HTTP keep-alive connection reuse 4. **Connection Reuse**: By draining the body, the middleware signals to the HTTP client that the connection can be safely reused in connection pools ### Resource Management * **Default MaxDrain**: 8KB (8192 bytes) to balance connection reuse benefits with performance * **Drain Mechanism**: Uses `io.CopyN` to limit the amount of data drained, preventing excessive memory consumption on large bodies * **Error Handling**: Silently ignores drain and close errors to prevent interference with the main request flow ### Performance Characteristics * **Minimal Overhead**: Only adds a single function call and defer statement per request * **Memory Efficient**: Draining is capped by MaxDrain to prevent memory exhaustion * **No Blocking**: Draining respects the MaxDrain limit and doesn't attempt to fully read unbounded bodies ## Why It Matters Without proper body closing: * Connections may not be reused * Memory leaks can occur * Connection pool exhaustion ## Best Practices * Place early in middleware chain * Use with connection pooling * Consider draining for HTTP keep-alive ## Testing The bodyclose middleware includes comprehensive test coverage to ensure proper functionality: | Test Case | Description | Expected Behavior | | --------------------------- | -------------------------------------------------- | ----------------------------------------------------------------- | | `TestNew` | Basic middleware creation with default options | Request body is properly handled, returns HTTP 200 OK | | `TestWithOptions_DrainBody` | Middleware with DrainBody enabled | Body is closed after being drained, tracking confirms closure | | `TestWithOptions_NoDrain` | Middleware with DrainBody disabled | Body is closed without draining, tracking confirms closure | | `TestNilBody` | Request with nil body (GET request) | Middleware handles nil body gracefully, returns HTTP 200 OK | | `TestDrain` | Using the Drain() convenience function | Body is drained before closing, request completes successfully | | `TestNoDrain` | Using the NoDrain() convenience function | Body is closed without draining, request completes successfully | | `TestWithOptions_MaxDrain` | Custom MaxDrain limit (10 bytes on 1000-byte body) | Only drains up to MaxDrain bytes, prevents excessive memory usage | ### Test Coverage The test suite uses a custom `trackingBody` type that monitors: * Whether the body was closed (`closed` field) * Whether the body was fully drained (`drained` field) This ensures that the middleware behaves correctly in various scenarios including unread bodies, partial reads, and different configuration options. ## Related Middlewares * [bodylimit](/middlewares/bodylimit) - Limit body size * [bodydump](/middlewares/bodydump) - Dump request bodies * [timeout](/middlewares/timeout) - Request timeout # Body Dump Source: https://docs.go-mizu.dev/middlewares/bodydump Request/response body dumping middleware for debugging. ## Overview The `bodydump` middleware captures and logs request and response bodies for debugging purposes. Use it when you need: * Debug API requests * Log request/response pairs * Troubleshoot integrations ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/bodydump" ``` ## Quick Start ```go theme={null} app := mizu.New() app.Use(bodydump.New(func(c *mizu.Ctx, reqBody, resBody []byte) { log.Printf("Request: %s", reqBody) log.Printf("Response: %s", resBody) })) ``` ## Configuration ### Options | Option | Type | Default | Description | | ------------------ | --------------------------------- | -------- | ------------------------ | | `Request` | `bool` | `true` | Dump request body | | `Response` | `bool` | `true` | Dump response body | | `MaxSize` | `int64` | `64KB` | Max body size to capture | | `Handler` | `func(*mizu.Ctx, []byte, []byte)` | Required | Dump callback | | `SkipPaths` | `[]string` | `nil` | Paths to skip | | `SkipContentTypes` | `[]string` | `nil` | Content types to skip | ## Examples ### Basic Logging ```go theme={null} app.Use(bodydump.New(func(c *mizu.Ctx, req, res []byte) { log.Printf("[%s %s] Req: %s Res: %s", c.Request().Method, c.Request().URL.Path, req, res) })) ``` ### Skip Large Bodies ```go theme={null} app.Use(bodydump.WithOptions(bodydump.Options{ Handler: dumpHandler, MaxSize: 1024, // Only dump first 1KB })) ``` ### Skip Specific Paths ```go theme={null} app.Use(bodydump.WithOptions(bodydump.Options{ Handler: dumpHandler, SkipPaths: []string{"/health", "/metrics"}, })) ``` ### Skip Content Types ```go theme={null} app.Use(bodydump.WithOptions(bodydump.Options{ Handler: dumpHandler, SkipContentTypes: []string{"image/jpeg", "image/png"}, })) ``` ### Request Only ```go theme={null} app.Use(bodydump.RequestOnly(func(c *mizu.Ctx, body []byte) { log.Printf("Request: %s", body) })) ``` ### Response Only ```go theme={null} app.Use(bodydump.ResponseOnly(func(c *mizu.Ctx, body []byte) { log.Printf("Response: %s", body) })) ``` ### JSON Formatting ```go theme={null} app.Use(bodydump.New(func(c *mizu.Ctx, req, res []byte) { entry := map[string]any{ "method": c.Request().Method, "path": c.Request().URL.Path, "request": string(req), "response": string(res), } json.NewEncoder(os.Stdout).Encode(entry) })) ``` ## API Reference ### Functions ```go theme={null} // New creates body dump middleware func New(handler func(*mizu.Ctx, []byte, []byte)) mizu.Middleware // WithOptions creates with configuration func WithOptions(opts Options) mizu.Middleware // RequestOnly dumps only request bodies func RequestOnly(handler func(*mizu.Ctx, []byte)) mizu.Middleware // ResponseOnly dumps only response bodies func ResponseOnly(handler func(*mizu.Ctx, []byte)) mizu.Middleware ``` ## Technical Details ### Implementation Overview The bodydump middleware uses a capture mechanism to intercept and store request/response bodies: 1. **Request Body Capture**: Reads the request body using `io.LimitReader` to respect the `MaxSize` limit, then restores it using `io.NopCloser` with a bytes reader so downstream handlers can still access it. 2. **Response Body Capture**: Implements a custom `responseCapture` wrapper that implements `http.ResponseWriter`. It intercepts `Write()` calls to capture the response body up to the configured `MaxSize` while still writing to the original response writer. 3. **Filtering Mechanisms**: * **Path Filtering**: Uses a map lookup for O(1) skip path checks * **Content-Type Filtering**: Skips configured content types to avoid dumping binary data * **Size Limiting**: Enforces maximum capture size on both request (via `io.LimitReader`) and response (via byte counting) ### Default Values * `MaxSize`: 64KB (65,536 bytes) * `Request`: true (enabled) * `Response`: true (enabled) ### Performance Considerations * Path and content-type skipping uses hash maps for efficient lookups * Body capture is limited by `MaxSize` to prevent memory issues * Request body is read once and restored for handler use * Response capture adds minimal overhead as it writes through to the original writer ## Security Warning Body dump may capture sensitive data. Never use in production without proper filtering. ## Best Practices * Only enable in development/debugging * Filter sensitive data * Set reasonable size limits * Skip high-volume endpoints ## Testing The bodydump middleware includes comprehensive test coverage for all major functionality: | Test Case | Description | Expected Behavior | | ------------------------------ | ---------------------------------------------- | ------------------------------------------------------------------ | | `TestNew` | Basic middleware creation with default options | Captures both request and response bodies correctly | | `TestWithOptions_RequestOnly` | Request-only dumping configuration | Dumps only request body, response body is empty | | `TestWithOptions_ResponseOnly` | Response-only dumping configuration | Dumps only response body, request body is empty | | `TestWithOptions_MaxSize` | Maximum size limit enforcement | Request body is truncated to MaxSize bytes (5 bytes in test) | | `TestWithOptions_SkipPaths` | Path skipping functionality | Handler is not called for paths in SkipPaths array | | `TestRequestOnly` | RequestOnly helper function | Captures only request body using simplified API | | `TestResponseOnly` | ResponseOnly helper function | Captures only response body using simplified API | | `TestBodyPreserved` | Request body preservation | Request body remains readable by downstream handlers after dumping | ## Related Middlewares * [logger](/middlewares/logger) - Request logging * [audit](/middlewares/audit) - Audit logging * [requestlog](/middlewares/requestlog) - Detailed logging # Body Limit Source: https://docs.go-mizu.dev/middlewares/bodylimit Request body size limiting middleware for security and resource protection. ## Overview The `bodylimit` middleware limits the size of request bodies to protect against large payload attacks and resource exhaustion. Use it when you need: * Protection against oversized requests * Memory usage control * Upload size limits * DoS attack prevention ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/bodylimit" ``` ## Quick Start ```go theme={null} app := mizu.New() // Limit to 10MB app.Use(bodylimit.New(bodylimit.MB(10))) ``` ## Configuration ### Options | Option | Type | Default | Description | | -------------- | ----------------------- | ------- | -------------------------- | | `Limit` | `int64` | `1MB` | Maximum body size in bytes | | `ErrorHandler` | `func(*mizu.Ctx) error` | - | Custom error handler | ## Examples ### Basic Limit ```go theme={null} // 1MB limit (default) app.Use(bodylimit.New(1 << 20)) // Using helper functions app.Use(bodylimit.New(bodylimit.KB(500))) // 500KB app.Use(bodylimit.New(bodylimit.MB(10))) // 10MB app.Use(bodylimit.New(bodylimit.GB(1))) // 1GB ``` ### Custom Error Handler ```go theme={null} app.Use(bodylimit.WithHandler(bodylimit.MB(10), func(c *mizu.Ctx) error { return c.JSON(413, map[string]string{ "error": "Request body too large", "max_size": "10MB", }) })) ``` ### Full Options ```go theme={null} app.Use(bodylimit.WithOptions(bodylimit.Options{ Limit: bodylimit.MB(5), ErrorHandler: func(c *mizu.Ctx) error { return c.Text(413, "File too large. Maximum size is 5MB.") }, })) ``` ### Different Limits Per Route ```go theme={null} // Global: 1MB app.Use(bodylimit.New(bodylimit.MB(1))) // File upload route: 100MB app.Post("/upload", uploadHandler, bodylimit.New(bodylimit.MB(100))) // Avatar upload: 2MB app.Post("/avatar", avatarHandler, bodylimit.New(bodylimit.MB(2))) ``` ### API vs Upload Routes ```go theme={null} // API routes: small limit api := app.Group("/api") api.Use(bodylimit.New(bodylimit.KB(100))) // 100KB for JSON // Upload routes: large limit uploads := app.Group("/uploads") uploads.Use(bodylimit.New(bodylimit.MB(50))) // 50MB for files ``` ## API Reference ### Functions ```go theme={null} // New creates middleware with byte limit func New(limit int64) mizu.Middleware // WithHandler creates middleware with custom error handler func WithHandler(limit int64, handler func(*mizu.Ctx) error) mizu.Middleware // WithOptions creates middleware with full options func WithOptions(opts Options) mizu.Middleware ``` ### Helper Functions ```go theme={null} // KB returns bytes for kilobytes func KB(n int64) int64 // MB returns bytes for megabytes func MB(n int64) int64 // GB returns bytes for gigabytes func GB(n int64) int64 ``` ## How It Works 1. **Content-Length Check**: If `Content-Length` header exceeds limit, returns 413 immediately 2. **Body Wrapping**: Wraps request body with `http.MaxBytesReader` 3. **Read Enforcement**: If body read exceeds limit, returns error ## Technical Details ### Implementation The bodylimit middleware uses a two-stage approach to enforce body size limits: 1. **Pre-flight Validation**: Before processing the request, it checks the `Content-Length` header against the configured limit. If the header indicates the body will exceed the limit, the middleware immediately returns a 413 status code without reading the body. 2. **Runtime Enforcement**: The middleware wraps the request body with `http.MaxBytesReader`, which enforces the limit during body reading operations. This catches cases where: * The `Content-Length` header is missing or set to -1 (unknown length) * The actual body size differs from the declared `Content-Length` * Chunked transfer encoding is used ### Default Behavior * **Default Limit**: 1MB (`1 << 20 bytes`) when `Limit` is set to 0 or negative value * **Default Error Response**: Returns HTTP 413 (Request Entity Too Large) with plain text message * **Body Wrapping**: Uses `http.MaxBytesReader` to wrap the request body, which provides graceful error handling during reads ### Error Handling The middleware supports custom error handlers through the `ErrorHandler` option. When the limit is exceeded: 1. If a custom `ErrorHandler` is configured, it is invoked with the request context 2. Otherwise, returns default 413 response with status text ### Helper Functions The middleware provides convenient helper functions for common size units: * `KB(n)`: Converts kilobytes to bytes (n × 1024) * `MB(n)`: Converts megabytes to bytes (n × 1024 × 1024) * `GB(n)`: Converts gigabytes to bytes (n × 1024 × 1024 × 1024) ## Best Practices * Set appropriate limits based on expected data * Use smaller limits for JSON APIs * Use larger limits for file uploads * Consider user experience in error messages * Monitor rejected requests ## Testing The bodylimit middleware includes comprehensive test coverage for all functionality: | Test Case | Description | Expected Behavior | | ---------------------------------------------- | ------------------------------------------------------------------------- | -------------------------------------------------------------------------- | | `TestNew/allows small body` | Tests that bodies under the limit are accepted | Returns 200 OK and the full body content | | `TestNew/rejects large body by content-length` | Tests Content-Length header validation | Returns 413 Request Entity Too Large when Content-Length exceeds limit | | `TestNew/rejects large body during read` | Tests enforcement during body reading when Content-Length is unknown (-1) | Returns error during body read operation | | `TestWithHandler` | Tests custom error handler functionality | Calls custom error handler and returns JSON error response with 413 status | | `TestWithOptions_Default` | Tests default 1MB limit when no limit is specified | Accepts 512KB body with default configuration | | `TestHelpers/KB` | Tests KB helper function with various inputs | Correctly converts KB to bytes (1KB = 1024, 10KB = 10240) | | `TestHelpers/MB` | Tests MB helper function with various inputs | Correctly converts MB to bytes (1MB = 1048576, 5MB = 5242880) | | `TestHelpers/GB` | Tests GB helper function with various inputs | Correctly converts GB to bytes (1GB = 1073741824, 2GB = 2147483648) | | `TestNew_WithHelpers/allows under limit` | Tests helper functions integrated with New() for bodies under limit | Accepts 5KB body with 10KB limit | | `TestNew_WithHelpers/rejects over limit` | Tests helper functions integrated with New() for bodies over limit | Rejects 15KB body with 10KB limit, returns 413 | ## Related Middlewares * [contenttype](/middlewares/contenttype) - Content-Type validation * [requestsize](/middlewares/requestsize) - Track request size # Bot Source: https://docs.go-mizu.dev/middlewares/bot Bot detection middleware for identifying automated traffic. ## Overview The `bot` middleware detects bot traffic based on user-agent patterns, behavior analysis, and known bot signatures. Use it when you need: * Bot traffic identification * Search engine crawler handling * Automated traffic filtering ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/bot" ``` ## Quick Start ```go theme={null} app := mizu.New() app.Use(bot.New()) app.Get("/", func(c *mizu.Ctx) error { if bot.IsBot(c) { return c.Text(200, "Hello, bot!") } return c.HTML(200, fullPage) }) ``` ## Configuration ### Options | Option | Type | Default | Description | | ----------- | ---------- | -------- | --------------------- | | `Block` | `bool` | `false` | Block detected bots | | `AllowGood` | `bool` | `true` | Allow known good bots | | `Patterns` | `[]string` | Built-in | Additional patterns | ## Examples ### Detect Bots ```go theme={null} app.Use(bot.New()) app.Get("/", func(c *mizu.Ctx) error { if bot.IsBot(c) { // Serve simplified content return c.Text(200, "Content") } return c.HTML(200, richContent) }) ``` ### Block Bad Bots ```go theme={null} app.Use(bot.New(bot.Options{ Block: true, AllowGood: true, // Allow Google, Bing, etc. })) ``` ### Custom Patterns ```go theme={null} app.Use(bot.New(bot.Options{ Patterns: []string{ "MyCustomBot", "InternalCrawler", }, })) ``` ### Bot-Specific Routes ```go theme={null} app.Get("/sitemap.xml", func(c *mizu.Ctx) error { return c.XML(200, sitemap) }) app.Get("/", func(c *mizu.Ctx) error { info := bot.Info(c) if info.IsSearchEngine { // SEO-optimized content } return c.HTML(200, content) }) ``` ## API Reference ### Functions ```go theme={null} // New creates bot detection middleware func New(opts ...Options) mizu.Middleware // IsBot returns true if request is from bot func IsBot(c *mizu.Ctx) bool // Info returns detailed bot information func Info(c *mizu.Ctx) *BotInfo ``` ### BotInfo ```go theme={null} type BotInfo struct { IsBot bool IsGoodBot bool IsSearchEngine bool Name string Category string } ``` ## Known Good Bots * Googlebot * Bingbot * Slurp (Yahoo) * DuckDuckBot * Baiduspider ## Technical Details ### User-Agent Pattern Matching The middleware uses compiled regular expressions for efficient pattern matching: * **Default Patterns**: Includes search engines (Googlebot, Bingbot), social bots (Facebook, Twitter), SEO tools (Semrush, Ahrefs), and HTTP clients (curl, wget) * **Pattern Compilation**: All patterns are compiled into a single regex at initialization for O(n) matching * **Case Insensitive**: Matching is case-insensitive to handle various User-Agent formats ### Bot Categories Detected bots are categorized: * `search`: Googlebot, Bingbot, Yandexbot, Baiduspider, DuckDuckBot * `social`: Facebook, Twitter, LinkedIn, Pinterest * `seo`: Semrush, Ahrefs, Majestic, MJ12bot * `tool`: curl, wget, Python requests, Go HTTP client * `crawler`: Generic crawlers, spiders, scrapers ### Context Storage Bot information is stored in the request context using `context.WithValue()` for thread-safe access throughout the request lifecycle. ### Allowlist/Blocklist Logic When `BlockBots` is enabled: 1. If `AllowedBots` is set, only listed bots are allowed (allowlist mode) 2. If `BlockedBots` is set, only listed bots are blocked (blocklist mode) 3. If neither is set, all detected bots are blocked ## Best Practices * Allow search engine bots for SEO * Serve simplified content to bots * Log bot traffic for analysis * Rate limit suspicious bots ## Testing The middleware includes comprehensive test coverage. Key test cases: | Test Case | Description | Expected Behavior | | --------------------------------------------- | ----------------------------- | -------------------------------------------------- | | `TestNew/detect_googlebot` | Detects Googlebot user agent | IsBot=true, BotName="googlebot", Category="search" | | `TestNew/detect_curl` | Detects curl HTTP client | IsBot=true, Category="tool" | | `TestNew/normal_browser` | Normal browser user agent | IsBot=false | | `TestWithOptions_BlockBots/block_bot` | Block detected bots | Returns 403 Forbidden | | `TestWithOptions_BlockBots/allow_browser` | Allow normal browsers | Returns 200 OK | | `TestWithOptions_AllowedBots/allowed_bot` | Allowlist mode with Googlebot | Returns 200 OK for allowed bot | | `TestWithOptions_AllowedBots/non-allowed_bot` | Allowlist mode with curl | Returns 403 Forbidden | | `TestWithOptions_BlockedBots/blocked_bot` | Blocklist mode with Semrush | Returns 403 Forbidden | | `TestWithOptions_CustomPatterns` | Custom bot pattern detection | Detects custom bot pattern | | `TestWithOptions_ErrorHandler` | Custom error handler | Returns JSON response | | `TestIsBot` | Helper function test | Returns true for bots | | `TestBotName` | Bot name extraction | Returns detected bot name | | `TestBlock` | Block all bots helper | Returns 403 for any bot | | `TestAllow` | Allow specific bots helper | Allows listed bots only | | `TestDeny` | Deny specific bots helper | Blocks listed bots | ## Related Middlewares * [ratelimit](/middlewares/ratelimit) - Rate limiting * [ipfilter](/middlewares/ipfilter) - IP filtering * [xrequestedwith](/middlewares/xrequestedwith) - AJAX detection # Bulkhead Source: https://docs.go-mizu.dev/middlewares/bulkhead Bulkhead pattern middleware for isolating failures and limiting concurrency. ## Overview The `bulkhead` middleware implements the bulkhead pattern, limiting concurrent requests to prevent cascade failures and ensure fair resource allocation. Use it when you need: * Failure isolation between services * Concurrent request limiting * Resource protection ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/bulkhead" ``` ## Quick Start ```go theme={null} app := mizu.New() // Limit to 10 concurrent requests app.Use(bulkhead.New(10)) ``` ## Configuration ### Options | Option | Type | Default | Description | | --------------- | ----------------------- | -------- | --------------------------------- | | `MaxConcurrent` | `int` | Required | Max concurrent requests | | `MaxWaiting` | `int` | `0` | Max waiting requests (0 = reject) | | `Timeout` | `time.Duration` | `0` | Wait timeout | | `ErrorHandler` | `func(*mizu.Ctx) error` | - | Custom rejection handler | ## Examples ### Simple Limit ```go theme={null} // Reject if more than 10 concurrent app.Use(bulkhead.New(10)) ``` ### With Waiting Queue ```go theme={null} // Allow 10 concurrent, 20 waiting app.Use(bulkhead.WithOptions(bulkhead.Options{ MaxConcurrent: 10, MaxWaiting: 20, })) ``` ### With Timeout ```go theme={null} // Wait up to 5 seconds for slot app.Use(bulkhead.WithOptions(bulkhead.Options{ MaxConcurrent: 10, MaxWaiting: 50, Timeout: 5 * time.Second, })) ``` ### Per-Route Limits ```go theme={null} // Different limits for different routes api := app.Group("/api") api.Use(bulkhead.New(100)) heavy := app.Group("/heavy") heavy.Use(bulkhead.New(5)) ``` ### Custom Error ```go theme={null} app.Use(bulkhead.WithOptions(bulkhead.Options{ MaxConcurrent: 10, ErrorHandler: func(c *mizu.Ctx) error { return c.JSON(503, map[string]string{ "error": "Service temporarily unavailable", }) }, })) ``` ## API Reference ### Functions ```go theme={null} // New creates bulkhead with max concurrent func New(maxConcurrent int) mizu.Middleware // WithOptions creates bulkhead with configuration func WithOptions(opts Options) mizu.Middleware ``` ## How It Works 1. Request arrives 2. Check if under concurrent limit 3. If under: acquire slot, process, release 4. If over: check waiting queue 5. If queue full or timeout: reject with 503 ## HTTP Status Codes | Code | Meaning | | ---- | ----------------------------------- | | 503 | Service unavailable (bulkhead full) | ## Technical Details ### Implementation The bulkhead middleware uses a semaphore pattern with buffered channels to control concurrency: * **Semaphore Channel**: A buffered channel with capacity equal to `MaxConcurrent` acts as the semaphore for slot allocation * **Waiting Queue**: A counter tracks the number of requests waiting for a slot, capped at `MaxWait` * **Non-blocking Acquire**: First attempts to acquire a slot without blocking using `select` with `default` * **Blocking Wait**: If no slot is available, increments the waiting counter and blocks until a slot becomes available or context is cancelled * **Thread Safety**: Uses `sync.Mutex` to protect the waiting counter and ensure thread-safe operations ### Core Components **Bulkhead Structure**: * `sem chan struct{}`: Buffered channel for semaphore-based slot management * `waiting int`: Current number of requests in the waiting queue * `maxWait int`: Maximum allowed requests in the waiting queue * `mu sync.Mutex`: Mutex for protecting shared state **Manager**: * Manages multiple named bulkheads for isolation between different services or paths * Thread-safe bulkhead creation and retrieval using `sync.RWMutex` * Provides aggregated statistics across all managed bulkheads **Statistics**: * Real-time metrics including active requests, waiting requests, and available slots * Useful for monitoring and debugging bulkhead behavior ### Request Flow 1. Request arrives at middleware 2. Attempts non-blocking slot acquisition via `select` statement 3. If slot acquired: processes request and releases slot via `defer` 4. If no slot available: * Checks if waiting queue is full * If full: rejects immediately with error handler or 503 status * If space available: increments waiting counter and blocks on semaphore channel 5. When slot becomes available or context cancelled: decrements waiting counter 6. On context cancellation: returns context error (e.g., timeout, cancellation) ## Best Practices * Set limits based on resource capacity * Use different bulkheads for different services * Monitor rejection rates * Combine with circuit breaker for full resilience ## Testing ### Test Coverage | Test Case | Description | Expected Behavior | | ------------------------------ | ----------------------------------------------------------- | ------------------------------------------------------------------------------------- | | `TestNew` | Tests basic bulkhead functionality with concurrent requests | Some requests succeed (200), others are rejected (503) when bulkhead is full | | `TestBulkhead_Stats` | Validates bulkhead statistics reporting | Returns correct name, max active count, and available slots | | `TestNewBulkhead_ErrorHandler` | Tests custom error handler for rejected requests | Rejected requests use custom error handler with 429 status instead of default 503 | | `TestManager` | Tests bulkhead manager's get-or-create behavior | Returns same bulkhead instance for same name, different instances for different names | | `TestManager_Stats` | Validates manager's aggregated statistics | Returns stats for all managed bulkheads by name | | `TestForPath` | Tests path-based bulkhead isolation | Creates separate bulkheads for different URL paths | | `TestDefaults` | Validates default configuration values | Uses default values of 10 for MaxConcurrent and MaxWait when not specified | ## Related Middlewares * [circuitbreaker](/middlewares/circuitbreaker) - Circuit breaker pattern * [ratelimit](/middlewares/ratelimit) - Rate limiting * [concurrency](/middlewares/concurrency) - Concurrency control # Cache Source: https://docs.go-mizu.dev/middlewares/cache Cache-Control header middleware for HTTP caching. ## Overview The `cache` middleware sets Cache-Control headers to control HTTP caching behavior. It helps optimize performance by instructing browsers and CDNs how to cache responses. ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/cache" ``` ## Quick Start ```go theme={null} app := mizu.New() // Cache for 1 hour app.Use(cache.New(time.Hour)) ``` ## Configuration | Option | Type | Description | | ---------------------- | --------------- | -------------------------- | | `MaxAge` | `time.Duration` | Browser cache duration | | `SMaxAge` | `time.Duration` | CDN/proxy cache duration | | `Public` | `bool` | Allow public caching | | `Private` | `bool` | Restrict to browser only | | `NoCache` | `bool` | Require revalidation | | `NoStore` | `bool` | Don't cache at all | | `NoTransform` | `bool` | Prevent transformations | | `MustRevalidate` | `bool` | Must check freshness | | `Immutable` | `bool` | Content never changes | | `StaleWhileRevalidate` | `time.Duration` | Serve stale while fetching | | `StaleIfError` | `time.Duration` | Serve stale on errors | ## Examples ### Basic Caching ```go theme={null} // Cache for 1 hour app.Use(cache.New(time.Hour)) ``` ### Public Cache (CDN) ```go theme={null} // Allow CDN caching app.Get("/static/*", staticHandler, cache.Public(24*time.Hour)) ``` ### Private Cache (Browser Only) ```go theme={null} // User-specific data, browser only app.Get("/profile", profileHandler, cache.Private(time.Hour)) ``` ### Immutable Assets ```go theme={null} // Never changes (versioned assets) app.Get("/assets/*", assetHandler, cache.Immutable(365*24*time.Hour)) ``` ### Stale-While-Revalidate ```go theme={null} // Serve stale for 1 hour while revalidating app.Get("/api/feed", feedHandler, cache.SWR(time.Minute, time.Hour)) ``` ### Full Configuration ```go theme={null} app.Use(cache.WithOptions(cache.Options{ MaxAge: time.Hour, SMaxAge: 24 * time.Hour, Public: true, MustRevalidate: true, StaleWhileRevalidate: 10 * time.Minute, })) ``` ### Different Cache Strategies ```go theme={null} // API: Short cache with revalidation api := app.Group("/api") api.Use(cache.SWR(time.Minute, 5*time.Minute)) // Static: Long cache, immutable static := app.Group("/static") static.Use(cache.Immutable(365*24*time.Hour)) // Private data: Browser only private := app.Group("/user") private.Use(cache.Private(5*time.Minute)) ``` ## API Reference ```go theme={null} func New(maxAge time.Duration) mizu.Middleware func WithOptions(opts Options) mizu.Middleware func Public(maxAge time.Duration) mizu.Middleware func Private(maxAge time.Duration) mizu.Middleware func Immutable(maxAge time.Duration) mizu.Middleware func Static(maxAge time.Duration) mizu.Middleware func SWR(maxAge, stale time.Duration) mizu.Middleware ``` ## Common Patterns | Use Case | Directive | | ------------- | ------------------------------------- | | Static files | `public, max-age=31536000, immutable` | | API responses | `private, max-age=60` | | CDN cached | `public, s-maxage=86400, max-age=60` | | User data | `private, no-cache` | | Never cache | `no-store` | ## Technical Details ### Implementation The cache middleware operates by constructing and setting the `Cache-Control` HTTP header based on the provided configuration options. The middleware: 1. **Header Construction**: Uses the `buildCacheControl` function to build the Cache-Control header string from the Options struct 2. **Non-Overriding Behavior**: Only sets the Cache-Control header if it hasn't been set by the handler or previous middleware 3. **Default Fallback**: Returns `no-cache` when no options are specified 4. **Time Conversion**: Converts Go `time.Duration` values to seconds for cache directives (max-age, s-maxage, etc.) ### Cache-Control Directives The middleware supports the following HTTP Cache-Control directives: * **public**: Response may be cached by any cache (browsers, CDNs, proxies) * **private**: Response is intended for a single user and should only be cached by the browser * **no-cache**: Cache must revalidate with the origin server before using cached content * **no-store**: Response must not be stored in any cache * **no-transform**: Intermediaries must not transform the response (e.g., image compression) * **must-revalidate**: Cache must verify the status of stale resources and expired ones should not be used * **proxy-revalidate**: Same as must-revalidate but only for shared caches * **max-age=N**: Maximum time (in seconds) a resource is considered fresh * **s-maxage=N**: Overrides max-age for shared caches (CDNs, proxies) * **immutable**: Resource will never change during its freshness lifetime * **stale-while-revalidate=N**: Cache may serve stale content while revalidating in the background * **stale-if-error=N**: Cache may serve stale content if an error occurs during revalidation ### Helper Functions The package provides several convenience functions that configure common caching strategies: * **New(maxAge)**: Basic caching with max-age * **Public(maxAge)**: Public caching with max-age * **Private(maxAge)**: Private caching with max-age * **Immutable(maxAge)**: Immutable public content with max-age * **Static(maxAge)**: Alias for Immutable, suitable for static assets * **SWR(maxAge, stale)**: Stale-while-revalidate pattern for improved perceived performance ## Best Practices 1. **Use appropriate cache strategies** for different content types 2. **Leverage stale-while-revalidate** for improved user experience 3. **Set immutable** for versioned static assets 4. **Use private** for user-specific data 5. **Combine with CDN** using s-maxage for better edge caching ## Testing The cache middleware includes comprehensive test coverage for all functionality: | Test Case | Description | Expected Behavior | | ---------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------- | | TestNew | Basic middleware creation with max-age | Sets Cache-Control header with max-age=3600 for 1 hour duration | | TestWithOptions - public with max-age | Public cache with max-age | Contains "public" and "max-age=3600" directives | | TestWithOptions - private with max-age | Private cache with max-age | Contains "private" and "max-age=300" directives | | TestWithOptions - no-cache | No-cache directive | Contains "no-cache" directive | | TestWithOptions - no-store | No-store directive | Contains "no-store" directive | | TestWithOptions - must-revalidate | Must-revalidate with max-age | Contains "must-revalidate" and "max-age=3600" directives | | TestWithOptions - immutable | Immutable content | Contains "public" and "immutable" directives | | TestWithOptions - s-maxage | Shared cache max-age | Contains "max-age=3600" and "s-maxage=7200" directives | | TestWithOptions - stale-while-revalidate | Stale-while-revalidate pattern | Contains "max-age=3600" and "stale-while-revalidate=1800" directives | | TestPublic | Public cache helper function | Cache-Control header contains "public" directive | | TestPrivate | Private cache helper function | Cache-Control header contains "private" directive | | TestImmutable | Immutable cache helper function | Cache-Control header contains "immutable" directive | | TestStatic | Static assets cache helper function | Cache-Control header contains "public" and "immutable" directives | | TestSWR | Stale-while-revalidate helper function | Cache-Control header contains "stale-while-revalidate=1800" directive | | TestWithOptions\_DoesNotOverride | Existing Cache-Control header preservation | Middleware does not override pre-existing Cache-Control headers | | TestWithOptions\_Empty | Empty options handling | Defaults to "no-cache" when no options are provided | ## Security Considerations * **Sensitive data**: Always use `private` or `no-store` for sensitive information * **User-specific content**: Never use `public` for personalized data * **Authentication**: Consider using `no-cache` or `no-store` for authenticated routes ## Related Middlewares * [nocache](/middlewares/nocache) - Prevent caching # Canary Source: https://docs.go-mizu.dev/middlewares/canary Canary deployment middleware for gradual traffic shifting. ## Overview The `canary` middleware enables canary deployments by routing a percentage of traffic to different handlers, allowing gradual rollouts. Use it when you need: * Gradual feature rollouts * A/B testing * Blue-green deployments ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/canary" ``` ## Quick Start ```go theme={null} app := mizu.New() // Route 10% to canary app.Use(canary.New(canary.Options{ Canary: canaryHandler, Percent: 10, })) ``` ## Configuration ### Options | Option | Type | Default | Description | | --------- | -------------- | -------- | ----------------------- | | `Canary` | `mizu.Handler` | Required | Canary handler | | `Percent` | `int` | `0` | Percentage to canary | | `Header` | `string` | `""` | Force canary via header | | `Cookie` | `string` | `""` | Force canary via cookie | | `Sticky` | `bool` | `false` | Sticky sessions | ## Examples ### Percentage-Based ```go theme={null} app.Use(canary.New(canary.Options{ Canary: v2Handler, Percent: 5, // 5% to v2 })) ``` ### With Sticky Sessions ```go theme={null} app.Use(canary.New(canary.Options{ Canary: v2Handler, Percent: 10, Sticky: true, // Same user always gets same version })) ``` ### Header Override ```go theme={null} app.Use(canary.New(canary.Options{ Canary: v2Handler, Percent: 10, Header: "X-Canary", // Force canary with X-Canary: true })) ``` ### Cookie-Based ```go theme={null} app.Use(canary.New(canary.Options{ Canary: v2Handler, Cookie: "canary", // Check canary=true cookie })) ``` ### Gradual Rollout ```go theme={null} // Start at 1%, increase over time percent := atomic.Int32{} percent.Store(1) app.Use(canary.New(canary.Options{ Canary: v2Handler, PercentFunc: func() int { return int(percent.Load()) }, })) // Increase percentage via admin endpoint app.Post("/admin/canary", func(c *mizu.Ctx) error { p, _ := strconv.Atoi(c.Query("percent")) percent.Store(int32(p)) return c.Text(200, "OK") }) ``` ## API Reference ### Functions ```go theme={null} // New creates canary middleware func New(opts Options) mizu.Middleware ``` ## Technical Details ### Implementation Overview The canary middleware uses a deterministic counter-based approach for traffic distribution: * **Counter-Based Distribution**: Uses an atomic counter (`atomic.AddUint64`) that increments on each request. The modulo operation (`counter % 100 < percentage`) ensures predictable distribution over time. * **Context Storage**: Stores the canary decision in the request context using a private context key, accessible via `IsCanary(c)`. * **Override Precedence**: Decision order is: Header override > Cookie override > Custom Selector > Percentage-based selection. ### Key Components **Core Functions:** * `New(percentage int)`: Creates middleware with simple percentage-based routing * `WithOptions(opts Options)`: Creates middleware with full configuration options * `IsCanary(c *mizu.Ctx)`: Checks if current request is using canary version * `Route(canary, stable Handler)`: Routes to different handlers based on canary status * `Middleware(canaryMw, stableMw)`: Applies different middleware chains based on canary status **ReleaseManager:** * Manages multiple named canary releases * Each release has independent counter and percentage * Useful for managing multiple feature rollouts simultaneously **Selectors:** * `RandomSelector(percentage)`: Uses `math/rand` for random selection (non-cryptographic) * `HeaderSelector(header, value)`: Selects based on header value * `CookieSelector(name, value)`: Selects based on cookie value * Custom selectors via `Options.Selector` function ### Security Notes The implementation intentionally uses `math/rand` (not `crypto/rand`) for performance: * Canary selection is non-security-critical * `gosec G404` warnings are suppressed with explanation * The counter-based default approach is fully deterministic ## Best Practices * Start with low percentages (1-5%) * Monitor error rates for canary traffic * Use sticky sessions for stateful applications * Provide override mechanism for testing ## Testing ### Test Coverage | Test Case | Description | Expected Behavior | | ---------------------------------------- | ---------------------------------------------------- | --------------------------------------------------------------------- | | TestNew | Basic percentage-based canary routing with 50% split | Approximately 50% of 100 requests are marked as canary (40-60% range) | | TestWithOptions\_Header (with header) | Header override with X-Canary: true | Request is marked as canary when header is present | | TestWithOptions\_Header (without header) | No header with 0% percentage | Request is not marked as canary | | TestWithOptions\_Cookie | Cookie-based canary selection | Request is marked as canary when cookie matches | | TestWithOptions\_Selector | Custom selector based on User-Agent | Request is marked as canary when User-Agent equals "Canary" | | TestRoute | Route function with stable traffic | Returns "stable" response for non-canary requests | | TestRoute\_Canary | Route function with canary header | Returns "canary" response when X-Canary header is set | | TestMiddleware (stable) | Middleware selection for stable traffic | Applies stable middleware, sets X-Version: stable header | | TestMiddleware (canary) | Middleware selection for canary traffic | Applies canary middleware, sets X-Version: canary header | | TestReleaseManager | ReleaseManager set/get operations | Can set and retrieve release configurations by name | | TestReleaseManager\_ShouldUseCanary | ReleaseManager percentage distribution | Approximately 50% canary selection over 100 calls (40-60% range) | | TestHeaderSelector (matching) | HeaderSelector with matching value | Selector returns true for matching header value | | TestHeaderSelector (non-matching) | HeaderSelector with non-matching value | Selector returns false for non-matching header value | | TestCookieSelector | CookieSelector with matching cookie | Selector returns true for matching cookie name and value | ## Related Middlewares * [feature](/middlewares/feature) - Feature flags * [mirror](/middlewares/mirror) - Request mirroring * [version](/middlewares/version) - API versioning # CAPTCHA Source: https://docs.go-mizu.dev/middlewares/captcha CAPTCHA verification middleware supporting reCAPTCHA, hCaptcha, and Turnstile. ## Overview The `captcha` middleware verifies CAPTCHA tokens to protect forms and APIs from bots. It supports multiple providers including Google reCAPTCHA (v2/v3), hCaptcha, and Cloudflare Turnstile. Use it when you need: * Bot protection for forms * Spam prevention * Account creation protection * API abuse prevention ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/captcha" ``` ## Quick Start ```go theme={null} app := mizu.New() // Google reCAPTCHA v2 app.Use(captcha.ReCaptchaV2("your-secret-key")) ``` ## Configuration ### Options | Option | Type | Default | Description | | -------------- | ----------------------------------------------- | ----------------------------- | ------------------------------ | | `Provider` | `Provider` | `ProviderRecaptchaV2` | CAPTCHA provider | | `Secret` | `string` | - | Secret key from provider | | `TokenLookup` | `string` | `"form:g-recaptcha-response"` | Where to find token | | `MinScore` | `float64` | `0.5` | Minimum score for v3 providers | | `Verifier` | `func(token string, c *mizu.Ctx) (bool, error)` | - | Custom verifier | | `ErrorHandler` | `func(*mizu.Ctx, error) error` | - | Custom error handler | | `SkipPaths` | `[]string` | - | Paths to skip verification | | `Timeout` | `time.Duration` | `10s` | Verification timeout | ### Providers ```go theme={null} const ( ProviderRecaptchaV2 // Google reCAPTCHA v2 (checkbox) ProviderRecaptchaV3 // Google reCAPTCHA v3 (invisible) ProviderHCaptcha // hCaptcha ProviderTurnstile // Cloudflare Turnstile ProviderCustom // Custom verification ) ``` ## Examples ### Google reCAPTCHA v2 ```go theme={null} app.Use(captcha.ReCaptchaV2(os.Getenv("RECAPTCHA_SECRET"))) ``` Frontend: ```html theme={null}
    ``` ### Google reCAPTCHA v3 ```go theme={null} // Require minimum score of 0.7 app.Use(captcha.ReCaptchaV3(os.Getenv("RECAPTCHA_SECRET"), 0.7)) ``` Frontend: ```html theme={null}
    ``` ### hCaptcha ```go theme={null} app.Use(captcha.HCaptcha(os.Getenv("HCAPTCHA_SECRET"))) ``` Frontend: ```html theme={null}
    ``` ### Cloudflare Turnstile ```go theme={null} app.Use(captcha.Turnstile(os.Getenv("TURNSTILE_SECRET"))) ``` Frontend: ```html theme={null}
    ``` ### Skip Certain Paths ```go theme={null} app.Use(captcha.WithOptions(captcha.Options{ Provider: captcha.ProviderRecaptchaV2, Secret: os.Getenv("RECAPTCHA_SECRET"), SkipPaths: []string{ "/api/webhook", // External webhooks "/api/health", // Health checks }, })) ``` ### Custom Token Location ```go theme={null} // Token from header instead of form app.Use(captcha.WithOptions(captcha.Options{ Provider: captcha.ProviderRecaptchaV3, Secret: secret, TokenLookup: "header:X-Captcha-Token", })) ``` ### Custom Error Handler ```go theme={null} app.Use(captcha.WithOptions(captcha.Options{ Provider: captcha.ProviderRecaptchaV2, Secret: secret, ErrorHandler: func(c *mizu.Ctx, err error) error { if err == captcha.ErrMissingToken { return c.JSON(400, map[string]string{ "error": "Please complete the CAPTCHA", }) } return c.JSON(403, map[string]string{ "error": "CAPTCHA verification failed. Please try again.", }) }, })) ``` ### Custom Verifier ```go theme={null} app.Use(captcha.Custom( func(token string, c *mizu.Ctx) (bool, error) { // Custom verification logic return myVerifier.Verify(token, c.ClientIP()) }, "form:my-captcha-token", )) ``` ### Route-Specific Protection ```go theme={null} // Only protect specific routes registerCaptcha := captcha.ReCaptchaV2(secret) loginCaptcha := captcha.ReCaptchaV3(secret, 0.5) app.Post("/register", registerHandler, registerCaptcha) app.Post("/login", loginHandler, loginCaptcha) ``` ## API Reference ### Functions ```go theme={null} // New creates middleware with default provider func New(secret string) mizu.Middleware // WithOptions creates middleware with custom options func WithOptions(opts Options) mizu.Middleware // Provider-specific constructors func ReCaptchaV2(secret string) mizu.Middleware func ReCaptchaV3(secret string, minScore float64) mizu.Middleware func HCaptcha(secret string) mizu.Middleware func Turnstile(secret string) mizu.Middleware // Custom creates middleware with custom verifier func Custom(verifier func(string, *mizu.Ctx) (bool, error), tokenLookup string) mizu.Middleware ``` ### Error Types ```go theme={null} var ( ErrMissingToken // Token not found in request ErrInvalidToken // Token verification failed ErrVerifyFailed // Verification request failed ) ``` ## Provider Comparison | Provider | Type | Privacy | Free Tier | | ------------ | --------- | --------------- | --------- | | reCAPTCHA v2 | Checkbox | Google | Yes | | reCAPTCHA v3 | Invisible | Google | Yes | | hCaptcha | Checkbox | Privacy-focused | Yes | | Turnstile | Invisible | Cloudflare | Yes | ## Best Practices * Use v3/invisible CAPTCHAs for better UX * Set appropriate score thresholds for v3 * Monitor verification failures for tuning * Have fallback for CAPTCHA failures * Test with automation tools ## Technical Details ### Token Extraction The middleware supports extracting tokens from three locations: * **Form**: `form:field-name` - Extracts from POST form data * **Header**: `header:Header-Name` - Extracts from HTTP headers * **Query**: `query:param-name` - Extracts from URL query parameters The `TokenLookup` option uses the format `source:key` where source is one of `form`, `header`, or `query`. ### Verification Flow 1. **Request Filtering**: Skips verification for safe methods (GET, HEAD, OPTIONS) and configured skip paths 2. **Token Extraction**: Retrieves token based on `TokenLookup` configuration 3. **Verification**: * For custom verifiers: Calls the provided `Verifier` function * For provider verifiers: Makes HTTP POST to provider's API with secret, token, and client IP 4. **Score Validation**: For v3 providers (reCAPTCHA v3, Turnstile), validates score against `MinScore` threshold 5. **Error Handling**: Uses custom `ErrorHandler` if provided, otherwise returns default 400 response ### Client IP Detection The middleware detects client IP in the following order: 1. `X-Forwarded-For` header (first IP in comma-separated list) 2. `X-Real-IP` header 3. `RemoteAddr` from request (strips port if present) The detected IP is sent to the provider API for enhanced verification. ### Provider API Integration Each provider has a specific verification endpoint: * **reCAPTCHA v2/v3**: `https://www.google.com/recaptcha/api/siteverify` * **hCaptcha**: `https://hcaptcha.com/siteverify` * **Turnstile**: `https://challenges.cloudflare.com/turnstile/v0/siteverify` The middleware sends: * `secret`: Your secret key * `response`: The CAPTCHA token * `remoteip`: Client IP address (optional) ### Default Values | Setting | Default Value | | ----------- | ----------------------------- | | Provider | `ProviderRecaptchaV2` | | TokenLookup | `"form:g-recaptcha-response"` | | MinScore | `0.5` | | Timeout | `10s` | ### Error Types The middleware defines three specific error types: * `ErrMissingToken`: Token not found in the specified lookup location * `ErrInvalidToken`: Token verification failed (invalid or low score) * `ErrVerifyFailed`: HTTP request to provider API failed ## Testing ### Test Coverage | Test Case | Description | Expected Behavior | | ------------------------------------------ | --------------------------------------- | -------------------------------------------------- | | `TestWithOptions_MissingToken` | POST request without CAPTCHA token | Returns 400 Bad Request | | `TestWithOptions_CustomVerifier` (valid) | Custom verifier with valid token | Request proceeds (200 OK) | | `TestWithOptions_CustomVerifier` (invalid) | Custom verifier with invalid token | Returns 400 Bad Request | | `TestWithOptions_SkipSafeMethods` | GET request without token | Request proceeds (200 OK), CAPTCHA skipped | | `TestWithOptions_SkipPaths` | POST to path in SkipPaths | Request proceeds (200 OK), CAPTCHA skipped | | `TestWithOptions_HeaderToken` | Token provided via HTTP header | Token extracted from header, verification succeeds | | `TestWithOptions_QueryToken` | Token provided via query parameter | Token extracted from query, verification succeeds | | `TestWithOptions_CustomErrorHandler` | Invalid token with custom error handler | Custom handler returns 403 Forbidden with JSON | | `TestCustom` | Custom verifier function | Custom verification logic applied successfully | | `TestNew` | Creating middleware with New() | Middleware instance created with defaults | | `TestErrors` | Error message validation | All error types return correct messages | | `TestExtractToken_InvalidLookup` | TokenLookup without colon separator | Returns 400 Bad Request (token not found) | | `TestVerifyToken_WithMockServer` (valid) | Valid token verified by mock server | Returns 200 OK | | `TestVerifyToken_WithMockServer` (invalid) | Invalid token verified by mock server | Returns 400 Bad Request | | `TestVerifyToken_RecaptchaV3Score` (high) | reCAPTCHA v3 with score above threshold | Returns 200 OK | | `TestVerifyToken_RecaptchaV3Score` (low) | reCAPTCHA v3 with score below threshold | Returns 400 Bad Request | | `TestVerifyToken_ServerError` | Provider API returns 500 error | Returns 400 Bad Request | | `TestVerifyToken_InvalidJSON` | Provider returns malformed JSON | Returns 400 Bad Request | | `TestVerifyToken_UnknownProvider` | Unknown provider type | Returns 400 Bad Request | | `TestGetClientIP_XForwardedFor` | X-Forwarded-For header present | Extracts first IP from header, sends to provider | | `TestGetClientIP_XRealIP` | X-Real-IP header present | Extracts IP from header, sends to provider | | `TestReCaptchaV2` | ReCaptchaV2 constructor | Creates middleware with reCAPTCHA v2 config | | `TestReCaptchaV3` | ReCaptchaV3 constructor | Creates middleware with reCAPTCHA v3 config | | `TestHCaptcha` | HCaptcha constructor | Creates middleware with hCaptcha config | | `TestTurnstile` | Turnstile constructor | Creates middleware with Turnstile config | | `TestWithOptions_VerifierError` | Custom verifier returns error | Returns 400 Bad Request | | `TestWithOptions_HeadMethod` | HEAD request without token | Request proceeds (200 OK), CAPTCHA skipped | | `TestWithOptions_OptionsMethod` | OPTIONS request without token | Request proceeds (200 OK), CAPTCHA skipped | ## Security Considerations 1. **Secret Key** - Never expose secret key in frontend code 2. **Score Tuning** - Adjust v3 scores based on traffic patterns 3. **Timeout** - Set reasonable timeout for verification 4. **Fallback** - Have manual review process for edge cases ## Related Middlewares * [honeypot](/middlewares/honeypot) - Hidden field bot detection * [ratelimit](/middlewares/ratelimit) - Rate limiting # Chaos Source: https://docs.go-mizu.dev/middlewares/chaos Chaos engineering middleware for resilience testing. ## Overview The `chaos` middleware injects failures and latency into your application for chaos engineering and resilience testing. Use it to verify how your system handles errors and delays. ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/chaos" ``` ## Quick Start ```go theme={null} app := mizu.New() // Inject 10% errors with random latency app.Use(chaos.WithOptions(chaos.Options{ Enabled: true, ErrorRate: 10, LatencyMin: 100 * time.Millisecond, LatencyMax: 500 * time.Millisecond, })) ``` ## Configuration | Option | Type | Default | Description | | ------------ | ---------------------- | ------- | -------------------------------------- | | `Enabled` | `bool` | `false` | Enable chaos injection | | `ErrorRate` | `int` | `0` | Percentage of requests to fail (0-100) | | `ErrorCode` | `int` | `500` | HTTP status code for errors | | `LatencyMin` | `time.Duration` | `0` | Minimum latency to inject | | `LatencyMax` | `time.Duration` | `0` | Maximum latency to inject | | `Selector` | `func(*mizu.Ctx) bool` | - | Filter which requests to affect | ## Examples ### Error Injection ```go theme={null} // Fail 20% of requests with 500 error app.Use(chaos.Error(20, 500)) ``` ### Latency Injection ```go theme={null} // Add 100-500ms latency to all requests app.Use(chaos.Latency(100*time.Millisecond, 500*time.Millisecond)) ``` ### Combined Chaos ```go theme={null} app.Use(chaos.WithOptions(chaos.Options{ Enabled: true, ErrorRate: 5, // 5% errors ErrorCode: 503, // Service Unavailable LatencyMin: 50 * time.Millisecond, LatencyMax: 200 * time.Millisecond, })) ``` ### Selective Chaos (Path-based) ```go theme={null} app.Use(chaos.WithOptions(chaos.Options{ Enabled: true, ErrorRate: 50, Selector: chaos.PathSelector("/api/orders", "/api/payments"), })) // Only affects /api/orders and /api/payments ``` ### Selective Chaos (Method-based) ```go theme={null} app.Use(chaos.WithOptions(chaos.Options{ Enabled: true, ErrorRate: 10, Selector: chaos.MethodSelector("POST", "PUT", "DELETE"), })) // Only affects write operations ``` ### Header-triggered Chaos ```go theme={null} app.Use(chaos.WithOptions(chaos.Options{ Enabled: true, ErrorRate: 100, Selector: chaos.HeaderSelector("X-Chaos-Test"), })) // Only affects requests with X-Chaos-Test header ``` ### Dynamic Control ```go theme={null} controller := chaos.NewController() app.Use(controller.Middleware()) // Control chaos via admin endpoints app.Post("/admin/chaos/enable", func(c *mizu.Ctx) error { controller.Enable() return c.Text(200, "Chaos enabled") }) app.Post("/admin/chaos/disable", func(c *mizu.Ctx) error { controller.Disable() return c.Text(200, "Chaos disabled") }) app.Post("/admin/chaos/config", func(c *mizu.Ctx) error { rate, _ := strconv.Atoi(c.FormValue("error_rate")) controller.SetErrorRate(rate) latencyMs, _ := strconv.Atoi(c.FormValue("latency_ms")) controller.SetLatency( time.Duration(latencyMs/2)*time.Millisecond, time.Duration(latencyMs)*time.Millisecond, ) return c.Text(200, "Configured") }) app.Get("/admin/chaos/status", func(c *mizu.Ctx) error { return c.JSON(200, map[string]bool{ "enabled": controller.IsEnabled(), }) }) ``` ### Environment-Based ```go theme={null} // Only enable in testing environment if os.Getenv("CHAOS_ENABLED") == "true" { rate, _ := strconv.Atoi(os.Getenv("CHAOS_ERROR_RATE")) app.Use(chaos.WithOptions(chaos.Options{ Enabled: true, ErrorRate: rate, })) } ``` ### Custom Selector ```go theme={null} app.Use(chaos.WithOptions(chaos.Options{ Enabled: true, ErrorRate: 30, Selector: func(c *mizu.Ctx) bool { // Only affect non-admin users userRole := c.Get("user_role") return userRole != "admin" }, })) ``` ### Per-Route Chaos ```go theme={null} // Global middleware (disabled) app.Use(chaos.New()) // Enable chaos for specific route app.Get("/test/chaos", chaos.Error(50, 500), func(c *mizu.Ctx) error { return c.Text(200, "Success!") }, ) ``` ### Simulate Downstream Failures ```go theme={null} app.Get("/api/external", func(c *mizu.Ctx) error { // Middleware adds latency/errors before reaching here result, err := callExternalService() if err != nil { return c.JSON(500, map[string]string{"error": err.Error()}) } return c.JSON(200, result) }) ``` ## Selectors | Selector | Description | | ---------------------------- | -------------------------- | | `PathSelector(paths...)` | Match specific URL paths | | `MethodSelector(methods...)` | Match HTTP methods | | `HeaderSelector(header)` | Match requests with header | ## Controller Methods ```go theme={null} func NewController() *Controller func (c *Controller) Enable() func (c *Controller) Disable() func (c *Controller) IsEnabled() bool func (c *Controller) SetErrorRate(rate int) func (c *Controller) SetErrorCode(code int) func (c *Controller) SetLatency(min, max time.Duration) func (c *Controller) SetSelector(selector func(*mizu.Ctx) bool) func (c *Controller) Middleware() mizu.Middleware ``` ## API Reference ```go theme={null} func New() mizu.Middleware func WithOptions(opts Options) mizu.Middleware func Error(rate int, code int) mizu.Middleware func Latency(min, max time.Duration) mizu.Middleware // Selectors func PathSelector(paths ...string) func(*mizu.Ctx) bool func MethodSelector(methods ...string) func(*mizu.Ctx) bool func HeaderSelector(header string) func(*mizu.Ctx) bool ``` ## Safety Always protect chaos endpoints: ```go theme={null} // Restrict chaos control to admin admin := app.Group("/admin") admin.Use(basicauth.New(basicauth.Options{ Users: map[string]string{"admin": "secret"}, })) // Add chaos control routes to admin group admin.Post("/chaos/enable", enableChaosHandler) ``` ## Technical Details ### Architecture The chaos middleware implements failure injection through a middleware chain pattern, supporting both static and dynamic configuration: * **Static Configuration**: `WithOptions()`, `Error()`, and `Latency()` create middleware with fixed settings * **Dynamic Configuration**: `Controller` provides thread-safe runtime configuration changes ### Implementation Details **Random Number Generation**: * Uses `math/rand` instead of `crypto/rand` for performance (intentionally weak RNG) * Error injection: Generates random number 0-99 and compares against error rate percentage * Latency injection: Calculates random duration between min and max using `rand.Int63n()` **Latency Calculation**: ``` latency = LatencyMin + rand.Int63n(LatencyMax - LatencyMin) ``` **Request Flow**: 1. Check if chaos is enabled 2. Apply selector filter (if configured) 3. Inject latency (if configured) 4. Inject error based on probability (if configured) 5. Pass to next handler (if no error injected) **Selector Functions**: * `PathSelector`: Creates a map of paths for O(1) lookup * `MethodSelector`: Creates a map of HTTP methods for O(1) lookup * `HeaderSelector`: Checks header presence using standard library **Controller Thread Safety**: * Options are read atomically during request processing * Enable/Disable uses boolean flag * Configuration methods update fields directly (suitable for controlled admin access) ### Performance Considerations * Selector maps provide constant-time path/method matching * No mutex locking on hot path for static configuration * `time.Sleep()` blocks the handler goroutine (intentional for testing) * Random number generation is non-cryptographic for speed ## Best Practices * Never enable in production without safeguards * Use selectors to limit scope * Start with low error rates * Monitor during chaos testing * Use header-triggered chaos for CI/CD tests * Implement circuit breakers in your clients ## Testing The chaos middleware includes comprehensive test coverage for all features: | Test Case | Description | Expected Behavior | | ----------------------------- | ----------------------------------- | ------------------------------------------- | | `TestNew` | Default middleware creation | Returns 200 OK (chaos disabled by default) | | `TestError` | Error injection with 100% rate | Returns 503 Service Unavailable | | `TestLatency` | Latency injection (50-100ms) | Request takes >= 50ms to complete | | `TestWithOptions_Disabled` | Disabled chaos with 100% error rate | Returns 200 OK (no error injection) | | `TestWithOptions_Selector` | Custom selector for specific path | Injects error on `/chaos`, allows `/normal` | | `TestController` | Controller enable/disable | Correctly toggles enabled state | | `TestController_Middleware` | Controller middleware integration | Injects errors only when enabled | | `TestController_SetErrorCode` | Custom error code via controller | Returns 502 Bad Gateway | | `TestPathSelector` | Path-based selector | Matches `/api/chaos`, ignores `/api/normal` | | `TestHeaderSelector` | Header presence selector | Selects requests with `X-Chaos` header | | `TestMethodSelector` | HTTP method selector | Matches POST, ignores GET | ## Related Middlewares * [timeout](/middlewares/timeout) - Request timeout * [circuitbreaker](/middlewares/circuitbreaker) - Circuit breaker * [recover](/middlewares/recover) - Panic recovery # Circuit Breaker Source: https://docs.go-mizu.dev/middlewares/circuitbreaker Circuit breaker pattern middleware for fault tolerance and resilience. ## Overview The `circuitbreaker` middleware implements the circuit breaker pattern to prevent cascading failures. When errors exceed a threshold, it "opens" the circuit, immediately failing requests without calling the handler, giving the system time to recover. Use it when you need: * Protection against cascading failures * Graceful degradation * System resilience * External service call protection ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/circuitbreaker" ``` ## Quick Start ```go theme={null} app := mizu.New() // Default settings: 5 failures opens circuit for 30s app.Use(circuitbreaker.New()) ``` ## Configuration ### Options | Option | Type | Default | Description | | --------------- | ----------------------- | --------- | ----------------------------- | | `Threshold` | `int` | `5` | Failures before opening | | `Timeout` | `time.Duration` | `30s` | Time before half-open | | `MaxRequests` | `int` | `1` | Requests allowed in half-open | | `OnStateChange` | `func(from, to State)` | - | State change callback | | `IsFailure` | `func(error) bool` | Any error | Failure detection | | `ErrorHandler` | `func(*mizu.Ctx) error` | - | Open circuit handler | ### States ```go theme={null} const ( StateClosed // Normal operation, requests pass through StateOpen // Circuit tripped, requests fail immediately StateHalfOpen // Testing recovery, limited requests allowed ) ``` ## Examples ### Basic Circuit Breaker ```go theme={null} // Opens after 5 failures, resets after 30s app.Use(circuitbreaker.New()) ``` ### Custom Threshold ```go theme={null} app.Use(circuitbreaker.WithOptions(circuitbreaker.Options{ Threshold: 10, // Open after 10 failures Timeout: time.Minute, // Stay open for 1 minute })) ``` ### Custom Failure Detection ```go theme={null} app.Use(circuitbreaker.WithOptions(circuitbreaker.Options{ Threshold: 5, IsFailure: func(err error) bool { // Only count specific errors as failures if err == nil { return false } // Don't count client errors var httpErr *HTTPError if errors.As(err, &httpErr) && httpErr.Code < 500 { return false } return true }, })) ``` ### State Change Monitoring ```go theme={null} app.Use(circuitbreaker.WithOptions(circuitbreaker.Options{ Threshold: 5, OnStateChange: func(from, to circuitbreaker.State) { log.Printf("Circuit breaker: %s -> %s", from, to) if to == circuitbreaker.StateOpen { // Alert operations team alertOps("Circuit breaker opened!") } }, })) ``` ### Custom Error Response ```go theme={null} app.Use(circuitbreaker.WithOptions(circuitbreaker.Options{ Threshold: 5, ErrorHandler: func(c *mizu.Ctx) error { return c.JSON(503, map[string]string{ "error": "Service temporarily unavailable", "message": "Please try again in a few moments", }) }, })) ``` ### Per-Route Circuit Breakers ```go theme={null} // Separate circuit breakers for different services externalAPI := circuitbreaker.WithOptions(circuitbreaker.Options{ Threshold: 3, Timeout: time.Minute, }) database := circuitbreaker.WithOptions(circuitbreaker.Options{ Threshold: 5, Timeout: 30 * time.Second, }) app.Get("/api/external", externalHandler, externalAPI) app.Get("/api/data", dataHandler, database) ``` ### Recovery Testing ```go theme={null} app.Use(circuitbreaker.WithOptions(circuitbreaker.Options{ Threshold: 5, Timeout: 30 * time.Second, MaxRequests: 3, // Allow 3 test requests in half-open state })) ``` ### With Metrics ```go theme={null} var ( circuitOpens = prometheus.NewCounter(...) circuitCloses = prometheus.NewCounter(...) ) app.Use(circuitbreaker.WithOptions(circuitbreaker.Options{ Threshold: 5, OnStateChange: func(from, to circuitbreaker.State) { switch to { case circuitbreaker.StateOpen: circuitOpens.Inc() case circuitbreaker.StateClosed: circuitCloses.Inc() } }, })) ``` ### Fallback Response ```go theme={null} func withFallback(fallback func(*mizu.Ctx) error) mizu.Middleware { cb := circuitbreaker.WithOptions(circuitbreaker.Options{ Threshold: 5, ErrorHandler: func(c *mizu.Ctx) error { return fallback(c) }, }) return cb } // Use cached data when circuit opens app.Get("/api/data", dataHandler, withFallback(func(c *mizu.Ctx) error { cached := cache.Get("api_data") return c.JSON(200, map[string]any{ "data": cached, "cached": true, }) })) ``` ## State Machine ``` ┌─────────────┐ success │ │ failure >= threshold ┌──────────────│ Closed │──────────────┐ │ │ │ │ │ └─────────────┘ ▼ │ ▲ ┌─────────────┐ │ │ │ │ │ success│ │ Open │ │ (all) │ │ │ │ │ └──────┬──────┘ │ ┌─────┴─────┐ │ │ │ │ │ timeout │ │ Half-Open │◄───────────────┘ │ │ │ │ └─────┬─────┘ │ │ │ │ failure └─────────────────────┼─────────────────────► │ ▼ Open ``` ## API Reference ### Functions ```go theme={null} // New creates circuit breaker with defaults func New() mizu.Middleware // WithOptions creates circuit breaker with custom options func WithOptions(opts Options) mizu.Middleware ``` ### State Type ```go theme={null} type State int func (s State) String() string // Returns "closed", "open", or "half-open" ``` ## Technical Details ### State Machine Implementation The circuit breaker implements a thread-safe state machine with three states: * **Closed State**: Normal operation where all requests are allowed through. Failures are counted, and when the failure count reaches the `Threshold`, the circuit transitions to Open. * **Open State**: Protective state where all requests are immediately rejected without calling the handler. After the `Timeout` period, the circuit automatically transitions to Half-Open. * **Half-Open State**: Recovery testing state where a limited number of requests (controlled by `MaxRequests`) are allowed through. If all test requests succeed, the circuit closes. If any request fails, the circuit immediately reopens. ### Threshold Mechanism The failure threshold works differently in each state: * **In Closed State**: Consecutive failures are counted. Once the count reaches `Threshold`, the circuit opens. Successful requests reset the failure counter to zero. * **In Open State**: No requests are processed, so failures are not counted. The circuit waits for the `Timeout` period before transitioning to Half-Open. * **In Half-Open State**: Successes are counted. The circuit requires `MaxRequests` consecutive successes to close. A single failure immediately reopens the circuit. ### Thread Safety The circuit breaker uses a mutex (`sync.Mutex`) to protect concurrent access to: * Current state * Failure and success counters * Last failure timestamp All state transitions and counter updates are atomic operations protected by the mutex. ### Failure Detection The `IsFailure` function determines what constitutes a failure: * Default behavior: Any non-nil error is considered a failure * Custom behavior: Can be configured to filter specific errors (e.g., ignore 4xx client errors, only count 5xx server errors) ## Best Practices * Set thresholds based on normal error rates * Use meaningful timeout values * Monitor state changes for alerting * Consider different breakers for different dependencies * Implement fallback responses when possible ## Testing The circuit breaker middleware includes comprehensive test coverage: | Test Case | Description | Expected Behavior | | ------------------------------- | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------- | | `TestNew` | Default circuit breaker with 5 failure threshold | After 5 consecutive failures, circuit opens and blocks subsequent requests with 503 status | | `TestWithOptions_Threshold` | Custom threshold of 3 failures | Circuit opens after exactly 3 failures instead of default 5 | | `TestWithOptions_Timeout` | Custom timeout of 100ms | After timeout period, circuit transitions from Open to Half-Open and allows test requests | | `TestWithOptions_OnStateChange` | State transition callback | Callback is invoked with correct from/to states on each transition (e.g., closed->open, open->half-open) | | `TestWithOptions_ErrorHandler` | Custom error handler for open circuit | Custom error handler is invoked with proper JSON response when circuit is open | | `TestWithOptions_IsFailure` | Custom failure detection logic | Only errors matching custom criteria count toward threshold; other errors are ignored | | `TestState_String` | State string representation | Each state (Closed, Open, Half-Open, Unknown) returns correct string representation | ## Related Middlewares * [ratelimit](/middlewares/ratelimit) - Rate limiting * [timeout](/middlewares/timeout) - Request timeout * [recover](/middlewares/recover) - Panic recovery # Compress Source: https://docs.go-mizu.dev/middlewares/compress Response compression middleware supporting gzip and deflate encoding. ## Overview The `compress` middleware automatically compresses HTTP responses using gzip or deflate encoding when the client supports it. This reduces bandwidth usage and improves load times for text-based content. Use it when you need: * Reduced bandwidth for API responses * Faster page loads for web applications * Compression for JSON, HTML, CSS, and JavaScript ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/compress" ``` ## Quick Start ```go theme={null} app := mizu.New() // Enable gzip compression app.Use(compress.Gzip()) // Or enable deflate app.Use(compress.Deflate()) ``` ## Configuration ### Options | Option | Type | Default | Description | | -------------- | ---------- | ---------- | --------------------------------- | | `Level` | `int` | `6` | Compression level (1-9) | | `MinSize` | `int` | `1024` | Minimum response size to compress | | `ContentTypes` | `[]string` | Text types | Content types to compress | ## Examples ### Gzip Compression ```go theme={null} app.Use(compress.Gzip()) ``` ### Custom Compression Level ```go theme={null} // Best compression (slower) app.Use(compress.GzipLevel(9)) // Fastest compression app.Use(compress.GzipLevel(1)) ``` ### With Options ```go theme={null} app.Use(compress.New(compress.Options{ MinSize: 1000, // Only compress responses > 1KB ContentTypes: []string{ "text/plain", "text/html", "text/css", "application/json", "application/javascript", }, })) ``` ### Auto-Select Encoding ```go theme={null} // Automatically selects gzip or deflate based on Accept-Encoding app.Use(compress.New(compress.Options{ MinSize: 512, })) ``` ## API Reference ### Functions ```go theme={null} // Gzip creates gzip compression middleware func Gzip() mizu.Middleware // GzipLevel creates gzip middleware with specific level func GzipLevel(level int) mizu.Middleware // Deflate creates deflate compression middleware func Deflate() mizu.Middleware // New creates compression middleware with options func New(opts Options) mizu.Middleware ``` ### Compression Levels | Level | Description | | ----- | ----------------------------- | | 1 | Best speed, least compression | | 6 | Default balance | | 9 | Best compression, slowest | ## Behavior * Automatically sets `Content-Encoding` header * Adds `Vary: Accept-Encoding` header * Skips compression if client doesn't support it * Skips already-encoded responses * Skips responses smaller than `MinSize` ## Technical Details ### Implementation Overview The compress middleware uses a buffering strategy to determine whether compression should be applied: 1. **Buffering Phase**: Responses are buffered until they reach the `MinSize` threshold 2. **Decision Point**: Once the threshold is reached, the middleware checks: * If the client supports the requested encoding (via `Accept-Encoding` header) * If the content type is compressible (matches `ContentTypes` list) * If the response is not already encoded (no existing `Content-Encoding` header) 3. **Compression Phase**: If all checks pass, the appropriate compression writer is applied ### Writer Pooling The middleware uses `sync.Pool` to efficiently reuse compression writers: * **Gzip Pool**: Maintains a pool of `gzip.Writer` instances * **Deflate Pool**: Maintains a pool of `flate.Writer` instances * Writers are reset and returned to the pool after each request * This reduces garbage collection pressure and improves performance ### Custom Response Writer The `compressWriter` wraps the standard `http.ResponseWriter` and provides: * **Buffering**: Accumulates response data until compression decision is made * **Lazy Header Writing**: Delays header writing until compression status is determined * **Automatic Cleanup**: Ensures compression writers are properly closed and returned to pool * **Flush Support**: Implements `http.Flusher` for streaming responses ### Encoding Selection When using `New()` with auto-selection: 1. Checks for `gzip` in `Accept-Encoding` header first (preferred) 2. Falls back to `deflate` if gzip is not supported 3. Uses no compression if neither is supported When using specific functions (`Gzip()`, `Deflate()`): * Only applies compression if the client explicitly supports the selected encoding ### Default Content Types The following content types are compressed by default: * `text/html`, `text/css`, `text/plain`, `text/javascript`, `text/xml` * `application/json`, `application/javascript`, `application/xml` * `application/xhtml+xml`, `application/rss+xml`, `application/atom+xml` * `image/svg+xml` ## Best Practices * Use level 6 for most applications (good balance) * Set appropriate `MinSize` to avoid compressing tiny responses * Only compress text-based content types * Place before other response-modifying middleware ## Testing The compress middleware includes comprehensive test coverage for various scenarios: | Test Case | Description | Expected Behavior | | ---------------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | | `TestGzip/compresses response` | Tests basic gzip compression with large response | Response is compressed with gzip, Content-Encoding header set to "gzip", body can be decompressed correctly | | `TestGzip/skips without accept-encoding` | Tests behavior when Accept-Encoding header is missing | Response is not compressed, no Content-Encoding header set | | `TestGzip/sets vary header` | Tests Vary header is properly set | Vary header contains "Accept-Encoding" | | `TestGzipLevel` | Tests gzip compression with specific level (level 9) | Response is compressed with specified compression level, Content-Encoding set to "gzip" | | `TestDeflate` | Tests deflate compression algorithm | Response is compressed with deflate, Content-Encoding header set to "deflate" | | `TestNew_SmallResponse` | Tests MinSize threshold with response smaller than limit | Small responses (\< MinSize) are not compressed | | `TestNew_NonCompressibleType` | Tests content type filtering | Non-text content types (e.g., application/octet-stream) are not compressed | | `TestNew_AlreadyEncoded` | Tests handling of pre-encoded responses | Existing Content-Encoding header is preserved, no double compression | | `TestNew_AutoSelectEncoding` | Tests automatic encoding selection based on Accept-Encoding | Middleware selects gzip or deflate based on client preference, prefers gzip when both available | ## Related Middlewares * [cache](/middlewares/cache) - HTTP caching * [etag](/middlewares/etag) - ETag generation * [vary](/middlewares/vary) - Vary header management # Concurrency Source: https://docs.go-mizu.dev/middlewares/concurrency Concurrency limiting middleware for controlling parallel request processing. ## Overview The `concurrency` middleware limits the number of requests processed in parallel, preventing resource exhaustion and ensuring stable performance. Use it when you need: * Limit CPU-intensive operations * Control database connection usage * Prevent memory exhaustion ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/concurrency" ``` ## Quick Start ```go theme={null} app := mizu.New() // Max 50 concurrent requests app.Use(concurrency.New(50)) ``` ## Configuration ### Options | Option | Type | Default | Description | | -------------- | ----------------------- | -------- | -------------------- | | `Limit` | `int` | Required | Max concurrent | | `ErrorHandler` | `func(*mizu.Ctx) error` | - | Custom error handler | ## Examples ### Basic Limit ```go theme={null} app.Use(concurrency.New(50)) ``` ### Custom Error ```go theme={null} app.Use(concurrency.WithOptions(concurrency.Options{ Limit: 50, ErrorHandler: func(c *mizu.Ctx) error { return c.JSON(503, map[string]string{ "error": "Too many concurrent requests", }) }, })) ``` ### Per-Route Limits ```go theme={null} // Heavy endpoints heavy := app.Group("/heavy") heavy.Use(concurrency.New(5)) // Light endpoints light := app.Group("/api") light.Use(concurrency.New(100)) ``` ## API Reference ### Functions ```go theme={null} // New creates concurrency limiter func New(limit int) mizu.Middleware // WithOptions creates with configuration func WithOptions(opts Options) mizu.Middleware ``` ## Technical Details The concurrency middleware uses Go's buffered channels as semaphores to control concurrent request processing: ### Implementation Mechanisms 1. **Semaphore Pattern**: Uses a buffered channel with capacity equal to the maximum concurrent requests limit 2. **Non-blocking Acquisition**: The default `New()` and `WithOptions()` use `select` with `default` to immediately reject requests when at capacity 3. **Blocking Acquisition**: The `Blocking()` variant blocks requests until a slot becomes available 4. **Context-aware**: The `WithContext()` variant respects context cancellation while waiting for a slot ### Core Components * **Options struct**: Configures maximum concurrent requests and custom error handling * **Semaphore channel**: Controls access to the handler based on the configured limit * **Deferred release**: Ensures semaphore slots are released even if the handler panics * **Retry-After header**: Automatically set to "1" second when requests are rejected ### Behavior Patterns * **Zero or negative limit**: Immediately rejects all requests with 503 Service Unavailable * **Default error response**: Returns 503 with "Server at capacity" message * **Custom error handler**: Allows full control over rejection response format and status code ## Best Practices * Set limits based on resource capacity * Monitor concurrent request counts * Use different limits for different workloads * Combine with timeout for hung requests ## Testing | Test Case | Description | Expected Behavior | | ---------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------ | | `TestNew` | Creates middleware with limit of 2 and sends 5 concurrent requests | Max concurrent requests never exceeds the configured limit of 2 | | `TestNew_RejectsOverCapacity` | Sends 3 concurrent requests with limit of 1 | Some requests are rejected with 503 Service Unavailable status | | `TestWithOptions_ErrorHandler` | Uses custom error handler with Max: 0 | Custom error handler is invoked, returns 429 with JSON error message | | `TestBlocking` | Uses Blocking variant with limit of 2 and 5 concurrent requests | All requests eventually succeed (200 OK), max concurrent never exceeds 2 | | `TestRetryAfterHeader` | Sends request with Max: 0 (immediate rejection) | Response includes "Retry-After: 1" header | | `TestWithContext` | Uses WithContext variant with limit of 2 and 5 concurrent requests | Max concurrent requests never exceeds limit of 2 | | `TestWithContext_ContextCancellation` | Tests context cancellation with WithContext variant | First request blocks, completes when channel is closed | | `TestWithOptions_NegativeMax` | Configures middleware with Max: -1 | All requests rejected with 503 Service Unavailable | | `TestWithOptions_ErrorHandlerAtCapacity` | Custom error handler with Max: 1, sends 2 concurrent requests | Second request triggers custom error handler, returns 429 status | ## Related Middlewares * [bulkhead](/middlewares/bulkhead) - Bulkhead pattern * [ratelimit](/middlewares/ratelimit) - Rate limiting * [timeout](/middlewares/timeout) - Request timeout # Conditional Source: https://docs.go-mizu.dev/middlewares/conditional Conditional middleware execution based on request properties. ## Overview The `conditional` middleware enables conditional execution of other middleware based on request properties. Use it when you need: * Skip middleware for certain routes * Apply middleware conditionally * Dynamic middleware chains ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/conditional" ``` ## Quick Start ```go theme={null} app := mizu.New() // Only apply auth for /api routes app.Use(conditional.If( func(c *mizu.Ctx) bool { return strings.HasPrefix(c.Request().URL.Path, "/api") }, authMiddleware, )) ``` ## Examples ### Path-Based ```go theme={null} // Skip for health checks app.Use(conditional.Unless( func(c *mizu.Ctx) bool { return c.Request().URL.Path == "/health" }, rateLimitMiddleware, )) ``` ### Method-Based ```go theme={null} // Only for mutations app.Use(conditional.If( func(c *mizu.Ctx) bool { m := c.Request().Method return m == "POST" || m == "PUT" || m == "DELETE" }, csrfMiddleware, )) ``` ### Header-Based ```go theme={null} // Only for API requests app.Use(conditional.If( func(c *mizu.Ctx) bool { return c.Request().Header.Get("Accept") == "application/json" }, apiMiddleware, )) ``` ### Combine Conditions ```go theme={null} app.Use(conditional.If( conditional.And( conditional.PathPrefix("/api"), conditional.Not(conditional.Path("/api/public")), ), authMiddleware, )) ``` ## API Reference ### Functions ```go theme={null} // If applies middleware when condition is true func If(cond func(*mizu.Ctx) bool, m mizu.Middleware) mizu.Middleware // Unless applies middleware when condition is false func Unless(cond func(*mizu.Ctx) bool, m mizu.Middleware) mizu.Middleware // Condition helpers func Path(path string) func(*mizu.Ctx) bool func PathPrefix(prefix string) func(*mizu.Ctx) bool func Method(methods ...string) func(*mizu.Ctx) bool func Header(key, value string) func(*mizu.Ctx) bool func And(conditions ...func(*mizu.Ctx) bool) func(*mizu.Ctx) bool func Or(conditions ...func(*mizu.Ctx) bool) func(*mizu.Ctx) bool func Not(cond func(*mizu.Ctx) bool) func(*mizu.Ctx) bool ``` ## Technical Details ### HTTP Conditional Request Processing The conditional middleware implements HTTP conditional request handling as per RFC 7232. It provides efficient caching mechanisms through ETags and Last-Modified headers. **Core Components:** 1. **Response Capture**: The middleware intercepts the response using a custom `responseCapture` writer that buffers both the status code and body content before sending it to the client. 2. **ETag Generation**: By default, ETags are generated using MD5 hashing of the response body. The middleware supports both strong and weak ETags: * Strong ETag: `"hash-value"` * Weak ETag: `W/"hash-value"` 3. **Last-Modified Handling**: When a `ModTimeFunc` is provided, the middleware sets the Last-Modified header and processes If-Modified-Since conditional requests. **Request Flow:** 1. Check if request method is GET or HEAD (only these methods support conditional requests) 2. Capture the response body and status code 3. Generate ETag from response body (if enabled) 4. Set Last-Modified header (if configured) 5. Check If-None-Match header against ETag 6. Check If-Modified-Since header against modification time 7. Return 304 Not Modified if conditions match, or send full response **Performance Considerations:** * The middleware buffers the entire response body for ETag generation * MD5 hashing is used for ETag (not for security, just content fingerprinting) * Only processes 2xx status codes to avoid caching errors * Skips processing for non-GET/HEAD methods ## Security Considerations While MD5 is used for ETag generation, it's important to note that this is **not** a security concern. ETags are content fingerprints for caching purposes, not cryptographic signatures. The MD5 algorithm is perfectly suitable for this use case. ## Best Practices * Use clear condition names * Keep conditions simple * Document conditional behavior * Test edge cases * Consider response size when buffering for ETag generation * Use weak ETags for frequently changing content * Combine ETag and Last-Modified for optimal caching * Provide custom ETagFunc for better performance with large responses ## Testing The conditional middleware includes comprehensive test coverage for various scenarios: | Test Case | Description | Expected Behavior | | ----------------------------- | --------------------------------------------------------- | ------------------------------------------ | | TestNew | Basic middleware initialization with defaults | ETag header should be present in response | | TestIfNoneMatch\_NotModified | Request with matching If-None-Match header | Returns 304 Not Modified status | | TestIfNoneMatch\_Modified | Request with non-matching If-None-Match (content changed) | Returns 200 OK with new content | | TestWithOptions\_WeakETag | Weak ETag generation enabled | ETag header starts with "W/" prefix | | TestWithOptions\_LastModified | Last-Modified header with custom ModTimeFunc | Last-Modified header present in response | | TestIfModifiedSince | Request with If-Modified-Since matching modification time | Returns 304 Not Modified status | | TestSkipNonGET | POST request processing | No ETag header for non-GET/HEAD methods | | TestETagOnly | ETag-only mode without Last-Modified | Only ETag header present | | TestLastModifiedOnly | Last-Modified only mode without ETag | Only Last-Modified header present | | TestWithModTime | Combined ETag and Last-Modified | Both headers present in response | | TestCustomETagFunc | Custom ETag generation function | Uses custom ETag value instead of MD5 hash | ## Related Middlewares * [filter](/middlewares/filter) - Request filtering * [feature](/middlewares/feature) - Feature flags # Content Type Source: https://docs.go-mizu.dev/middlewares/contenttype Content-Type validation and enforcement middleware. ## Overview The `contenttype` middleware validates and enforces Content-Type headers on requests. It ensures requests have the expected content type and can set default content types for responses. Use it when you need: * JSON-only API endpoints * Form submission validation * Content-Type enforcement * Default response types ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/contenttype" ``` ## Quick Start ```go theme={null} app := mizu.New() // Require JSON for all POST/PUT/PATCH requests app.Use(contenttype.RequireJSON()) ``` ## Functions ### Request Validation | Function | Description | | ------------------- | ------------------------------ | | `Require(types...)` | Require specific content types | | `RequireJSON()` | Require `application/json` | | `RequireForm()` | Require form content types | | `Default(type)` | Set default if missing | ### Response Headers | Function | Description | | ------------------- | ------------------------- | | `SetResponse(type)` | Set response Content-Type | ## Examples ### Require JSON ```go theme={null} // Only accept JSON requests app.Use(contenttype.RequireJSON()) app.Post("/api/users", func(c *mizu.Ctx) error { // Guaranteed to receive JSON var user User if err := c.BindJSON(&user); err != nil { return err } return c.JSON(201, user) }) ``` ### Require Form Data ```go theme={null} // Only accept form submissions app.Post("/contact", contactHandler, contenttype.RequireForm()) ``` ### Multiple Content Types ```go theme={null} // Accept either JSON or XML app.Use(contenttype.Require("application/json", "application/xml")) ``` ### Set Default Content-Type ```go theme={null} // Set default for requests without Content-Type app.Use(contenttype.Default("application/json")) ``` ### Set Response Content-Type ```go theme={null} // All responses are JSON app.Use(contenttype.SetResponse("application/json; charset=utf-8")) ``` ### Per-Route Configuration ```go theme={null} // API routes: JSON only api := app.Group("/api") api.Use(contenttype.RequireJSON()) // Form routes: Form data only forms := app.Group("/forms") forms.Use(contenttype.RequireForm()) ``` ### Combined Request/Response ```go theme={null} // Require JSON input, set JSON output app.Use(contenttype.RequireJSON()) app.Use(contenttype.SetResponse("application/json; charset=utf-8")) ``` ## API Reference ### Functions ```go theme={null} // Require specific content types func Require(types ...string) mizu.Middleware // RequireJSON requires application/json func RequireJSON() mizu.Middleware // RequireForm requires form content types func RequireForm() mizu.Middleware // Default sets default Content-Type if not present func Default(contentType string) mizu.Middleware // SetResponse sets response Content-Type func SetResponse(contentType string) mizu.Middleware ``` ## Behavior * Only validates POST, PUT, PATCH requests (methods with bodies) * Compares media type only, ignores parameters (charset, boundary) * Returns 415 Unsupported Media Type on validation failure ## Common Content Types | Type | Description | | ----------------------------------- | ---------------- | | `application/json` | JSON data | | `application/xml` | XML data | | `application/x-www-form-urlencoded` | URL-encoded form | | `multipart/form-data` | File uploads | | `text/plain` | Plain text | ## Technical Details ### Implementation Overview The contenttype middleware provides Content-Type validation and enforcement through a set of middleware functions that operate on request and response headers. #### Request Validation (`Require`, `RequireJSON`, `RequireForm`) The validation middleware follows these steps: 1. **Method Filtering**: Only validates requests with body content (POST, PUT, PATCH). GET, DELETE, and other methods bypass validation. 2. **Header Extraction**: Retrieves the Content-Type header from the incoming request. 3. **Media Type Parsing**: Extracts the base media type by: * Finding the semicolon separator (if present) * Stripping parameters (charset, boundary, etc.) * Trimming whitespace from the result 4. **Case-Insensitive Matching**: Compares the extracted media type against allowed types using case-insensitive comparison. 5. **Error Response**: Returns HTTP 415 (Unsupported Media Type) if: * Content-Type header is missing (for methods that require it) * The media type doesn't match any allowed types #### Default Content-Type (`Default`) Sets a default Content-Type header on the request if none is present. This middleware: * Checks if the Content-Type header exists * Sets the header to the specified default value if missing * Preserves existing headers without modification #### Response Content-Type (`SetResponse`) Sets the Content-Type header on the response. This middleware: * Unconditionally sets the response Content-Type header * Executes before the handler processes the request * Can be combined with request validation middleware ### Key Design Decisions * **Parameter Stripping**: Media type parameters are ignored during validation, allowing `application/json; charset=utf-8` to match `application/json` * **Method-Specific**: Only methods with request bodies are validated * **Fail-Fast**: Validation happens before the handler executes * **Composable**: Multiple middleware can be chained for complex scenarios ## Best Practices * Use `RequireJSON()` for REST APIs * Use `RequireForm()` for traditional forms * Set response Content-Type for consistency * Combine `Default()` with `Require()` to provide fallback behavior * Apply validation at the router group level for route-specific rules ## Testing The contenttype middleware includes comprehensive test coverage for all functions and edge cases. ### Test Cases | Test Case | Description | Expected Behavior | | ---------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------ | | **Require Middleware** | | | | Allows matching content type | POST request with `application/json` header | Request proceeds (200 OK) | | Allows with charset | POST request with `application/json; charset=utf-8` | Request proceeds (200 OK), parameters ignored | | Rejects wrong content type | POST request with `application/xml` header (not in allowed list) | Returns 415 Unsupported Media Type | | Rejects missing content type | POST request without Content-Type header | Returns 415 Unsupported Media Type | | Skips GET requests | GET request without Content-Type header | Request proceeds (200 OK), validation skipped | | **RequireJSON Middleware** | | | | Allows JSON | POST request with `application/json` header | Request proceeds (200 OK) | | Rejects form | POST request with `application/x-www-form-urlencoded` header | Returns 415 Unsupported Media Type | | **RequireForm Middleware** | | | | Allows form-urlencoded | POST request with `application/x-www-form-urlencoded` header | Request proceeds (200 OK) | | Allows multipart | POST request with `multipart/form-data; boundary=----` header | Request proceeds (200 OK) | | **Default Middleware** | | | | Sets default when missing | POST request without Content-Type header | Request header set to `application/json` | | Preserves existing | POST request with `text/plain` header | Existing header preserved as `text/plain` | | **SetResponse Middleware** | | | | Sets response Content-Type | GET request to endpoint with SetResponse middleware | Response includes `Content-Type: application/json; charset=utf-8` header | ## Related Middlewares * [bodylimit](/middlewares/bodylimit) - Body size limits * [validator](/middlewares/validator) - Request validation # CORS Source: https://docs.go-mizu.dev/middlewares/cors Cross-Origin Resource Sharing middleware for handling browser cross-origin requests. ## Overview The `cors` middleware handles Cross-Origin Resource Sharing (CORS), allowing your API to be safely accessed from different domains. It automatically handles preflight requests and sets the appropriate headers. Use it when you need: * API access from browser applications on different domains * Single-page applications calling your backend * Mobile apps using web views ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/cors" ``` ## Quick Start ```go theme={null} app := mizu.New() // Allow all origins (development only) app.Use(cors.AllowAll()) // Or allow specific origins app.Use(cors.WithOrigins("https://example.com", "https://app.example.com")) ``` ## Configuration ### Options | Option | Type | Default | Description | | --------------------- | ------------------- | -------------------------------------- | ----------------------------- | | `AllowOrigins` | `[]string` | `["*"]` | Allowed origins | | `AllowMethods` | `[]string` | `["GET", "POST", "HEAD"]` | Allowed HTTP methods | | `AllowHeaders` | `[]string` | `["Origin", "Content-Type", "Accept"]` | Allowed request headers | | `ExposeHeaders` | `[]string` | `[]` | Headers exposed to browser | | `AllowCredentials` | `bool` | `false` | Allow credentials (cookies) | | `MaxAge` | `time.Duration` | `0` | Preflight cache duration | | `AllowOriginFunc` | `func(string) bool` | `nil` | Custom origin validator | | `AllowPrivateNetwork` | `bool` | `false` | Enable Private Network Access | ## Examples ### Allow All Origins (Development) ```go theme={null} app.Use(cors.AllowAll()) ``` ### Specific Origins ```go theme={null} app.Use(cors.New(cors.Options{ AllowOrigins: []string{ "https://example.com", "https://admin.example.com", }, })) ``` ### With Credentials ```go theme={null} app.Use(cors.New(cors.Options{ AllowOrigins: []string{"https://app.example.com"}, AllowCredentials: true, AllowMethods: []string{"GET", "POST", "PUT", "DELETE"}, AllowHeaders: []string{"Authorization", "Content-Type"}, })) ``` ### Custom Origin Validation ```go theme={null} app.Use(cors.New(cors.Options{ AllowOriginFunc: func(origin string) bool { // Allow all subdomains of example.com return strings.HasSuffix(origin, ".example.com") }, })) ``` ### Preflight Caching ```go theme={null} app.Use(cors.New(cors.Options{ AllowOrigins: []string{"https://example.com"}, MaxAge: 12 * time.Hour, // Cache preflight for 12 hours })) ``` ## API Reference ### Functions ```go theme={null} // New creates CORS middleware with options func New(opts Options) mizu.Middleware // AllowAll creates permissive CORS (development only) func AllowAll() mizu.Middleware // WithOrigins creates CORS allowing specific origins func WithOrigins(origins ...string) mizu.Middleware ``` ## Technical Details ### Implementation Overview The CORS middleware implements the W3C Cross-Origin Resource Sharing specification by: 1. **Origin Validation**: Checks incoming requests against allowed origins using either: * Static origin list (`AllowOrigins`) * Custom validation function (`AllowOriginFunc`) * Wildcard matching for development scenarios 2. **Header Management**: Sets appropriate CORS headers based on configuration: * `Access-Control-Allow-Origin`: The allowed origin (specific or wildcard) * `Access-Control-Allow-Methods`: Permitted HTTP methods * `Access-Control-Allow-Headers`: Permitted request headers * `Access-Control-Expose-Headers`: Headers exposed to the client * `Access-Control-Allow-Credentials`: Credential support flag * `Access-Control-Max-Age`: Preflight cache duration in seconds * `Vary: Origin`: Added when using specific origins to ensure proper caching 3. **Preflight Handling**: Automatically responds to OPTIONS requests with: * No content (204 status) * All necessary CORS headers * Optional Private Network Access support ### Default Values When options are not specified, the middleware uses these defaults: * **AllowOrigins**: `["*"]` (all origins) * **AllowMethods**: `["GET", "POST", "HEAD"]` * **AllowHeaders**: `["Origin", "Content-Type", "Accept"]` * **ExposeHeaders**: `[]` (empty) * **AllowCredentials**: `false` * **MaxAge**: `0` (no caching) ### Request Flow 1. Extract `Origin` header from request 2. If no origin header, skip CORS processing 3. Validate origin against allowed list or function 4. If origin not allowed, continue without CORS headers 5. Set `Access-Control-Allow-Origin` header (wildcard or specific) 6. Add `Vary: Origin` header when using specific origins 7. Set credentials header if enabled 8. Set expose headers if configured 9. For OPTIONS requests (preflight): * Set methods and headers * Set max-age if configured * Handle Private Network Access if enabled * Return 204 No Content 10. For regular requests, continue to next handler ## Security Considerations 1. **Never use `AllowAll()` in production** - It exposes your API to any origin 2. **Be careful with credentials** - When `AllowCredentials` is true, you cannot use wildcard origins 3. **Validate origins** - Use `AllowOriginFunc` for dynamic origin validation 4. **Limit exposed headers** - Only expose headers that clients actually need ## Best Practices * Use specific origins in production * Cache preflight requests with `MaxAge` to reduce OPTIONS requests * Combine with authentication middleware for protected endpoints * Use `AllowPrivateNetwork` only when needed for local network access ## Testing The CORS middleware includes comprehensive test coverage for all features and edge cases: | Test Case | Description | Expected Behavior | | ------------------------------------------------ | ----------------------------------------- | -------------------------------------------------------------------------------------- | | `TestNew/allows matching origin` | Request with allowed origin | Returns 200 OK with `Access-Control-Allow-Origin` header set to the matching origin | | `TestNew/ignores non-matching origin` | Request with non-allowed origin | Processes request normally but does not set CORS headers | | `TestNew/handles preflight` | OPTIONS request with origin | Returns 204 No Content with `Access-Control-Allow-Methods` and other preflight headers | | `TestNew/ignores requests without origin` | Request without Origin header | Processes request normally without setting any CORS headers | | `TestAllowAll` | AllowAll() with any origin | Sets `Access-Control-Allow-Origin: *` for any origin | | `TestWithOrigins/http://a.com` | WithOrigins() with allowed origin | Sets `Access-Control-Allow-Origin` to the specific allowed origin | | `TestWithOrigins/http://b.com` | WithOrigins() with another allowed origin | Sets `Access-Control-Allow-Origin` to the specific allowed origin | | `TestWithOrigins/http://c.com` | WithOrigins() with non-allowed origin | Does not set `Access-Control-Allow-Origin` header | | `TestNew_AllowCredentials` | Credentials enabled with specific origin | Sets `Access-Control-Allow-Credentials: true` and specific origin (not wildcard) | | `TestNew_ExposeHeaders` | Custom expose headers configured | Sets `Access-Control-Expose-Headers` with comma-separated header list | | `TestNew_MaxAge` | MaxAge set to 12 hours | Sets `Access-Control-Max-Age: 43200` on preflight requests | | `TestNew_AllowOriginFunc/http://example.com` | Custom origin validator (exact match) | Allows the origin when validation function returns true | | `TestNew_AllowOriginFunc/http://api.example.com` | Custom origin validator (subdomain) | Allows subdomain when validation function returns true | | `TestNew_AllowOriginFunc/http://other.com` | Custom origin validator (non-matching) | Denies origin when validation function returns false | | `TestNew_PrivateNetwork` | Private Network Access enabled | Sets `Access-Control-Allow-Private-Network: true` when requested | | `TestNew_VaryHeader` | Specific origin configured | Adds `Vary: Origin` header to ensure proper caching | ## Related Middlewares * [cors2](/middlewares/cors2) - Enhanced CORS with additional features * [secure](/middlewares/secure) - HTTPS enforcement * [helmet](/middlewares/helmet) - Security headers # CORS2 Source: https://docs.go-mizu.dev/middlewares/cors2 Enhanced CORS middleware with preflight caching and advanced options. ## Overview The `cors2` middleware provides enhanced Cross-Origin Resource Sharing handling with additional features like pattern-based origin matching, preflight response caching, and more granular control. Use it when you need: * Wildcard subdomain matching * Advanced preflight caching * More control over CORS behavior ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/cors2" ``` ## Quick Start ```go theme={null} app := mizu.New() // Allow all subdomains of example.com app.Use(cors2.New(cors2.Options{ AllowOriginPatterns: []string{"https://*.example.com"}, })) ``` ## Configuration ### Options | Option | Type | Default | Description | | --------------------- | --------------- | ---------------- | ----------------------- | | `AllowOrigins` | `[]string` | `["*"]` | Exact origins allowed | | `AllowOriginPatterns` | `[]string` | `[]` | Wildcard patterns | | `AllowMethods` | `[]string` | Standard methods | Allowed methods | | `AllowHeaders` | `[]string` | Standard headers | Allowed headers | | `ExposeHeaders` | `[]string` | `[]` | Exposed headers | | `AllowCredentials` | `bool` | `false` | Allow credentials | | `MaxAge` | `time.Duration` | `0` | Preflight cache time | | `AllowPrivateNetwork` | `bool` | `false` | Private network access | | `OptionsPassthrough` | `bool` | `false` | Pass OPTIONS to handler | ## Examples ### Wildcard Subdomains ```go theme={null} app.Use(cors2.New(cors2.Options{ AllowOriginPatterns: []string{ "https://*.example.com", "https://*.staging.example.com", }, })) ``` ### Combined Exact and Pattern ```go theme={null} app.Use(cors2.New(cors2.Options{ AllowOrigins: []string{"https://example.com"}, AllowOriginPatterns: []string{"https://*.example.com"}, })) ``` ### Full Configuration ```go theme={null} app.Use(cors2.New(cors2.Options{ AllowOrigins: []string{"https://app.example.com"}, AllowMethods: []string{"GET", "POST", "PUT", "DELETE"}, AllowHeaders: []string{"Authorization", "Content-Type", "X-Request-ID"}, ExposeHeaders: []string{"X-Total-Count", "X-Page-Count"}, AllowCredentials: true, MaxAge: 12 * time.Hour, AllowPrivateNetwork: false, })) ``` ## API Reference ### Functions ```go theme={null} // New creates CORS2 middleware with options func New(opts Options) mizu.Middleware ``` ## Pattern Syntax * `*` matches any sequence of characters * `https://*.example.com` matches `https://app.example.com`, `https://api.example.com` * Patterns are matched against the full origin including protocol ## Technical Details ### Implementation Overview The cors2 middleware implements a simplified CORS handling mechanism with the following key components: **Origin Matching:** * Supports wildcard (`*`) for all origins * Implements exact origin matching with case-insensitive comparison * The `matchOrigin` function handles both wildcard and exact match scenarios **Header Management:** * Sets `Access-Control-Allow-Origin` based on the request origin and configured options * Conditionally sets `Access-Control-Allow-Credentials` when credentials are enabled * Applies `Access-Control-Expose-Headers` for custom headers that should be exposed to the browser **Preflight Request Handling:** * Detects OPTIONS requests for preflight handling * Responds with `Access-Control-Allow-Methods` and `Access-Control-Allow-Headers` * Sets `Access-Control-Max-Age` to cache preflight responses (when configured) * Returns HTTP 204 (No Content) for preflight requests **Default Values:** * Origin: `*` (all origins) * Methods: `GET, POST, PUT, DELETE, OPTIONS` * Headers: `Content-Type, Authorization` * Credentials: `false` * MaxAge: `0` (no caching) ### Helper Functions The middleware provides convenience functions: * `New()`: Creates middleware with default settings * `WithOptions(opts)`: Creates middleware with custom configuration * `AllowOrigin(origin)`: Quick setup for a specific origin * `AllowAll()`: Permissive setup with extended methods and headers * `AllowCredentials(origin)`: Enables credentials for a specific origin ## Best Practices * Use exact origins when possible for security * Use patterns only for known subdomain structures * Set appropriate MaxAge to reduce preflight requests * Be cautious with credentials and wildcards ## Testing The cors2 middleware includes comprehensive test coverage for all functionality: | Test Case | Description | Expected Behavior | | ----------------------------------------------- | --------------------------------------- | ------------------------------------------------------------------------ | | `TestNew` | Default middleware with wildcard origin | Sets `Access-Control-Allow-Origin: *` for any request with Origin header | | `TestWithOptions_SpecificOrigin` (matching) | Request with matching origin | Returns the specific origin in `Access-Control-Allow-Origin` header | | `TestWithOptions_SpecificOrigin` (non-matching) | Request with non-matching origin | No `Access-Control-Allow-Origin` header is set | | `TestPreflight` | OPTIONS request (preflight) | Returns 204 No Content with Allow-Methods and Allow-Headers set | | `TestWithOptions_Credentials` | Middleware with credentials enabled | Sets `Access-Control-Allow-Credentials: true` | | `TestWithOptions_ExposeHeaders` | Custom expose headers configuration | Sets `Access-Control-Expose-Headers` with configured values | | `TestWithOptions_MaxAge` | MaxAge set to 1 hour | Sets `Access-Control-Max-Age: 3600` on OPTIONS requests | | `TestAllowOrigin` | Helper function for specific origin | Allows only the specified origin | | `TestAllowAll` | Helper function for permissive CORS | Sets wildcard origin and max-age header | | `TestAllowCredentials` | Helper function for credentials | Enables credentials for specific origin | ## Related Middlewares * [cors](/middlewares/cors) - Basic CORS handling * [secure](/middlewares/secure) - Security settings * [helmet](/middlewares/helmet) - Security headers # CSRF Source: https://docs.go-mizu.dev/middlewares/csrf Cross-Site Request Forgery protection middleware for secure form submissions. ## Overview The `csrf` middleware protects against Cross-Site Request Forgery attacks by generating and validating tokens. It ensures that form submissions originate from your application, not from malicious sites. Use it when you have: * HTML forms that modify data * Server-rendered applications * Any POST/PUT/DELETE endpoints accessed via browser ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/csrf" ``` ## Quick Start ```go theme={null} app := mizu.New() // Production: secure cookies app.Use(csrf.Protect([]byte("32-byte-secret-key-here-12345"))) // Development: insecure cookies (HTTP) app.Use(csrf.ProtectDev([]byte("32-byte-secret-key-here-12345"))) ``` ## Configuration ### Options | Option | Type | Default | Description | | ---------------- | ------------------------------ | ----------------------- | ------------------------------- | | `Secret` | `[]byte` | **required** | Secret key for token generation | | `TokenLength` | `int` | `32` | Length of random token | | `TokenLookup` | `string` | `"header:X-CSRF-Token"` | Where to find token in request | | `CookieName` | `string` | `"_csrf"` | Name of CSRF cookie | | `CookiePath` | `string` | `"/"` | Cookie path | | `CookieMaxAge` | `int` | `86400` | Cookie max age in seconds | | `CookieSecure` | `bool` | `false` | Secure cookie flag | | `CookieHTTPOnly` | `bool` | `true` | HTTPOnly cookie flag | | `SameSite` | `http.SameSite` | `Lax` | SameSite cookie attribute | | `ErrorHandler` | `func(*mizu.Ctx, error) error` | - | Custom error handler | | `SkipPaths` | `[]string` | - | Paths to skip CSRF validation | ### TokenLookup Format The `TokenLookup` option uses `"source:name"` format: | Source | Example | Description | | -------- | ----------------------- | ------------------------- | | `header` | `"header:X-CSRF-Token"` | Read from request header | | `form` | `"form:_csrf"` | Read from form field | | `query` | `"query:csrf"` | Read from query parameter | ## Examples ### Basic Form Protection ```go theme={null} secret := csrf.GenerateSecret() // Or use a fixed 32-byte secret app.Use(csrf.New(csrf.Options{ Secret: secret, CookieSecure: true, // Use in production with HTTPS })) app.Get("/form", func(c *mizu.Ctx) error { token := csrf.Token(c) return c.HTML(200, `
    `) }) app.Post("/submit", func(c *mizu.Ctx) error { // CSRF validated automatically return c.Text(200, "Form submitted!") }) ``` ### Using Template Field Helper ```go theme={null} app.Get("/form", func(c *mizu.Ctx) error { // TemplateField returns a complete hidden input element field := csrf.TemplateField(c) return c.HTML(200, `
    `+field+`
    `) }) ``` ### JavaScript AJAX Requests ```go theme={null} app.Use(csrf.New(csrf.Options{ Secret: secret, TokenLookup: "header:X-CSRF-Token", })) ``` ```html theme={null} ``` ### Skip API Routes ```go theme={null} app.Use(csrf.New(csrf.Options{ Secret: secret, SkipPaths: []string{ "/api/webhook", // External webhooks "/api/public", // Public API endpoints }, })) ``` ### Custom Error Handler ```go theme={null} app.Use(csrf.New(csrf.Options{ Secret: secret, ErrorHandler: func(c *mizu.Ctx, err error) error { if err == csrf.ErrTokenMissing { return c.HTML(403, `

    Security Error

    Missing security token. Please try again.

    `) } return c.HTML(403, `

    Security Error

    Invalid security token. Please refresh and try again.

    `) }, })) ``` ### Multiple Token Sources ```go theme={null} // Accept token from either header or form app.Use(csrf.New(csrf.Options{ Secret: secret, TokenLookup: "form:_csrf", // Primary: form field })) // Alternative: Check header if form field missing // (Requires custom implementation) ``` ### Development vs Production ```go theme={null} func setupCSRF(app *mizu.App, isDev bool) { secret := []byte(os.Getenv("CSRF_SECRET")) if isDev { app.Use(csrf.ProtectDev(secret)) } else { app.Use(csrf.Protect(secret)) } } ``` ## API Reference ### Functions ```go theme={null} // New creates CSRF middleware with options func New(opts Options) mizu.Middleware // Protect creates middleware with secure cookies (production) func Protect(secret []byte) mizu.Middleware // ProtectDev creates middleware with insecure cookies (development) func ProtectDev(secret []byte) mizu.Middleware // GenerateSecret generates a secure random secret func GenerateSecret() []byte ``` ### Token Functions ```go theme={null} // Token extracts CSRF token from context func Token(c *mizu.Ctx) string // TemplateField returns HTML hidden input field func TemplateField(c *mizu.Ctx) string // TokenExpiry returns token expiration time func TokenExpiry(opts Options) time.Time ``` ### Error Types ```go theme={null} var ( ErrTokenMissing // CSRF token not found in request ErrTokenInvalid // CSRF token validation failed ) ``` ## How It Works 1. **Token Generation**: On GET requests, a token is generated and stored in a cookie 2. **Token Validation**: On POST/PUT/DELETE requests, the token must be included 3. **Double Submit**: The cookie token must match the request token 4. **HMAC Signature**: Tokens are signed to prevent tampering ``` GET /form ← Sets _csrf cookie with token ← Provides token for form POST /submit → Sends _csrf cookie (automatic) → Sends token in form/header ← Server validates both match ``` ## Technical Details ### Token Generation The middleware generates cryptographically secure tokens using the following process: 1. **Random Bytes**: Generates random bytes of specified length (default 32) using `crypto/rand` 2. **HMAC Signature**: Creates an HMAC-SHA256 signature of the random bytes using the secret key 3. **Encoding**: Encodes both the token and signature using base64 URL encoding 4. **Format**: Combines them as `token.signature` for tamper detection ```go theme={null} // Token format: base64(random_bytes).base64(hmac_sha256(random_bytes)) token := generateToken(32, secret) // Example: "a1b2c3d4...xyz.9f8e7d6c..." ``` ### Token Validation Validation follows a multi-step process to ensure security: 1. **Constant-Time Comparison**: Uses `subtle.ConstantTimeCompare` to prevent timing attacks 2. **Cookie-Request Match**: Verifies the cookie token exactly matches the request token 3. **Signature Verification**: Decodes and validates the HMAC signature 4. **HMAC Equality**: Uses `hmac.Equal` for secure signature comparison ### Safe vs Unsafe Methods The middleware distinguishes between HTTP methods: * **Safe Methods** (GET, HEAD, OPTIONS, TRACE): Generate or reuse tokens, no validation required * **Unsafe Methods** (POST, PUT, DELETE, PATCH): Require valid token in both cookie and request ### Context Storage Tokens are stored in the request context using a private `contextKey` type to prevent collisions with other middleware or application code. This allows retrieval via the `Token()` function throughout the request lifecycle. ### Path Skipping The `SkipPaths` option uses a map for O(1) lookup performance, allowing specific routes (like webhooks or public APIs) to bypass CSRF validation entirely. ## Security Considerations 1. **Secret Key** - Use a strong, random 32-byte secret 2. **Cookie Settings** - Use `Secure`, `HttpOnly`, and `SameSite` in production 3. **Token Rotation** - Tokens are per-session, not per-request 4. **HTTPS Required** - Always use HTTPS in production ### Recommended Production Settings ```go theme={null} app.Use(csrf.New(csrf.Options{ Secret: []byte(os.Getenv("CSRF_SECRET")), CookieSecure: true, CookieHTTPOnly: true, SameSite: http.SameSiteStrictMode, CookieMaxAge: 3600, // 1 hour })) ``` ## Best Practices * Always include CSRF protection for forms * Use secure cookies in production * Skip CSRF for API endpoints using Bearer auth * Regenerate token after login/logout * Set appropriate cookie expiration ## Testing The CSRF middleware includes comprehensive test coverage for all functionality: | Test Case | Description | Expected Behavior | | ------------------------------- | ---------------------------------------------- | --------------------------------------------------------- | | **Basic Functionality** | | | | Sets cookie on GET | GET request to protected endpoint | CSRF cookie is set with generated token | | Rejects POST without token | POST request without CSRF token | Returns 403 Forbidden | | Accepts POST with valid token | POST with matching cookie and header token | Request succeeds with 200 OK | | Rejects POST with invalid token | POST with mismatched tokens | Returns 403 Forbidden | | **Token Lookup Sources** | | | | Form token lookup | POST with token in form field | Token extracted from form and validated | | Query token lookup | POST with token in query parameter | Token extracted from query and validated | | Header token lookup | POST with token in header (default) | Token extracted from header and validated | | **Path Skipping** | | | | Skips listed path | POST to path in SkipPaths | Request bypasses CSRF validation | | Protects non-listed path | POST to path not in SkipPaths | CSRF validation required | | **Error Handling** | | | | Custom error handler | POST without token with ErrorHandler set | Custom error handler called with error | | Default error handler | POST without token, no ErrorHandler | Returns 403 with error text | | **Helper Functions** | | | | TemplateField generation | Calling TemplateField() with context | Returns HTML hidden input with token | | Protect middleware | Creating middleware with Protect() | Secure cookies enabled by default | | ProtectDev middleware | Creating middleware with ProtectDev() | Insecure cookies for development | | GenerateSecret uniqueness | Generating multiple secrets | Each secret is unique and 32 bytes | | **Validation Logic** | | | | Valid token validation | Validating matching tokens with correct secret | Returns true | | Mismatched tokens | Validating different tokens | Returns false | | Invalid signature | Validating token with wrong secret | Returns false | | Malformed token | Validating token without signature separator | Returns false | | **Token Generation** | | | | Token format | Generated token structure | Contains random bytes and HMAC signature separated by "." | | Token uniqueness | Generating 100 tokens | All tokens are unique | | **Configuration Validation** | | | | Panics without secret | Creating middleware with empty secret | Panics with error message | | Panics with invalid TokenLookup | Creating middleware with malformed TokenLookup | Panics with error message | ## Related Middlewares * [secure](/middlewares/secure) - HTTPS enforcement * [helmet](/middlewares/helmet) - Security headers # CSRF2 Source: https://docs.go-mizu.dev/middlewares/csrf2 Enhanced CSRF protection with double submit cookie pattern. ## Overview The `csrf2` middleware provides enhanced CSRF protection using the double submit cookie pattern, where a token is stored in both a cookie and must be submitted in a header or form field. Use it when you need: * Stateless CSRF protection * Cookie-based token validation * Enhanced security for SPAs ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/csrf2" ``` ## Quick Start ```go theme={null} app := mizu.New() app.Use(csrf2.New(csrf2.Options{})) app.Post("/submit", func(c *mizu.Ctx) error { return c.JSON(200, map[string]string{"status": "ok"}) }) ``` ## Configuration ### Options | Option | Type | Default | Description | | ---------------- | ----------------------- | ---------------------------- | -------------------- | | `CookieName` | `string` | `"csrf_token"` | Cookie name | | `HeaderName` | `string` | `"X-CSRF-Token"` | Header name | | `FormField` | `string` | `"csrf_token"` | Form field name | | `CookiePath` | `string` | `"/"` | Cookie path | | `CookieSecure` | `bool` | `false` | HTTPS only | | `CookieSameSite` | `http.SameSite` | `Lax` | SameSite policy | | `IgnoreMethods` | `[]string` | `["GET", "HEAD", "OPTIONS"]` | Methods to skip | | `ErrorHandler` | `func(*mizu.Ctx) error` | - | Custom error handler | ## Examples ### Basic Usage ```go theme={null} app.Use(csrf2.New(csrf2.Options{})) ``` ### Secure Configuration ```go theme={null} app.Use(csrf2.New(csrf2.Options{ CookieName: "_csrf", HeaderName: "X-CSRF-Token", CookieSecure: true, CookieSameSite: http.SameSiteStrictMode, })) ``` ### Custom Error Handler ```go theme={null} app.Use(csrf2.New(csrf2.Options{ ErrorHandler: func(c *mizu.Ctx) error { return c.JSON(403, map[string]string{ "error": "Invalid CSRF token", }) }, })) ``` ### Get Token for Forms ```go theme={null} app.Get("/form", func(c *mizu.Ctx) error { token := csrf2.Token(c) return c.HTML(200, `
    `) }) ``` ## How It Works 1. Middleware sets CSRF token in cookie 2. Client reads cookie and includes token in header/form 3. On state-changing requests, token is validated 4. If tokens match, request proceeds 5. If not, returns 403 Forbidden ## API Reference ### Functions ```go theme={null} // New creates CSRF2 middleware func New(opts Options) mizu.Middleware // Token returns CSRF token from context func Token(c *mizu.Ctx) string ``` ## Client Implementation ### JavaScript (SPA) ```javascript theme={null} // Read token from cookie function getCsrfToken() { return document.cookie .split('; ') .find(row => row.startsWith('csrf_token=')) ?.split('=')[1]; } // Include in requests fetch('/api/submit', { method: 'POST', headers: { 'X-CSRF-Token': getCsrfToken(), 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); ``` ## Technical Details ### Token Generation The middleware generates cryptographically secure tokens using the following process: 1. **Random Bytes**: Generates random bytes using `crypto/rand` 2. **Timestamp**: Appends current Unix timestamp for rotation support 3. **Signature**: Signs the token with SHA-256 using the provided secret 4. **Encoding**: Base64 URL-safe encoding for transport ### Token Validation Token validation uses constant-time comparison to prevent timing attacks: 1. **Decode**: Both cookie and submitted tokens are base64-decoded 2. **Length Check**: Validates minimum token length (8 bytes) 3. **Constant-Time Compare**: Uses XOR-based comparison across all bytes 4. **Result**: Returns validation result without early exit ### Cookie Storage Tokens are stored in HTTP cookies with configurable security options: * **HttpOnly**: Default true to prevent JavaScript access * **Secure**: Configurable for HTTPS enforcement * **SameSite**: Default Lax mode for CSRF protection * **MaxAge**: Default 24 hours (86400 seconds) * **Path**: Default "/" for site-wide availability ### Token Retrieval The middleware checks for tokens in the following order: 1. **Header**: Checks configured header name (default: `X-CSRF-Token`) 2. **Form**: Parses form data for configured field name (default: `_csrf`) 3. **Query**: Checks URL query parameters as fallback ### Origin Validation When enabled, the middleware validates request origin: 1. **Origin Header**: Primary validation source 2. **Referer Fallback**: Used if Origin header is missing 3. **Allowed Origins**: Compares against configured whitelist 4. **Host Match**: Falls back to request host if no whitelist specified ### Advanced Features * **Token Masking**: XOR-based masking for additional security * **Fingerprinting**: SHA-256 hash of User-Agent and Accept headers * **Form Input**: HTML helper for hidden form fields * **Meta Tag**: HTML meta tag helper for SPAs ## Best Practices * Always use HTTPS in production * Use SameSite Strict for sensitive applications * Include token in all state-changing requests * Regenerate tokens on authentication changes ## Testing ### Test Coverage | Test Case | Description | Expected Behavior | | ------------------------- | ------------------------------- | --------------------------------------------------------------- | | `TestNew` | Basic middleware initialization | Creates middleware and sets CSRF cookie on GET request | | `TestGetToken` | Token retrieval from context | Token is stored in context and retrievable via `GetToken()` | | `TestValidateTokenHeader` | Token validation via header | POST request with valid token in header succeeds (200 OK) | | `TestValidateTokenForm` | Token validation via form field | POST request with valid token in form field succeeds (200 OK) | | `TestMissingToken` | Request without CSRF token | POST request without token returns 403 Forbidden | | `TestInvalidToken` | Request with invalid token | POST request with invalid token returns 403 Forbidden | | `TestSkipPaths` | Path exclusion from validation | Configured paths skip CSRF validation | | `TestSkipMethods` | HTTP method exclusion | Safe methods (GET, HEAD, OPTIONS) skip validation | | `TestCustomErrorHandler` | Custom error handling | Custom error handler returns JSON response on failure | | `TestCustomCookieName` | Custom cookie configuration | CSRF cookie uses configured custom name | | `TestCustomHeaderName` | Custom header configuration | Token validation accepts configured custom header name | | `TestTokenHandler` | Token endpoint handler | Token handler returns JSON with current CSRF token | | `TestFormInput` | HTML form input helper | Generates hidden input field with token value | | `TestMetaTag` | HTML meta tag helper | Generates meta tag with csrf-token name and value | | `TestMaskUnmask` | Token masking/unmasking | Masked token differs from original, unmask restores original | | `TestFingerprint` | Request fingerprinting | Same headers produce same fingerprint, different headers differ | ## Related Middlewares * [csrf](/middlewares/csrf) - Basic CSRF protection * [session](/middlewares/session) - Session management * [secure](/middlewares/secure) - Security settings # Embed Source: https://docs.go-mizu.dev/middlewares/embed Embedded filesystem middleware for serving bundled assets. ## Overview The `embed` middleware provides helpers for serving files from Go's embedded filesystem (embed.FS), enabling single-binary deployments. Use it when you need: * Single binary deployment * Bundled static assets * Embedded templates ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/embed" ``` ## Quick Start ```go theme={null} //go:embed static/* var staticFS embed.FS app := mizu.New() app.Use(embed.Static(staticFS, "static")) ``` ## Examples ### Serve Static Files ```go theme={null} //go:embed static/* var staticFS embed.FS app.Use(embed.Static(staticFS, "static")) // Files at static/css/app.css available at /css/app.css ``` ### With Prefix ```go theme={null} //go:embed assets/* var assetsFS embed.FS app.Use(embed.WithOptions(embed.Options{ FS: assetsFS, Root: "assets", Prefix: "/assets", })) // /assets/js/app.js -> assets/js/app.js ``` ### Multiple Embeds ```go theme={null} //go:embed static/* var staticFS embed.FS //go:embed templates/* var templatesFS embed.FS // Serve static files app.Use(embed.Static(staticFS, "static")) // Load templates tmpl := embed.MustParseTemplates(templatesFS, "templates/*.html") ``` ### SPA with Embed ```go theme={null} //go:embed dist/* var distFS embed.FS app.Use(embed.SPA(distFS, "dist")) ``` ## API Reference ### Functions ```go theme={null} // Static serves embedded static files func Static(fs embed.FS, root string) mizu.Middleware // SPA serves embedded SPA func SPA(fs embed.FS, root string) mizu.Middleware // WithOptions creates with configuration func WithOptions(opts Options) mizu.Middleware // Helper functions func MustParseTemplates(fs embed.FS, pattern string) *template.Template func ReadFile(fs embed.FS, name string) ([]byte, error) ``` ## Build Considerations ```go theme={null} // Embed entire directory //go:embed static/* // Embed specific files //go:embed favicon.ico robots.txt // Embed with pattern //go:embed templates/*.html templates/**/*.html ``` ## Technical Details ### Implementation Overview The embed middleware provides a bridge between Go's `embed.FS` and Mizu's HTTP serving capabilities. It wraps the standard library's `http.FileServer` with additional features like custom root directories, caching headers, and SPA routing. ### Core Components **Filesystem Handling** * Uses `fs.FS` interface for maximum flexibility * Supports `fs.Sub` to create subdirectory filesystems * Falls back to original filesystem if subdirectory doesn't exist * Implements proper path cleaning and normalization **File Serving Strategy** 1. Cleans and normalizes the request path 2. Attempts to open the requested file 3. Falls back to index file for directory requests 4. Delegates to `NotFoundHandler` or next middleware if file not found 5. Uses `http.FileServer` for actual serving (handles range requests, content-type, etc.) **Cache Control** * Optional `MaxAge` setting for Cache-Control headers * Custom `itoa` function to avoid allocations for header values * Headers set before delegating to file server **SPA Support** * Custom `NotFoundHandler` that serves index.html for missing routes * Preserves normal file serving for actual assets * Uses `http.ServeContent` for proper handling of conditional requests ### Handler vs Middleware The package provides both middleware and handler functions: * **Middleware** (`New`, `WithOptions`, `Static`, etc.): Falls through to next handler if file not found * **Handler** (`Handler`, `HandlerWithOptions`): Directly serves files without fallthrough ## Best Practices * Use for production deployments * Keep embedded files small * Use content hashing for cache busting * Test embedded files in CI ## Testing The embed middleware includes comprehensive test coverage for all functionality: | Test Case | Description | Expected Behavior | | ----------------------------------------- | ------------------------------------- | --------------------------------------------------------------- | | `TestNew` | Basic middleware creation and serving | Serves root index.html and nested files successfully | | `TestWithOptions_Root` | Serving from a subdirectory | Files from subdirectory accessible at root URL path | | `TestWithOptions_MaxAge` | Cache control headers | Cache-Control header set to specified max-age value | | `TestWithOptions_NotFoundHandler` | Custom not found handling | Custom handler invoked for missing files with JSON response | | `TestHandler` | Direct handler (non-middleware) | Serves files without middleware chain | | `TestStatic` | Static helper function | Serves files from specified subdirectory | | `TestWithCaching` | Caching helper function | Cache-Control header set correctly | | `TestIndexFile` | Index file serving | Root and subdirectory index files served automatically | | `TestFallthrough` | Middleware fallthrough | Non-existent files fall through to next handler | | `TestSPA` | Single-page application mode | Existing files served normally, unknown routes serve index.html | | `TestSPA_DefaultIndex` | SPA with default index | Empty index parameter defaults to "index.html" | | `TestHandlerWithOptions` | Handler with options | Options applied correctly in handler mode | | `TestHandlerWithOptions_Root` | Handler with custom root | Files served from specified root directory | | `TestWithOptions_InvalidRoot` | Invalid root directory handling | Falls back to original filesystem when root doesn't exist | | `TestWithOptions_PathWithoutLeadingSlash` | Path normalization | Paths without leading slash handled correctly | | `TestItoa` | Integer to string conversion | Custom itoa function converts integers correctly | ## Related Middlewares * [static](/middlewares/static) - File system static files * [spa](/middlewares/spa) - SPA support * [favicon](/middlewares/favicon) - Favicon serving # Envelope Source: https://docs.go-mizu.dev/middlewares/envelope Response envelope middleware for consistent API response structure. ## Overview The `envelope` middleware wraps all responses in a consistent structure, adding metadata like status, pagination, and timestamps. Use it when you need: * Consistent API responses * Response metadata * Standardized error format ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/envelope" ``` ## Quick Start ```go theme={null} app := mizu.New() app.Use(envelope.New()) app.Get("/users", func(c *mizu.Ctx) error { return c.JSON(200, users) }) // Response: {"success": true, "data": [...users]} ``` ## Configuration ### Options | Option | Type | Default | Description | | ------------------ | -------- | ----------- | ------------------ | | `SuccessKey` | `string` | `"success"` | Success field name | | `DataKey` | `string` | `"data"` | Data field name | | `ErrorKey` | `string` | `"error"` | Error field name | | `IncludeTimestamp` | `bool` | `false` | Add timestamp | | `IncludeRequestID` | `bool` | `false` | Add request ID | ## Examples ### Default Envelope ```go theme={null} app.Use(envelope.New()) // Success: {"success": true, "data": {...}} // Error: {"success": false, "error": "message"} ``` ### With Metadata ```go theme={null} app.Use(envelope.WithOptions(envelope.Options{ IncludeTimestamp: true, IncludeRequestID: true, })) // {"success": true, "data": {...}, "timestamp": "...", "request_id": "..."} ``` ### Custom Structure ```go theme={null} app.Use(envelope.WithOptions(envelope.Options{ SuccessKey: "ok", DataKey: "result", ErrorKey: "message", })) // {"ok": true, "result": {...}} ``` ### With Pagination ```go theme={null} app.Use(envelope.New()) app.Get("/users", func(c *mizu.Ctx) error { envelope.SetMeta(c, map[string]any{ "page": 1, "per_page": 20, "total": 100, "total_pages": 5, }) return c.JSON(200, users) }) // {"success": true, "data": [...], "meta": {"page": 1, ...}} ``` ## API Reference ### Functions ```go theme={null} // New creates envelope middleware func New() mizu.Middleware // WithOptions creates with configuration func WithOptions(opts Options) mizu.Middleware // SetMeta adds metadata to response func SetMeta(c *mizu.Ctx, meta map[string]any) ``` ## Response Format ### Success Response ```json theme={null} { "success": true, "data": { ... }, "meta": { ... } } ``` ### Error Response ```json theme={null} { "success": false, "error": "Error message", "code": "ERROR_CODE" } ``` ## Technical Details ### Implementation Architecture The envelope middleware uses a custom response writer (`envelopeWriter`) to intercept and capture the original response before wrapping it in the envelope structure. #### Response Writer The middleware captures responses through a buffer-based writer: * Intercepts `WriteHeader()` calls to capture HTTP status codes * Buffers response body through `Write()` calls * Stores status code (defaults to 200 if not explicitly set) #### Envelope Construction The envelope is constructed after the handler completes: 1. **Content-Type Check**: Only wraps responses matching configured content types (default: `application/json`) 2. **Success Determination**: Status codes 200-399 are considered successful 3. **Response Parsing**: Original JSON response is unmarshaled into the data field 4. **Error Handling**: For error responses, attempts to extract error message from: * `error` field in response body * `message` field in response body * Handler error if available 5. **Metadata Addition**: When `IncludeMeta` is enabled, adds status code and request ID 6. **Re-encoding**: Final envelope is marshaled to JSON and written to the original writer #### Field Customization All field names are customizable through options: * `SuccessField`: Controls the success indicator field name * `DataField`: Controls the data payload field name * `ErrorField`: Controls the error message field name #### Content-Type Filtering The middleware only wraps responses with matching content types: * Checks exact match or with charset suffix (e.g., `application/json; charset=utf-8`) * Non-matching responses pass through unmodified * Default content types: `["application/json"]` ### Helper Functions The package provides convenience functions for common responses: * `Success()`: 200 OK with data * `Created()`: 201 Created with data * `NoContent()`: 204 No Content * `BadRequest()`: 400 Bad Request with error message * `Unauthorized()`: 401 Unauthorized with error message * `Forbidden()`: 403 Forbidden with error message * `NotFound()`: 404 Not Found with error message * `InternalError()`: 500 Internal Server Error with error message ## Best Practices * Use consistent structure across APIs * Include relevant metadata * Document envelope format * Handle nested errors properly ## Testing The envelope middleware includes comprehensive tests covering all functionality: | Test Case | Description | Expected Behavior | | ------------------------------ | ---------------------------------- | ------------------------------------------------------------ | | `TestNew` | Default middleware initialization | Wraps response with `success: true` and `data` field | | `TestWithOptions_IncludeMeta` | Metadata inclusion with request ID | Includes `meta` object with `status_code` and `request_id` | | `TestWithOptions_CustomFields` | Custom field names | Uses custom `ok`, `result`, and `message` fields | | `TestSuccess` | Success helper function | Returns 200 OK with `success: true` and data | | `TestError` | Error helper function | Returns error status with `success: false` and error message | | `TestCreated` | Created helper function | Returns 201 Created status with success response | | `TestBadRequest` | BadRequest helper function | Returns 400 Bad Request with error message | | `TestUnauthorized` | Unauthorized helper function | Returns 401 Unauthorized with error message | | `TestForbidden` | Forbidden helper function | Returns 403 Forbidden with error message | | `TestNotFound` | NotFound helper function | Returns 404 Not Found with error message | | `TestInternalError` | InternalError helper function | Returns 500 Internal Server Error with error message | | `TestNoContent` | NoContent helper function | Returns 204 No Content without body | ## Related Middlewares * [transformer](/middlewares/transformer) - Response transformation * [requestid](/middlewares/requestid) - Request IDs * [errorpage](/middlewares/errorpage) - Error pages # Error Page Source: https://docs.go-mizu.dev/middlewares/errorpage Custom error page middleware for handling HTTP errors. ## Overview The `errorpage` middleware provides custom error pages for HTTP error responses, replacing default error responses with branded pages. Use it when you need: * Custom 404 pages * Branded error pages * User-friendly error messages ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/errorpage" ``` ## Quick Start ```go theme={null} app := mizu.New() app.Use(errorpage.New(errorpage.Options{ 404: errorpage.File("./errors/404.html"), 500: errorpage.File("./errors/500.html"), })) ``` ## Configuration ### Options Error code to handler mapping: ```go theme={null} type Options map[int]Handler ``` ## Examples ### HTML Files ```go theme={null} app.Use(errorpage.New(errorpage.Options{ 404: errorpage.File("./errors/404.html"), 500: errorpage.File("./errors/500.html"), })) ``` ### Inline HTML ```go theme={null} app.Use(errorpage.New(errorpage.Options{ 404: errorpage.HTML(`

    Page Not Found

    The page you requested does not exist.

    `), })) ``` ### JSON Errors ```go theme={null} app.Use(errorpage.New(errorpage.Options{ 404: errorpage.JSON(map[string]string{ "error": "Resource not found", "code": "NOT_FOUND", }), })) ``` ### Custom Handler ```go theme={null} app.Use(errorpage.New(errorpage.Options{ 404: func(c *mizu.Ctx) error { // Log the 404 log.Printf("404: %s", c.Request().URL.Path) // Return custom response return c.HTML(404, custom404Page) }, })) ``` ### Catch All Errors ```go theme={null} app.Use(errorpage.New(errorpage.Options{ 400: errorHandler, 401: errorHandler, 403: errorHandler, 404: errorHandler, 500: errorHandler, })) ``` ## API Reference ### Functions ```go theme={null} // New creates error page middleware func New(opts Options) mizu.Middleware // Response helpers func File(path string) Handler func HTML(content string) Handler func JSON(data any) Handler func Template(tmpl *template.Template, data any) Handler ``` ## Technical Details ### Implementation Overview The errorpage middleware uses a response writer wrapper (`statusWriter`) to intercept HTTP status codes before they are sent to the client. This allows the middleware to: 1. **Capture Status Codes**: Wraps the response writer to track status codes without immediately sending them 2. **Conditional Rendering**: Only renders error pages for status codes >= 400 when no response body has been written 3. **Template Processing**: Uses Go's `html/template` package for rendering error pages with customizable templates 4. **Default Pages**: Provides pre-configured error pages for common HTTP error codes (400, 401, 403, 404, 405, 500, 502, 503, 504) ### Response Writer Wrapper The `statusWriter` type intercepts `WriteHeader()` calls to delay status code propagation until after determining whether a custom error page should be shown: ```go theme={null} type statusWriter struct { http.ResponseWriter status int written bool } ``` This wrapper ensures that: * Status codes are captured but not immediately sent * Error pages are only shown when the response body hasn't been written * The middleware can distinguish between different error scenarios ### Error Handling Flow 1. Request enters middleware 2. Response writer is wrapped with `statusWriter` 3. Next handler is called 4. If status code >= 400 and no body written: * Custom error handler is invoked (if configured) * Custom 404 handler is invoked for 404s (if configured) * Appropriate error page is rendered using templates 5. Response is sent to client ## Best Practices * Keep error pages simple and fast * Include navigation back to home * Log errors for monitoring * Use different pages for client vs server errors ## Testing The errorpage middleware includes comprehensive test coverage for all major functionality: | Test Case | Description | Expected Behavior | | --------------------------------------------- | ------------------------------------------------ | ---------------------------------------------------------------------------- | | `TestNew` | Tests default error page rendering | Returns 404 status and displays default "Not Found" page with error code | | `TestWithOptions_CustomPages` | Tests custom error page configuration | Renders custom title "Oops!" and message for 404 errors | | `TestWithOptions_CustomTemplate` | Tests custom HTML template | Renders error using custom template format "Error 500: ..." | | `TestWithOptions_NotFoundHandler` | Tests custom 404 handler | Returns JSON response `{"error":"not found"}` instead of HTML | | `TestWithOptions_ErrorHandler` | Tests custom error handler for all errors | Returns "Custom error handler" text for any error status | | `TestWithOptions_SuccessDoesNotShowErrorPage` | Verifies successful responses bypass error pages | Returns "success" without error page interference | | `TestNotFound` | Tests NotFound() middleware function | Returns 404 status with "Not Found" text | | `TestCustom` | Tests Custom() middleware with specific pages | Renders "Maintenance" page for 503 status | | `TestPage404` | Tests Page404 helper function | Creates page with code 404 and custom title/message | | `TestPage500` | Tests Page500 helper function | Creates page with code 500 and custom title/message | | `TestDefaultPages` | Validates all default error pages exist | Confirms default pages for codes 400, 401, 403, 404, 405, 500, 502, 503, 504 | ## Related Middlewares * [recover](/middlewares/recover) - Panic recovery * [envelope](/middlewares/envelope) - Response wrapping * [logger](/middlewares/logger) - Request logging # ETag Source: https://docs.go-mizu.dev/middlewares/etag ETag generation middleware for HTTP caching and conditional requests. ## Overview The `etag` middleware automatically generates ETag headers for responses, enabling efficient HTTP caching with conditional requests. When a client sends a matching `If-None-Match` header, it returns 304 Not Modified. Use it when you need: * Bandwidth optimization through caching * Conditional GET requests * Cache validation ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/etag" ``` ## Quick Start ```go theme={null} app := mizu.New() // Enable ETag generation app.Use(etag.New()) app.Get("/api/data", func(c *mizu.Ctx) error { return c.JSON(200, data) }) ``` ## Configuration ### Options | Option | Type | Default | Description | | ---------- | --------------------- | -------- | -------------------- | | `Weak` | `bool` | `false` | Generate weak ETags | | `HashFunc` | `func([]byte) string` | MD5 hash | Custom hash function | ## Examples ### Strong ETags (Default) ```go theme={null} app.Use(etag.New()) // Generates: ETag: "abc123def456" ``` ### Weak ETags ```go theme={null} app.Use(etag.Weak()) // Generates: ETag: W/"abc123def456" ``` ### Custom Hash Function ```go theme={null} app.Use(etag.WithOptions(etag.Options{ HashFunc: func(body []byte) string { h := sha256.Sum256(body) return hex.EncodeToString(h[:16]) }, })) ``` ## How It Works 1. Request comes in without `If-None-Match` 2. Response is generated with ETag header 3. Client caches response with ETag 4. Next request includes `If-None-Match: "etag-value"` 5. If content unchanged, returns 304 Not Modified 6. If changed, returns new response with new ETag ## API Reference ### Functions ```go theme={null} // New creates ETag middleware with defaults func New() mizu.Middleware // Weak creates middleware generating weak ETags func Weak() mizu.Middleware // WithOptions creates middleware with configuration func WithOptions(opts Options) mizu.Middleware ``` ## Behavior * Only generates ETags for GET and HEAD requests * Skips error responses (4xx, 5xx) * Returns 304 Not Modified for matching ETags * Supports wildcard `If-None-Match: *` * Same content always produces same ETag ## Strong vs Weak ETags | Type | Format | Use Case | | ------ | ------------ | ----------------------- | | Strong | `"abc123"` | Byte-for-byte identical | | Weak | `W/"abc123"` | Semantically equivalent | ## Technical Details The ETag middleware implementation uses a buffered writer approach to intercept and process responses: ### Response Buffering The middleware buffers the entire response body using a custom `bufferedWriter` that wraps the original `http.ResponseWriter`. This allows the middleware to: * Calculate the hash of the complete response body * Determine the final status code * Conditionally return 304 Not Modified before sending the full response ### Hash Generation By default, the middleware uses CRC32 (IEEE polynomial) for hash generation, which provides: * Fast computation suitable for HTTP caching * Sufficient uniqueness for cache validation * Low memory overhead The hash function can be customized via `HashFunc` option to use alternative algorithms like SHA256 for enhanced security or collision resistance. ### ETag Format ETags are formatted according to RFC 7232: * Strong ETags: `""` - Indicates byte-for-byte identical content * Weak ETags: `W/""` - Indicates semantically equivalent content ### Conditional Request Handling The middleware checks the `If-None-Match` header from the client: * If it matches the generated ETag, returns 304 Not Modified with the ETag header * If it's a wildcard (`*`), always returns 304 Not Modified * If no match, proceeds with the full response including the ETag header ### Request Method Filtering The middleware only processes GET and HEAD requests, as these are the only methods where conditional requests are applicable according to HTTP specifications. ### Status Code Handling ETags are only generated for successful responses (2xx status codes) with non-empty body content. Error responses (4xx, 5xx) are not cached and do not receive ETag headers. ## Best Practices * Use strong ETags for static files * Use weak ETags for dynamic content that may vary slightly * Combine with Cache-Control for full caching strategy * Place after compression middleware ## Testing The middleware includes comprehensive test coverage for all functionality: | Test Case | Description | Expected Behavior | | ---------------------------------------------------- | ------------------------------------------ | -------------------------------------- | | `TestNew/generates_ETag` | Basic ETag generation for GET request | Returns 200 OK with quoted ETag header | | `TestNew/returns_304_for_matching_If-None-Match` | Client sends matching If-None-Match header | Returns 304 Not Modified | | `TestNew/returns_200_for_non-matching_If-None-Match` | Client sends non-matching If-None-Match | Returns 200 OK with full response | | `TestNew/wildcard_If-None-Match` | Client sends If-None-Match: \* | Returns 304 Not Modified | | `TestWeak` | Weak ETag generation | ETag header starts with `W/"` | | `TestWithOptions_CustomHash` | Custom hash function provided | Uses custom hash in ETag | | `TestNew_SkipsNonGetHead` | POST request made | No ETag header generated | | `TestNew_SkipsErrorResponses` | 500 error response | No ETag header generated | | `TestNew_ConsistentETag` | Same content returned twice | Both responses have identical ETags | | `TestNew_DifferentContent` | Different content returned | Different ETags generated | | `TestNew_HEAD` | HEAD request made | Processes request without errors | ## Related Middlewares * [cache](/middlewares/cache) - Cache-Control headers * [lastmodified](/middlewares/lastmodified) - Last-Modified headers * [compress](/middlewares/compress) - Response compression # Expvar Source: https://docs.go-mizu.dev/middlewares/expvar Expvar metrics endpoint middleware for Go runtime metrics. ## Overview The `expvar` middleware exposes Go's expvar metrics including runtime stats, memory usage, and custom variables. Use it when you need: * Go runtime metrics * Memory usage monitoring * Custom variable export ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/expvar" ``` ## Quick Start ```go theme={null} app := mizu.New() // Expose expvar endpoint app.Get("/debug/vars", expvar.Handler()) ``` ## Examples ### Basic Setup ```go theme={null} app.Get("/debug/vars", expvar.Handler()) ``` ### Custom Variables ```go theme={null} import "expvar" // Publish custom variable var requests = expvar.NewInt("requests") app.Use(func(next mizu.Handler) mizu.Handler { return func(c *mizu.Ctx) error { requests.Add(1) return next(c) } }) app.Get("/debug/vars", expvar.Handler()) ``` ### With Auth Protection ```go theme={null} debug := app.Group("/debug") debug.Use(basicauth.New(adminCredentials)) debug.Get("/vars", expvar.Handler()) ``` ## Default Variables | Variable | Description | | ---------- | ---------------------- | | `cmdline` | Command line arguments | | `memstats` | Memory statistics | ## API Reference ### Functions ```go theme={null} // Handler returns expvar HTTP handler func Handler() mizu.Handler ``` ## Response Format ```json theme={null} { "cmdline": ["./myapp"], "memstats": { "Alloc": 1234567, "TotalAlloc": 9876543, "Sys": 12345678, ... }, "requests": 42 } ``` ## Technical Details ### Standard Library Integration The middleware wraps Go's standard `expvar` package: * Uses `expvar.Handler()` to serve the variables endpoint * Exports all published variables as JSON * Includes built-in `cmdline` and `memstats` variables ### Path Matching * Only intercepts requests to the configured path (default: `/debug/vars`) * All other requests pass through to the next handler * Efficient path comparison using string equality ### Helper Functions The package provides convenience wrappers for common expvar types: * `NewInt(name)`: Creates and publishes an expvar.Int * `NewFloat(name)`: Creates and publishes an expvar.Float * `NewString(name)`: Creates and publishes an expvar.String * `NewMap(name)`: Creates and publishes an expvar.Map ### Thread Safety All expvar types are safe for concurrent use across goroutines. ## Best Practices * Protect endpoint in production * Use for debugging and monitoring * Combine with custom metrics * Don't expose sensitive data ## Testing The middleware includes comprehensive test coverage. Key test cases: | Test Case | Description | Expected Behavior | | ---------------------------- | ---------------------- | ---------------------------------- | | `TestNew/expvar_endpoint` | Request to /debug/vars | Returns 200 OK with expvar JSON | | `TestNew/non-expvar_path` | Request to other paths | Passes through to next handler | | `TestWithOptions_CustomPath` | Custom expvar path | Returns expvar data at custom path | | `TestHelpers/NewInt` | Integer counter helper | Creates and increments counter | | `TestHelpers/NewFloat` | Float gauge helper | Creates and sets float value | | `TestHelpers/NewString` | String variable helper | Creates and sets string value | | `TestHelpers/NewMap` | Map variable helper | Creates map with key-value pairs | | `TestHelpers/Get` | Get published variable | Returns published variable by name | | `TestHelpers/Do` | Iterate all variables | Iterates over all expvars | | `TestJSON` | JSON export function | Returns valid JSON object | ## Related Middlewares * [prometheus](/middlewares/prometheus) - Prometheus metrics * [pprof](/middlewares/pprof) - Profiling * [healthcheck](/middlewares/healthcheck) - Health checks # Fallback Source: https://docs.go-mizu.dev/middlewares/fallback Fallback handler middleware for graceful degradation. ## Overview The `fallback` middleware provides fallback responses when the primary handler fails, enabling graceful degradation of services. Use it when you need: * Graceful degradation * Default responses on failure * Service resilience ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/fallback" ``` ## Quick Start ```go theme={null} app := mizu.New() // Return cached data on error app.Use(fallback.New(func(c *mizu.Ctx) error { return c.JSON(200, cachedResponse) })) ``` ## Configuration ### Options | Option | Type | Default | Description | | ---------- | ------------------ | ---------- | ---------------------------------- | | `Handler` | `mizu.Handler` | Required | Fallback handler | | `OnError` | `func(error) bool` | All errors | Which errors trigger fallback | | `OnStatus` | `[]int` | `[]` | Status codes that trigger fallback | ## Examples ### Basic Fallback ```go theme={null} app.Use(fallback.New(func(c *mizu.Ctx) error { return c.JSON(200, map[string]string{ "message": "Service temporarily unavailable", "status": "degraded", }) })) ``` ### On Specific Errors ```go theme={null} app.Use(fallback.WithOptions(fallback.Options{ Handler: fallbackHandler, OnError: func(err error) bool { return errors.Is(err, ErrDatabaseUnavailable) }, })) ``` ### On Status Codes ```go theme={null} app.Use(fallback.WithOptions(fallback.Options{ Handler: fallbackHandler, OnStatus: []int{500, 502, 503, 504}, })) ``` ### Cached Response ```go theme={null} var cachedData atomic.Value // Update cache periodically go func() { for { data := fetchData() cachedData.Store(data) time.Sleep(time.Minute) } }() app.Use(fallback.New(func(c *mizu.Ctx) error { return c.JSON(200, cachedData.Load()) })) ``` ## API Reference ### Functions ```go theme={null} // New creates fallback middleware func New(handler mizu.Handler) mizu.Middleware // WithOptions creates with configuration func WithOptions(opts Options) mizu.Middleware ``` ## Technical Details ### Implementation Architecture The fallback middleware operates using a wrapper pattern that intercepts errors from downstream handlers: 1. **Error Interception**: The middleware wraps the next handler in the chain and catches any returned errors 2. **Panic Recovery**: When `CatchPanic` is enabled, it uses Go's `defer`/`recover` mechanism to catch panics and convert them to errors 3. **Response Capture**: For status code-based fallbacks (like `NotFound`), it uses a custom `responseCapture` type that wraps `http.ResponseWriter` to intercept status codes 4. **Handler Delegation**: Based on the configuration, it delegates to the appropriate fallback handler ### Key Components * **Options**: Configuration structure that defines when and how to trigger fallback responses * **responseCapture**: Internal type that captures HTTP response status codes before they're written * **panicError**: Custom error type that wraps panic values, converting non-error panics into error types ### Execution Flow ``` Request → Middleware → [Optional Panic Recovery] → Next Handler → Error Check → Fallback Handler (if needed) → Response ``` ### Error Handling Chain The middleware provides multiple fallback strategies that can be composed: 1. **New()**: Basic error handler with custom function 2. **Default()**: Simple text response fallback 3. **JSON()**: Structured JSON error responses 4. **Redirect()**: Redirect-based error handling 5. **Chain()**: Sequential fallback handlers with conditional logic 6. **NotFound()**: Status code-specific handling for 404s 7. **ForStatus()**: Generic status code-specific handling ## Best Practices * Keep fallback responses lightweight * Log when fallback is triggered * Monitor fallback rate * Use cached data when possible ## Testing The fallback middleware includes comprehensive test coverage for various scenarios: | Test Case | Description | Expected Behavior | | --------------------------------- | -------------------------------------- | --------------------------------------------------------------- | | `TestNew` | Basic fallback middleware creation | Returns custom error message when handler errors | | `TestWithOptions_CatchPanic` | Panic recovery with custom handler | Catches panic and returns custom response with 500 status | | `TestWithOptions_DefaultMessage` | Default message without custom handler | Returns configured default message on error | | `TestWithOptions_NoError` | Successful request handling | Passes through successful responses without triggering fallback | | `TestDefault` | Default text fallback | Returns simple text message on error | | `TestJSON` | JSON error responses | Returns JSON-formatted error with proper content type | | `TestRedirect` | Redirect on error | Redirects to specified URL with correct status code | | `TestChain` | Chained fallback handlers | Tries handlers in order, uses first that handles the error | | `TestChain_NoHandlerMatches` | Chain with no matches | Returns default "An error occurred" message | | `TestPanicWithError` | Panic with error type | Catches error panic and formats message correctly | | `TestPanicWithNonError` | Panic with non-error type | Converts non-error panic to "panic occurred" message | | `TestPanicWithDefaultHandler` | Panic without custom handler | Uses default message when no custom handler provided | | `TestNotFound` | 404 not found handling | Triggers custom handler on 404 status | | `TestForStatus` | Status code-specific handling | Creates middleware for specific status codes | | `TestWithOptions_DefaultsUsed` | Default configuration values | Uses "An error occurred" when no config provided | | `TestResponseCapture_WriteHeader` | Response status capturing | Captures first status code, ignores subsequent writes | | `TestResponseCapture_Write` | Response write interception | Sets status to 200 on first write if not set | | `TestPanicError_ErrorMethod` | Panic error type behavior | Correctly formats error messages for different panic types | ## Related Middlewares * [circuitbreaker](/middlewares/circuitbreaker) - Circuit breaker * [retry](/middlewares/retry) - Automatic retries * [cache](/middlewares/cache) - Response caching # Favicon Source: https://docs.go-mizu.dev/middlewares/favicon Favicon serving middleware for efficient favicon.ico handling. ## Overview The `favicon` middleware serves favicon files efficiently, caching them in memory and handling HEAD requests properly. It prevents favicon requests from reaching your application routes. Use it when you need: * Serve favicon from memory * Prevent favicon 404 logs * Custom favicon handling ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/favicon" ``` ## Quick Start ```go theme={null} app := mizu.New() // Serve favicon from file app.Use(favicon.New("./static/favicon.ico")) ``` ## Configuration ### Options | Option | Type | Default | Description | | -------------- | -------- | ---------------------------- | ---------------------------------- | | `File` | `string` | Required | Path to favicon file | | `Data` | `[]byte` | `nil` | Favicon data (alternative to file) | | `CacheControl` | `string` | `"public, max-age=31536000"` | Cache-Control header | ## Examples ### From File ```go theme={null} app.Use(favicon.New("./static/favicon.ico")) ``` ### From Embedded Data ```go theme={null} //go:embed favicon.ico var faviconData []byte app.Use(favicon.FromData(faviconData)) ``` ### Custom Cache Control ```go theme={null} app.Use(favicon.WithOptions(favicon.Options{ File: "./static/favicon.ico", CacheControl: "public, max-age=86400", // 1 day })) ``` ### Disable Favicon ```go theme={null} // Return empty response for favicon requests app.Use(favicon.Disabled()) ``` ## API Reference ### Functions ```go theme={null} // New creates favicon middleware from file func New(file string) mizu.Middleware // FromData creates middleware from byte data func FromData(data []byte) mizu.Middleware // WithOptions creates middleware with configuration func WithOptions(opts Options) mizu.Middleware // Disabled returns empty response for favicon requests func Disabled() mizu.Middleware ``` ## Behavior * Caches favicon in memory after first read * Sets proper Content-Type (image/x-icon) * Handles both GET and HEAD requests * Only matches exact `/favicon.ico` path ## Technical Details ### Implementation The favicon middleware implements efficient favicon serving with the following characteristics: 1. **Memory Caching**: Favicon data is loaded once during middleware initialization and cached in memory for the lifetime of the application. 2. **Content Type Detection**: Uses `http.DetectContentType()` to automatically detect the content type from the favicon data. Falls back to `image/x-icon` if detection returns `application/octet-stream` or `text/plain`. 3. **File System Support**: Supports three data sources: * Direct file path using `os.ReadFile` * Embedded data using `[]byte` * File system interface (`fs.FS`) for advanced use cases 4. **Request Handling**: * Only processes requests matching the configured URL path (default: `/favicon.ico`) * Only handles GET and HEAD methods, passes other methods to next handler * Sets appropriate headers: Content-Type, Content-Length, and Cache-Control * HEAD requests return headers without body 5. **Special Behaviors**: * Empty configuration (no data) returns HTTP 204 No Content * SVG middleware supports both `/favicon.ico` and `/favicon.svg` paths * Redirect middleware returns HTTP 301 Moved Permanently ### Performance Considerations * Favicon is loaded once at startup (panic on file errors) * No disk I/O during request handling * Minimal memory allocation per request * Early path matching prevents unnecessary processing ## Best Practices * Use embedded data for portable deployments * Set long cache duration for production * Place before other middleware to avoid unnecessary processing ## Testing The favicon middleware includes comprehensive test coverage for all features: | Test Case | Description | Expected Behavior | | -------------------------------------------------------- | --------------------------------------- | ------------------------------------------------------ | | `TestNew` - serve favicon | Serves favicon from file path | Returns 200 OK with correct Content-Type (image/png) | | `TestNew` - other paths pass through | Non-favicon paths are not intercepted | Request passes to next handler, returns route response | | `TestFromData` | Serves favicon from byte data | Returns 200 OK with favicon data | | `TestWithOptions_CustomURL` - custom url serves | Serves favicon at custom URL path | Returns 200 OK at `/icon.png` | | `TestWithOptions_CustomURL` - default url does not serve | Default path not served with custom URL | Returns 404 at `/favicon.ico` | | `TestWithOptions_FS` | Serves favicon from fs.FS interface | Returns 200 OK with data from MapFS | | `TestWithOptions_MaxAge` | Custom cache control max-age | Sets `Cache-Control: public, max-age=3600` | | `TestWithOptions_HeadMethod` | HEAD request handling | Returns 200 OK with headers but no body | | `TestEmpty` | Empty middleware returns no content | Returns 204 No Content for favicon requests | | `TestRedirect` | Redirects favicon to external URL | Returns 301 with Location header to CDN | | `TestSVG` - favicon.ico serves SVG | SVG data served at .ico path | Returns 200 OK with `image/svg+xml` content type | | `TestSVG` - favicon.svg serves SVG | SVG data served at .svg path | Returns 200 OK at `/favicon.svg` | | `TestWithOptions_NoData` | No data provided in options | Returns 204 No Content | ## Related Middlewares * [static](/middlewares/static) - Static file serving * [spa](/middlewares/spa) - Single Page Application * [cache](/middlewares/cache) - Cache headers # Feature Flags Source: https://docs.go-mizu.dev/middlewares/feature Feature flag middleware for controlled feature rollouts. ## Overview The `feature` middleware provides feature flag support for controlled rollouts, A/B testing, and gradual feature deployment. It supports static flags, in-memory providers, and custom providers. ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/feature" ``` ## Quick Start ```go theme={null} app := mizu.New() // Static feature flags app.Use(feature.New(feature.Flags{ "dark_mode": &feature.Flag{Name: "dark_mode", Enabled: true}, "new_checkout": &feature.Flag{Name: "new_checkout", Enabled: false}, })) app.Get("/", func(c *mizu.Ctx) error { if feature.IsEnabled(c, "dark_mode") { return c.JSON(200, darkModeResponse) } return c.JSON(200, normalResponse) }) ``` ## Configuration | Option | Type | Default | Description | | ---------- | ---------- | ------- | ---------------- | | `Provider` | `Provider` | Static | Flag provider | | `Flags` | `Flags` | - | Static flags map | ## Flag Structure ```go theme={null} type Flag struct { Name string Enabled bool Description string Metadata map[string]any } ``` ## Examples ### Static Flags ```go theme={null} app.Use(feature.New(feature.Flags{ "feature_a": &feature.Flag{ Name: "feature_a", Enabled: true, Description: "New dashboard design", }, "feature_b": &feature.Flag{ Name: "feature_b", Enabled: false, }, })) ``` ### Check Flag in Handler ```go theme={null} app.Get("/dashboard", func(c *mizu.Ctx) error { if feature.IsEnabled(c, "new_dashboard") { return renderNewDashboard(c) } return renderOldDashboard(c) }) ``` ### Get Flag Details ```go theme={null} app.Get("/features", func(c *mizu.Ctx) error { flag := feature.Get(c, "beta_feature") if flag != nil { return c.JSON(200, map[string]any{ "name": flag.Name, "enabled": flag.Enabled, "description": flag.Description, }) } return c.JSON(404, "Feature not found") }) ``` ### Memory Provider (Dynamic) ```go theme={null} // Create a mutable provider provider := feature.NewMemoryProvider() app.Use(feature.WithProvider(provider)) // Enable/disable flags at runtime app.Post("/admin/features/:name/enable", func(c *mizu.Ctx) error { provider.Enable(c.Param("name")) return c.Text(200, "Enabled") }) app.Post("/admin/features/:name/disable", func(c *mizu.Ctx) error { provider.Disable(c.Param("name")) return c.Text(200, "Disabled") }) app.Post("/admin/features/:name/toggle", func(c *mizu.Ctx) error { provider.Toggle(c.Param("name")) return c.Text(200, "Toggled") }) ``` ### Require Flag Middleware ```go theme={null} // Route requires feature to be enabled app.Get("/beta", feature.Require("beta_access", nil), func(c *mizu.Ctx) error { return c.Text(200, "Welcome to beta!") }, ) // Custom handler when disabled app.Get("/new-feature", feature.Require("new_feature", func(c *mizu.Ctx) error { return c.JSON(403, map[string]string{ "error": "Feature not available", }) }), newFeatureHandler, ) ``` ### Require All Flags ```go theme={null} // Requires ALL flags to be enabled app.Get("/admin/super", feature.RequireAll([]string{"admin_access", "super_powers"}, nil), superAdminHandler, ) ``` ### Require Any Flag ```go theme={null} // Requires ANY flag to be enabled app.Get("/premium", feature.RequireAny([]string{"premium_user", "trial_active"}, nil), premiumHandler, ) ``` ### Custom Provider ```go theme={null} type DatabaseProvider struct { db *sql.DB } func (p *DatabaseProvider) GetFlags(c *mizu.Ctx) (feature.Flags, error) { rows, err := p.db.Query("SELECT name, enabled FROM feature_flags") if err != nil { return nil, err } defer rows.Close() flags := make(feature.Flags) for rows.Next() { var name string var enabled bool rows.Scan(&name, &enabled) flags[name] = &feature.Flag{Name: name, Enabled: enabled} } return flags, nil } app.Use(feature.WithProvider(&DatabaseProvider{db: db})) ``` ### User-Specific Flags ```go theme={null} type UserProvider struct { db *sql.DB } func (p *UserProvider) GetFlags(c *mizu.Ctx) (feature.Flags, error) { userID := c.Get("user_id").(string) // Get user-specific flags flags := make(feature.Flags) // Check if user is in beta program var inBeta bool p.db.QueryRow("SELECT beta FROM users WHERE id = ?", userID).Scan(&inBeta) flags["beta_features"] = &feature.Flag{Name: "beta_features", Enabled: inBeta} return flags, nil } ``` ### Percentage Rollout ```go theme={null} type RolloutProvider struct { percentages map[string]int } func (p *RolloutProvider) GetFlags(c *mizu.Ctx) (feature.Flags, error) { userID := c.Get("user_id").(string) hash := hashUserID(userID) flags := make(feature.Flags) for name, pct := range p.percentages { flags[name] = &feature.Flag{ Name: name, Enabled: (hash % 100) < pct, } } return flags, nil } ``` ### List All Flags ```go theme={null} app.Get("/features", func(c *mizu.Ctx) error { flags := feature.GetFlags(c) result := make([]map[string]any, 0) for name, flag := range flags { result = append(result, map[string]any{ "name": name, "enabled": flag.Enabled, }) } return c.JSON(200, result) }) ``` ### Flag with Metadata ```go theme={null} provider := feature.NewMemoryProvider() provider.SetFlag(&feature.Flag{ Name: "new_checkout", Enabled: true, Description: "New checkout flow", Metadata: map[string]any{ "rollout_percentage": 50, "target_users": "premium", "experiment_id": "exp-123", }, }) app.Use(feature.WithProvider(provider)) ``` ## API Reference ```go theme={null} func New(flags Flags) mizu.Middleware func WithProvider(provider Provider) mizu.Middleware func WithOptions(opts Options) mizu.Middleware func GetFlags(c *mizu.Ctx) Flags func Get(c *mizu.Ctx, name string) *Flag func IsEnabled(c *mizu.Ctx, name string) bool func IsDisabled(c *mizu.Ctx, name string) bool func Require(name string, handler mizu.Handler) mizu.Middleware func RequireAll(names []string, handler mizu.Handler) mizu.Middleware func RequireAny(names []string, handler mizu.Handler) mizu.Middleware // Provider functions func StaticProvider(flags Flags) Provider func NewMemoryProvider() *MemoryProvider ``` ## Memory Provider Methods ```go theme={null} func (p *MemoryProvider) Set(name string, enabled bool) func (p *MemoryProvider) SetFlag(flag *Flag) func (p *MemoryProvider) Delete(name string) func (p *MemoryProvider) Enable(name string) func (p *MemoryProvider) Disable(name string) func (p *MemoryProvider) Toggle(name string) ``` ## Technical Details ### Architecture The feature middleware uses a provider-based architecture that allows flexible feature flag management: * **Context Storage**: Feature flags are stored in the request context using a private `contextKey{}` type for isolation * **Provider Interface**: The `Provider` interface defines a single method `GetFlags(c *mizu.Ctx) (Flags, error)` enabling custom implementations * **Thread Safety**: The `MemoryProvider` uses `sync.RWMutex` for concurrent read/write access * **Immutable Returns**: Providers return copies of flag maps to prevent external modification ### Implementation Details **Flag Resolution Flow**: 1. Middleware intercepts request 2. Provider's `GetFlags()` is called with context 3. Flags are stored in request context 4. Helper functions (`IsEnabled`, `Get`, etc.) retrieve flags from context 5. On provider error, empty flag map is used (fail-safe behavior) **Static Provider**: * Wraps a `Flags` map in an immutable provider * Returns defensive copies to prevent modification * Zero overhead after initialization **Memory Provider**: * Mutable in-memory flag storage * Uses read-write mutex for concurrent access * Methods: `Enable()`, `Disable()`, `Toggle()`, `Set()`, `SetFlag()`, `Delete()` * Safe for runtime flag updates **Middleware Functions**: * `Require(name, handler)`: Guards route requiring single flag * `RequireAll(names, handler)`: Guards route requiring all flags enabled * `RequireAny(names, handler)`: Guards route requiring at least one flag enabled * Custom handlers provide fallback responses when flags are disabled ### Performance Characteristics * **Static Provider**: O(n) copy operation per request where n is flag count * **Memory Provider**: O(n) copy with RLock, write operations use Lock * **Flag Lookup**: O(1) map lookup from context * **Context Storage**: Single context value, minimal memory overhead ## Best Practices * Use descriptive flag names * Document flag purposes * Clean up old flags after rollout * Use percentage rollouts for risky features * Implement a custom provider for production * Include metadata for analytics ## Testing The feature middleware includes comprehensive test coverage for all functionality: | Test Case | Description | Expected Behavior | | --------------------------- | ----------------------------------------- | ------------------------------------------------- | | `TestNew` | Static flags with enabled/disabled states | Correctly identifies enabled and disabled flags | | `TestIsDisabled` | Check if flag is disabled | Returns true for disabled flags | | `TestGetFlags` | Retrieve all flags from context | Returns complete flag collection | | `TestGet` | Retrieve specific flag with metadata | Returns flag with description and metadata intact | | `TestRequire` (enabled) | Route protection with enabled flag | Allows request to proceed (200 OK) | | `TestRequire` (disabled) | Route protection with disabled flag | Blocks request with 404 Not Found | | `TestRequire_CustomHandler` | Custom handler when flag disabled | Executes custom handler returning 403 Forbidden | | `TestRequireAll` | Require multiple flags all enabled | Allows request when all flags enabled | | `TestRequireAny` | Require at least one flag enabled | Allows request when any flag is enabled | | `TestMemoryProvider` | In-memory provider CRUD operations | Enable, Set, and SetFlag work correctly | | `TestMemoryProvider_Toggle` | Toggle flag state at runtime | Switches flag from enabled to disabled | | `TestMemoryProvider_Delete` | Delete flag from provider | Flag no longer exists after deletion | ## Related Middlewares * [multitenancy](/middlewares/multitenancy) - Multi-tenant support * [version](/middlewares/version) - API versioning # Filter Source: https://docs.go-mizu.dev/middlewares/filter Request filtering middleware for conditional routing. ## Overview The `filter` middleware provides request filtering capabilities, allowing or blocking requests based on custom conditions. Use it when you need: * Block specific requests * Conditional access * Request validation ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/filter" ``` ## Quick Start ```go theme={null} app := mizu.New() // Block requests without API key app.Use(filter.New(func(c *mizu.Ctx) bool { return c.Request().Header.Get("X-API-Key") != "" })) ``` ## Configuration ### Options | Option | Type | Default | Description | | -------------- | ----------------------- | -------- | ---------------- | | `Allow` | `func(*mizu.Ctx) bool` | Required | Allow function | | `ErrorHandler` | `func(*mizu.Ctx) error` | 403 | Custom rejection | ## Examples ### Allow Function ```go theme={null} app.Use(filter.New(func(c *mizu.Ctx) bool { // Allow if condition is true return isAllowed(c) })) ``` ### Block by User-Agent ```go theme={null} app.Use(filter.New(func(c *mizu.Ctx) bool { ua := c.Request().UserAgent() return !strings.Contains(ua, "BadBot") })) ``` ### Custom Error ```go theme={null} app.Use(filter.WithOptions(filter.Options{ Allow: allowFunc, ErrorHandler: func(c *mizu.Ctx) error { return c.JSON(403, map[string]string{ "error": "Access denied", }) }, })) ``` ### Combine Conditions ```go theme={null} app.Use(filter.New(func(c *mizu.Ctx) bool { hasKey := c.Request().Header.Get("X-API-Key") != "" isGet := c.Request().Method == http.MethodGet return hasKey || isGet })) ``` ## API Reference ### Functions ```go theme={null} // New creates filter middleware func New(allow func(*mizu.Ctx) bool) mizu.Middleware // WithOptions creates with configuration func WithOptions(opts Options) mizu.Middleware // Block creates blocking filter func Block(block func(*mizu.Ctx) bool) mizu.Middleware ``` ## Technical Details The filter middleware provides comprehensive request filtering through multiple criteria: ### Pattern Matching The middleware uses glob-to-regex conversion for flexible pattern matching: * `*` - Matches any characters except path separators (e.g., `/api/*` matches `/api/users` but not `/api/users/1`) * `**` - Matches any characters including path separators (e.g., `/admin/**` matches `/admin/deep/nested/path`) * `?` - Matches exactly one character Special regex characters (`.`, `+`, `^`, `$`, `|`, etc.) are automatically escaped. ### Filter Evaluation Order Filters are evaluated in the following order: 1. **HTTP Methods** - Check if method is in AllowedMethods (if configured) 2. **Blocked Hosts** - Check if host is in BlockedHosts (takes precedence over allowed) 3. **Allowed Hosts** - Check if host is in AllowedHosts (if configured) 4. **Blocked Paths** - Check if path matches BlockedPaths patterns (takes precedence over allowed) 5. **Allowed Paths** - Check if path matches AllowedPaths patterns (if configured) 6. **Blocked User Agents** - Check if UA matches BlockedUserAgents patterns (takes precedence over allowed) 7. **Allowed User Agents** - Check if UA matches AllowedUserAgents patterns (if configured) 8. **Custom Filter** - Execute CustomFilter function if provided If any check fails, the request is blocked and the OnBlock handler is called (or returns 403 Forbidden by default). ### Host Normalization Host names are normalized by: * Converting to lowercase for case-insensitive matching * Stripping port numbers (e.g., `example.com:8080` becomes `example.com`) ### Performance Optimization * HTTP methods are stored in a map for O(1) lookup * Hosts are stored in maps for O(1) lookup * Glob patterns are compiled to regex once during initialization * All patterns are pre-compiled to avoid runtime compilation overhead ## Best Practices * Keep filter logic simple * Log blocked requests * Use descriptive error messages * Combine with rate limiting ## Testing The filter middleware includes comprehensive test coverage for all filtering scenarios: | Test Case | Description | Expected Behavior | | ----------------------------- | -------------------------------------------- | -------------------------------------------------- | | TestNew | Creates filter with default options | Allows all requests to pass through | | TestMethods (allowed) | Filters by HTTP method - allowed method | Returns 200 OK for GET request | | TestMethods (blocked) | Filters by HTTP method - blocked method | Returns 403 Forbidden for DELETE request | | TestBlockPaths (allowed) | Blocks specific path patterns - allowed path | Returns 200 OK for root path | | TestBlockPaths (admin) | Blocks admin paths with `*` wildcard | Returns 403 Forbidden for /admin/users | | TestBlockPaths (deep) | Blocks deep paths with `**` wildcard | Returns 403 Forbidden for /internal/deep/path | | TestPaths (allowed) | Allows only specific path patterns | Returns 200 OK for /api/users | | TestPaths (blocked) | Blocks paths not in allow list | Returns 403 Forbidden for /secret | | TestHosts (allowed) | Filters by hostname - allowed host | Returns 200 OK for example.com | | TestHosts (blocked) | Filters by hostname - blocked host | Returns 403 Forbidden for evil.com | | TestBlockUserAgents (allowed) | Blocks specific user agents - allowed UA | Returns 200 OK for Mozilla/5.0 | | TestBlockUserAgents (curl) | Blocks curl user agent pattern | Returns 403 Forbidden for curl/7.68.0 | | TestBlockUserAgents (bot) | Blocks bot user agent pattern | Returns 403 Forbidden for Googlebot/2.1 | | TestCustomFilter (passes) | Custom filter function returns true | Returns 200 OK when X-Secret header is valid | | TestCustomFilter (fails) | Custom filter function returns false | Returns 403 Forbidden when X-Secret header missing | | TestCustomOnBlock | Custom OnBlock handler | Returns 405 with JSON error message | | TestBlockedHosts (allowed) | Blocks specific hosts - allowed host | Returns 200 OK for example.com | | TestBlockedHosts (blocked) | Blocks specific hosts - blocked host | Returns 403 Forbidden for spam.com | | TestGlobToRegex (/api/\*) | Glob pattern with single wildcard | Matches /api/users but not /api/users/1 | | TestGlobToRegex (/api/\*\*) | Glob pattern with double wildcard | Matches /api/users/1 (nested paths) | | TestGlobToRegex (\*.txt) | File extension pattern | Matches file.txt but not file.json | | TestGlobToRegex (/a?c) | Question mark single char match | Matches /abc | | TestGlobToRegex (/a.b) | Dot character escaping | Matches literal /a.b | ## Related Middlewares * [ipfilter](/middlewares/ipfilter) - IP-based filtering * [keyauth](/middlewares/keyauth) - API key auth * [rbac](/middlewares/rbac) - Role-based access # Fingerprint Source: https://docs.go-mizu.dev/middlewares/fingerprint Request fingerprinting middleware for client identification. ## Overview The `fingerprint` middleware generates unique fingerprints for incoming requests based on headers, IP addresses, and other request attributes. Useful for bot detection, analytics, and rate limiting. ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/fingerprint" ``` ## Quick Start ```go theme={null} app := mizu.New() app.Use(fingerprint.New()) app.Get("/", func(c *mizu.Ctx) error { hash := fingerprint.Hash(c) return c.JSON(200, map[string]string{"fingerprint": hash}) }) ``` ## Configuration | Option | Type | Default | Description | | --------------- | ----------------------------------- | -------------- | -------------------- | | `Headers` | `[]string` | Common headers | Headers to include | | `IncludeIP` | `bool` | `true` | Include client IP | | `IncludeMethod` | `bool` | `false` | Include HTTP method | | `IncludePath` | `bool` | `false` | Include request path | | `Custom` | `func(*mizu.Ctx) map[string]string` | - | Custom components | ## Default Headers ```go theme={null} []string{ "User-Agent", "Accept", "Accept-Language", "Accept-Encoding", "Connection", "Sec-Ch-Ua", "Sec-Ch-Ua-Mobile", "Sec-Ch-Ua-Platform", } ``` ## Examples ### Basic Usage ```go theme={null} app.Use(fingerprint.New()) app.Get("/", func(c *mizu.Ctx) error { info := fingerprint.Get(c) return c.JSON(200, map[string]any{ "hash": info.Hash, "components": info.Components, }) }) ``` ### With IP Address ```go theme={null} app.Use(fingerprint.WithIP()) // Fingerprint includes client IP ``` ### Headers Only ```go theme={null} app.Use(fingerprint.HeadersOnly( "User-Agent", "Accept-Language", )) ``` ### Full Fingerprint ```go theme={null} app.Use(fingerprint.Full()) // Includes IP, method, path, and all default headers ``` ### Custom Headers ```go theme={null} app.Use(fingerprint.WithOptions(fingerprint.Options{ Headers: []string{ "User-Agent", "Accept", "Accept-Language", "Accept-Encoding", "X-Custom-Header", }, })) ``` ### Include Request Path ```go theme={null} app.Use(fingerprint.WithOptions(fingerprint.Options{ IncludeIP: true, IncludeMethod: true, IncludePath: true, })) ``` ### Custom Components ```go theme={null} app.Use(fingerprint.WithOptions(fingerprint.Options{ Custom: func(c *mizu.Ctx) map[string]string { return map[string]string{ "session_id": c.Cookie("session_id"), "user_id": getUserID(c), } }, })) ``` ### Rate Limiting by Fingerprint ```go theme={null} app.Use(fingerprint.New()) var requestCounts = make(map[string]int) var mu sync.Mutex app.Use(func(next mizu.Handler) mizu.Handler { return func(c *mizu.Ctx) error { hash := fingerprint.Hash(c) mu.Lock() requestCounts[hash]++ count := requestCounts[hash] mu.Unlock() if count > 100 { return c.Text(429, "Rate limit exceeded") } return next(c) } }) ``` ### Bot Detection ```go theme={null} app.Use(fingerprint.New()) // Known bot fingerprints var knownBots = map[string]bool{ "abc123...": true, "def456...": true, } app.Use(func(next mizu.Handler) mizu.Handler { return func(c *mizu.Ctx) error { hash := fingerprint.Hash(c) if knownBots[hash] { return c.Text(403, "Bot detected") } return next(c) } }) ``` ### Analytics Tracking ```go theme={null} app.Use(fingerprint.New()) app.Use(func(next mizu.Handler) mizu.Handler { return func(c *mizu.Ctx) error { info := fingerprint.Get(c) // Track unique visitors analytics.TrackVisitor(info.Hash, map[string]any{ "path": c.Request().URL.Path, "components": info.Components, }) return next(c) } }) ``` ### Session Correlation ```go theme={null} app.Use(fingerprint.WithOptions(fingerprint.Options{ IncludeIP: true, Custom: func(c *mizu.Ctx) map[string]string { return map[string]string{ "session": c.Cookie("session_id"), } }, })) app.Get("/", func(c *mizu.Ctx) error { hash := fingerprint.Hash(c) // Correlate with existing sessions existingSession := findSession(hash) if existingSession != nil { return c.JSON(200, existingSession) } // Create new session return c.JSON(200, createSession(hash)) }) ``` ### Debugging Fingerprint ```go theme={null} app.Get("/debug/fingerprint", func(c *mizu.Ctx) error { info := fingerprint.Get(c) return c.JSON(200, map[string]any{ "hash": info.Hash, "components": info.Components, }) }) ``` Response: ```json theme={null} { "hash": "a1b2c3d4e5f6...", "components": { "User-Agent": "Mozilla/5.0...", "Accept": "text/html,application/xhtml+xml...", "Accept-Language": "en-US,en;q=0.9", "Accept-Encoding": "gzip, deflate, br", "IP": "192.168.1.1" } } ``` ### Fraud Detection ```go theme={null} app.Use(fingerprint.Full()) app.Post("/checkout", func(c *mizu.Ctx) error { hash := fingerprint.Hash(c) userID := c.Get("user_id").(string) // Check if fingerprint is associated with fraud if isFraudulent(hash) { return c.JSON(403, map[string]string{ "error": "Transaction blocked", }) } // Check for fingerprint/account mismatch if !matchesUser(hash, userID) { flagForReview(userID, hash) } return processCheckout(c) }) ``` ## Info Structure ```go theme={null} type Info struct { Hash string // SHA256 hash of components Components map[string]string // Individual fingerprint components } ``` ## API Reference ```go theme={null} func New() mizu.Middleware func WithOptions(opts Options) mizu.Middleware func HeadersOnly(headers ...string) mizu.Middleware func WithIP() mizu.Middleware func Full() mizu.Middleware func Get(c *mizu.Ctx) *Info func Hash(c *mizu.Ctx) string ``` ## Hash Algorithm The fingerprint hash is generated by: 1. Collecting all components (headers, IP, etc.) 2. Sorting component keys alphabetically 3. Concatenating as `key:value|key:value|...` 4. Computing SHA256 hash 5. Returning hex-encoded string ## Technical Details ### Implementation Architecture The fingerprint middleware uses a context-based storage mechanism to preserve fingerprint information throughout the request lifecycle. The implementation follows these key design principles: **Context Storage** * Uses a private `contextKey` struct type to avoid collisions with other middleware * Stores fingerprint information in the request context as an `*Info` pointer * Information persists across the entire middleware chain **Hash Generation Algorithm** * Deterministic hashing through alphabetical key sorting * Uses SHA256 cryptographic hash function * Produces 64-character hexadecimal strings * Format: `key1:value1|key2:value2|...|keyN:valueN` **Component Collection** The middleware collects fingerprint components in the following order: 1. Headers: Iterates through configured header list 2. IP Address: Extracted via `getClientIP()` helper 3. HTTP Method: Request method if enabled 4. Request Path: URL path if enabled 5. Custom Components: User-defined function results **IP Address Detection** The `getClientIP()` function implements a priority-based IP extraction: 1. First checks `X-Forwarded-For` header (takes first IP in comma-separated list) 2. Falls back to `X-Real-IP` header 3. Finally uses `RemoteAddr` from the request ### Performance Considerations * Minimal memory allocation through pre-sized maps * String builder for efficient concatenation * Single-pass component collection * Lazy evaluation: hash only computed once during middleware execution ### Thread Safety The middleware is thread-safe as each request receives its own context and Info struct. No shared state exists between requests. ## Best Practices * Use minimal headers for privacy compliance * Don't rely solely on fingerprints for authentication * Store fingerprints hashed, not raw * Combine with other signals for bot detection * Consider GDPR implications when storing fingerprints * Use for analytics and fraud detection, not tracking ## Testing The fingerprint middleware includes comprehensive test coverage for all configuration options and edge cases: | Test Case | Description | Expected Behavior | | ------------------------------- | --------------------------------- | -------------------------------------------------------------------------- | | `TestNew` | Default middleware initialization | Generates fingerprint hash from default headers (User-Agent, Accept, etc.) | | `TestWithOptions_IncludeIP` | IP address inclusion | Captures client IP from RemoteAddr and includes in components | | `TestWithOptions_IncludeMethod` | HTTP method inclusion | Includes HTTP method (GET, POST, etc.) in fingerprint components | | `TestWithOptions_IncludePath` | Request path inclusion | Captures and includes the full request path in components | | `TestWithOptions_Custom` | Custom component function | Executes custom function and merges returned map into components | | `TestHash` | Hash function retrieval | Returns 64-character SHA256 hex string via Hash() helper | | `TestConsistentHash` | Hash consistency | Identical requests produce identical hash values | | `TestHeadersOnly` | Custom header filtering | Only includes specified headers, excludes others (e.g., User-Agent) | | `TestWithIP` | WithIP() helper function | Enables IP inclusion using convenience function | | `TestFull` | Full() comprehensive mode | Includes IP, Method, Path, and all default headers | | `TestXForwardedFor` | X-Forwarded-For header parsing | Extracts first IP from comma-separated list in X-Forwarded-For header | ### Test Coverage All test cases validate: * Correct component extraction and storage * Proper context propagation * Hash generation and consistency * Configuration option handling * Edge cases (empty headers, missing values, etc.) ## Related Middlewares * [realip](/middlewares/realip) - Client IP extraction * [ratelimit](/middlewares/ratelimit) - Rate limiting * [ipfilter](/middlewares/ipfilter) - IP filtering # Forwarded Source: https://docs.go-mizu.dev/middlewares/forwarded X-Forwarded-* header parsing middleware for proxy environments. ## Overview The `forwarded` middleware parses X-Forwarded-\* headers set by load balancers and proxies, extracting client IP, protocol, host, and other forwarded information. ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/forwarded" ``` ## Quick Start ```go theme={null} app := mizu.New() app.Use(forwarded.New()) ``` ## Configuration | Option | Type | Default | Description | | ---------------- | ---------- | ------- | ------------------------------- | | `TrustProxy` | `bool` | `true` | Trust forwarded headers | | `TrustedProxies` | `[]string` | - | List of trusted proxy IPs/CIDRs | ## Headers Parsed | Header | Information | | -------------------- | --------------------- | | `X-Forwarded-For` | Client IP chain | | `X-Forwarded-Host` | Original host | | `X-Forwarded-Proto` | Protocol (http/https) | | `X-Forwarded-Port` | Original port | | `X-Forwarded-Prefix` | Path prefix | | `Forwarded` | RFC 7239 header | ## Examples ### Basic Usage ```go theme={null} app.Use(forwarded.New()) app.Get("/", func(c *mizu.Ctx) error { info := forwarded.Get(c) return c.JSON(200, map[string]any{ "clientIP": info.ClientIP, "proto": info.Proto, "host": info.Host, }) }) ``` ### Trusted Proxies Only ```go theme={null} app.Use(forwarded.WithOptions(forwarded.Options{ TrustProxy: true, TrustedProxies: []string{ "10.0.0.0/8", "192.168.1.0/24", }, })) ``` ### Helper Functions ```go theme={null} // Get client IP ip := forwarded.ClientIP(c) // Get protocol proto := forwarded.Proto(c) // Get host host := forwarded.Host(c) ``` ## Info Struct ```go theme={null} type Info struct { For string // X-Forwarded-For value Host string // Original host Proto string // http or https Port string // Original port Prefix string // Path prefix ClientIP net.IP // Parsed client IP } ``` ## API Reference ```go theme={null} func New() mizu.Middleware func WithOptions(opts Options) mizu.Middleware func Get(c *mizu.Ctx) *Info func FromContext(c *mizu.Ctx) *Info func ClientIP(c *mizu.Ctx) net.IP func Proto(c *mizu.Ctx) string func Host(c *mizu.Ctx) string ``` ## Technical Details ### Header Processing Priority The middleware processes headers in the following order: 1. **X-Forwarded-For**: Extracts the client IP from the first entry in the comma-separated list 2. **X-Forwarded-Host**: Sets the original host requested by the client 3. **X-Forwarded-Proto**: Determines if the original request was HTTP or HTTPS 4. **X-Forwarded-Port**: Captures the original port number 5. **X-Forwarded-Prefix**: Records any path prefix added by reverse proxies 6. **Forwarded** (RFC 7239): Parses the standardized Forwarded header, which can override previous values ### Trusted Proxy Validation When `TrustedProxies` is configured: * IP addresses are automatically converted to CIDR notation (single IPs get `/32`) * The middleware checks if the remote address matches any trusted CIDR ranges * If no match is found, all X-Forwarded-\* headers are ignored and the remote address is used ### IP Parsing The middleware handles various IP address formats: * Standard IPv4 addresses (e.g., `192.168.1.1`) * IPv6 addresses in RFC 7239 format with brackets (e.g., `[2001:db8::1]`) * Addresses with port numbers (automatically stripped) ### Context Storage Forwarded information is stored in the request context using a private context key, making it accessible throughout the request lifecycle via helper functions. ## Best Practices 1. **Always specify trusted proxies** in production environments to prevent header spoofing 2. **Use CIDR notation** for trusted proxy ranges to cover entire subnets 3. **Validate client IPs** after extraction if using them for security decisions 4. **Prefer RFC 7239 Forwarded header** when possible for better standardization 5. **Disable TrustProxy** when not behind a reverse proxy to avoid security issues ## Testing The middleware includes comprehensive test coverage for various scenarios: | Test Case | Description | Expected Behavior | | -------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------- | | `TestNew` | Basic middleware functionality with X-Forwarded headers | Correctly parses X-Forwarded-For, X-Forwarded-Proto, and X-Forwarded-Host | | `TestWithOptions_TrustedProxies` (trusted proxy) | Request from a trusted proxy IP | Honors X-Forwarded-For header and extracts client IP | | `TestWithOptions_TrustedProxies` (untrusted proxy) | Request from an untrusted proxy IP | Ignores X-Forwarded-For and uses RemoteAddr instead | | `TestWithOptions_XForwardedForMultiple` | Multiple IPs in X-Forwarded-For chain | Extracts the first (original client) IP from the chain | | `TestWithOptions_ForwardedHeader` | RFC 7239 Forwarded header parsing | Correctly parses for, proto, and host from Forwarded header | | `TestFromContext` | Alias function for Get() | FromContext returns same result as Get() | | `TestClientIP` | ClientIP helper function | Returns parsed client IP as net.IP | | `TestProto` (with header) | Protocol extraction with X-Forwarded-Proto | Returns "https" when header is present | | `TestProto` (without header) | Protocol extraction without header | Returns default "http" when no header present | | `TestHost` | Host extraction from X-Forwarded-Host | Returns forwarded host value | | `TestWithOptions_DisableTrustProxy` | TrustProxy disabled | Ignores all X-Forwarded headers and uses RemoteAddr | ## Security Note Only enable `TrustProxy` when behind a trusted proxy. Untrusted clients can spoof these headers. ## Related Middlewares * [realip](/middlewares/realip) - Simple IP extraction * [proxy](/middlewares/proxy) - Reverse proxy # GraphQL Source: https://docs.go-mizu.dev/middlewares/graphql GraphQL query validation middleware for securing GraphQL APIs. ## Overview The `graphql` middleware provides GraphQL query validation and security features including depth limiting, complexity analysis, introspection control, and field blocking. Use it when you need: * Query depth and complexity validation * Introspection query control * Field-level access control * Operation allowlisting * Protection against malicious queries ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/graphql" ``` ## Quick Start ```go theme={null} app := mizu.New() // Use default validation (max depth: 10, max complexity: 100) app.Use(graphql.New()) app.Post("/graphql", yourGraphQLHandler) ``` ## Configuration ### Options | Option | Type | Default | Description | | ---------------------- | ------------------------------ | ------- | ------------------------------------ | | `MaxDepth` | `int` | `10` | Maximum query depth allowed | | `MaxComplexity` | `int` | `100` | Maximum query complexity allowed | | `DisableIntrospection` | `bool` | `false` | Disable introspection queries | | `AllowedOperations` | `[]string` | `nil` | Whitelist of allowed operation names | | `BlockedFields` | `[]string` | `nil` | List of fields to block | | `ErrorHandler` | `func(*mizu.Ctx, error) error` | `nil` | Custom error handler | ## Examples ### Basic Setup ```go theme={null} // Use with default settings app.Use(graphql.New()) app.Post("/graphql", yourGraphQLHandler) ``` ### Custom Depth and Complexity Limits ```go theme={null} app.Use(graphql.WithOptions(graphql.Options{ MaxDepth: 5, MaxComplexity: 50, })) ``` ### Production Configuration ```go theme={null} // Production-ready settings with introspection disabled app.Use(graphql.Production()) ``` ### Disable Introspection ```go theme={null} // Block __schema and __type queries app.Use(graphql.NoIntrospection()) ``` ### Block Sensitive Fields ```go theme={null} // Prevent access to specific fields app.Use(graphql.BlockFields("password", "secret", "apiKey")) ``` ### Allowlist Operations ```go theme={null} app.Use(graphql.WithOptions(graphql.Options{ AllowedOperations: []string{"GetUser", "ListPosts", "CreateComment"}, })) ``` ### Custom Error Handler ```go theme={null} app.Use(graphql.WithOptions(graphql.Options{ MaxDepth: 10, ErrorHandler: func(c *mizu.Ctx, err error) error { return c.JSON(http.StatusForbidden, map[string]any{ "errors": []map[string]string{ {"message": "Query validation failed", "detail": err.Error()}, }, }) }, })) ``` ## API Reference ### Functions ```go theme={null} // New creates GraphQL validation middleware with default options func New() mizu.Middleware // WithOptions creates GraphQL validation middleware with custom options func WithOptions(opts Options) mizu.Middleware // MaxDepth creates middleware with a specific max depth func MaxDepth(depth int) mizu.Middleware // MaxComplexity creates middleware with a specific max complexity func MaxComplexity(complexity int) mizu.Middleware // NoIntrospection creates middleware that disables introspection func NoIntrospection() mizu.Middleware // Production creates middleware with production-ready settings func Production() mizu.Middleware // BlockFields creates middleware that blocks specific fields func BlockFields(fields ...string) mizu.Middleware ``` ### Error Types ```go theme={null} const ( ErrQueryTooDeep = "query exceeds maximum depth" ErrQueryTooComplex = "query exceeds maximum complexity" ErrIntrospectionDisabled = "introspection queries are disabled" ErrOperationNotAllowed = "operation not allowed" ErrBlockedField = "query contains blocked field" ) ``` ## Technical Details ### Query Validation Process The middleware validates GraphQL queries through the following process: 1. **Request Filtering**: Only processes POST requests with `application/json` content type 2. **Query Parsing**: Parses the request body into a `Query` structure containing: * `query`: The GraphQL query string * `operationName`: Named operation identifier * `variables`: Query variables map 3. **Validation Chain**: Applies validations in order: * Introspection check (if enabled) * Operation allowlist check (if configured) * Depth calculation and validation * Complexity calculation and validation * Blocked fields check (if configured) 4. **Body Restoration**: Restores the request body for downstream handlers ### Depth Calculation Query depth is calculated by counting the maximum nesting level of braces `{}`: * Tracks current depth using a counter * Increments on `{`, decrements on `}` * Records the maximum depth reached Example: ```graphql theme={null} { users { posts { id } } } // Depth: 3 ``` ### Complexity Calculation Query complexity is estimated using: * Count of field selections with braces: `\w+\s*(?:\([^)]*\))?\s*{` * Plus total count of opening braces `{` This provides a simple heuristic for query cost. ### Introspection Detection Introspection queries are detected using regex pattern matching for: * `__schema` - Schema introspection * `__type` - Type introspection ### Implementation Notes * The middleware preserves the request body after validation for downstream handlers * Non-POST requests and non-JSON requests bypass validation * Failed JSON parsing allows the request to pass through (handled by GraphQL server) * Default limits provide reasonable protection for most applications ## Best Practices * Disable introspection in production * Set appropriate depth limits based on your schema * Use complexity limits to prevent resource exhaustion * Block sensitive fields at the middleware level * Implement custom error handlers for better user experience * Combine with rate limiting for additional protection ## Testing ### Test Coverage | Test Case | Description | Expected Behavior | | -------------------------------------- | ----------------------------- | ------------------------------------------------------------------------ | | `TestNew` | Default middleware creation | Accepts valid queries with default limits | | `TestWithOptions_MaxDepth` | Query depth validation | Rejects queries exceeding max depth (4 > 2) | | `TestWithOptions_MaxComplexity` | Query complexity validation | Rejects queries exceeding max complexity | | `TestWithOptions_DisableIntrospection` | Introspection blocking | Blocks `__schema` and `__type` queries | | `TestWithOptions_BlockedFields` | Field blocking | Rejects queries containing blocked fields (password, secret) | | `TestSkipNonPOST` | GET request handling | Allows non-POST requests to pass through | | `TestMaxDepth` | MaxDepth helper function | Accepts queries within depth limit (2 ≤ 3) | | `TestMaxComplexity` | MaxComplexity helper function | Accepts queries within complexity limit | | `TestNoIntrospection` | NoIntrospection helper | Blocks `__type` introspection queries | | `TestProduction` | Production configuration | Applies production settings (depth:10, complexity:100, no introspection) | | `TestBlockFields` | BlockFields helper | Blocks specified mutation fields (deleteMutation, adminAccess) | | `TestCustomErrorHandler` | Custom error handling | Uses custom error handler with HTTP 403 status | ## Security Considerations ### Query Depth Attacks Deep nested queries can cause exponential computation: ```graphql theme={null} { users { friends { friends { friends { ... } } } } } ``` Mitigation: Set `MaxDepth` based on your schema's maximum legitimate nesting. ### Query Complexity Attacks Complex queries can exhaust server resources: ```graphql theme={null} { users { posts { comments { author { posts { comments { ... } } } } } } } ``` Mitigation: Use `MaxComplexity` to limit total query cost. ### Introspection Leaks Introspection exposes your entire schema structure: ```graphql theme={null} { __schema { types { name fields { name } } } } ``` Mitigation: Disable introspection in production with `NoIntrospection()` or `Production()`. ### Sensitive Field Access Prevent access to sensitive fields: ```graphql theme={null} { users { password ssn apiKey } } ``` Mitigation: Use `BlockFields()` to prevent access to sensitive data. ## Related Middlewares * [jsonrpc](/middlewares/jsonrpc) - JSON-RPC support * [ratelimit](/middlewares/ratelimit) - Rate limiting * [timeout](/middlewares/timeout) - Request timeout # H2C Source: https://docs.go-mizu.dev/middlewares/h2c HTTP/2 cleartext middleware for unencrypted HTTP/2 connections. ## Overview The `h2c` middleware enables HTTP/2 cleartext (h2c) connections, allowing HTTP/2 over unencrypted connections for development and internal services. Use it when you need: * HTTP/2 without TLS * Development HTTP/2 testing * Internal service communication ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/h2c" ``` ## Quick Start ```go theme={null} app := mizu.New() // Enable h2c server := h2c.Server(app) server.ListenAndServe(":8080") ``` ## Examples ### Basic H2C Server ```go theme={null} app := mizu.New() app.Get("/", func(c *mizu.Ctx) error { return c.Text(200, "HTTP/2 Cleartext!") }) // Wrap with h2c server := h2c.Server(app) log.Fatal(server.ListenAndServe(":8080")) ``` ### With Custom Server ```go theme={null} app := mizu.New() server := &http.Server{ Addr: ":8080", Handler: h2c.Handler(app), } server.ListenAndServe() ``` ## API Reference ### Functions ```go theme={null} // Server creates h2c-enabled server func Server(handler http.Handler) *http.Server // Handler wraps handler for h2c support func Handler(handler http.Handler) http.Handler ``` ## Technical Details ### Implementation Overview The h2c middleware provides HTTP/2 cleartext support through two main mechanisms: 1. **HTTP/2 Prior Knowledge**: Direct HTTP/2 connections where the client knows in advance that the server supports HTTP/2. Detected by checking `ProtoMajor == 2`. 2. **HTTP/2 Upgrade**: HTTP/1.1 upgrade mechanism using the h2c upgrade protocol (RFC 7540, Section 3.2). ### Core Components #### Options Configuration * `AllowUpgrade` (default: true): Enables HTTP/1.1 to HTTP/2 upgrade requests * `AllowDirect` (default: true): Enables direct HTTP/2 connections with prior knowledge * `OnUpgrade`: Optional callback function triggered when an upgrade occurs #### Context Information The middleware stores connection information in the request context: ```go theme={null} type Info struct { IsHTTP2 bool // True if request uses HTTP/2 Upgraded bool // True if upgraded from HTTP/1.1 Direct bool // True if direct HTTP/2 connection } ``` #### Upgrade Detection The middleware validates h2c upgrade requests by checking: * `Connection` header contains "Upgrade" token * `Upgrade` header equals "h2c" (case-insensitive) * `HTTP2-Settings` header is present #### Connection Handling * Uses `http.Hijacker` interface to take over the TCP connection * Sends `101 Switching Protocols` response * Manages connection lifecycle with buffered reading via `BufferedConn` #### Helper Functions * `GetInfo(c *mizu.Ctx)`: Retrieves h2c information from context * `IsHTTP2(c *mizu.Ctx)`: Quick check if request is HTTP/2 * `ParseSettings(r *http.Request)`: Decodes base64-encoded HTTP2-Settings header * `IsHTTP2Preface(data []byte)`: Validates HTTP/2 connection preface bytes ## Security Warning H2C transmits data unencrypted. Only use for: * Development environments * Internal services behind TLS termination * Testing scenarios ## Best Practices * Never use h2c in production over public networks * Use behind TLS-terminating load balancer * Test with `curl --http2-prior-knowledge` ## Testing The h2c middleware includes comprehensive test coverage for all functionality: | Test Case | Description | Expected Behavior | | ------------------------------------------ | -------------------------- | --------------------------------------------------------------------------- | | TestNew | Basic middleware creation | Middleware initializes with default options and processes HTTP/1.1 requests | | TestDetectHTTP1 | HTTP/1.1 detection | Correctly identifies HTTP/1.x requests, sets IsHTTP2 to false | | TestIsH2CUpgrade | Upgrade request validation | Validates h2c upgrade headers (Connection, Upgrade, HTTP2-Settings) | | TestContainsToken | Header token parsing | Correctly parses comma-separated header values | | TestGetInfo | Context info retrieval | Retrieves h2c info from request context | | TestIsHTTP2 | HTTP/2 detection helper | Returns correct boolean for HTTP/2 detection | | TestParseSettings | HTTP2-Settings parsing | Decodes base64-encoded settings header | | TestParseSettingsEmpty | Empty settings handling | Returns nil for missing HTTP2-Settings header | | TestParseSettingsInvalid | Invalid settings handling | Returns error for malformed base64 | | TestOnUpgrade | Upgrade callback | Executes OnUpgrade callback when upgrade occurs | | TestServerHandler | ServerHandler creation | Creates handler with h2c support | | TestWrap | Handler wrapping | Wraps standard http.Handler with h2c support | | TestIsHTTP2Preface | Preface detection | Validates HTTP/2 connection preface bytes | | TestDetect | Detection-only mode | Detects h2c without handling upgrade | | TestWithOptionsDisabled | Disabled options | Respects AllowUpgrade and AllowDirect settings | | TestBufferedConn | Buffered connection | Handles buffered reading with Peek and Read | | TestGetInfo\_NoContext | Missing context info | Returns empty Info when middleware not used | | TestDetect\_WithH2CUpgrade | Upgrade detection | Correctly detects and marks upgrade requests | | TestServerHandler\_WithH2CUpgrade | Handler upgrade flow | Processes h2c upgrade in ServerHandler | | TestServerHandler\_UpgradeDisabled | Disabled upgrade handling | Falls back to handler when upgrade disabled | | TestInfo\_Fields | Info struct fields | Validates Info struct field values | | TestConnectionPreface | Preface constant | Verifies correct HTTP/2 preface constant | | TestWithOptions\_HTTP2PriorKnowledge | Prior knowledge handling | Detects direct HTTP/2 connections | | TestWithOptions\_H2CUpgradeWithCallback | Upgrade with callback | Executes callback during upgrade | | TestHandleUpgrade\_NonHijackable | Non-hijackable fallback | Falls back to HTTP/1.1 when hijack unsupported | | TestServerHandler\_HTTP2PriorKnowledge | Prior knowledge in handler | Handles HTTP/2 prior knowledge in ServerHandler | | TestServerHandler\_H2CUpgrade\_WithHijack | Successful hijack | Successfully hijacks connection for upgrade | | TestServerHandler\_H2CUpgrade\_HijackError | Hijack error handling | Falls back to handler on hijack error | | TestServerHandler\_NonHijackable | Handler fallback | Falls back when ResponseWriter not hijackable | | TestDetect\_HTTP2PriorKnowledge | Prior knowledge detection | Detects HTTP/2 prior knowledge in Detect mode | | TestBufferedConn\_Underlying | Underlying connection ops | Validates Write, Close operations on BufferedConn | | TestWithOptions\_H2CUpgrade\_HijackError | Middleware hijack error | Handles hijack errors in middleware | | TestBufferedConn\_NetConnInterface | net.Conn interface | Validates complete net.Conn interface implementation | ## Related Middlewares * [secure](/middlewares/secure) - HTTPS enforcement * [proxy](/middlewares/proxy) - Reverse proxy # Header Source: https://docs.go-mizu.dev/middlewares/header Request and response header manipulation middleware. ## Overview The `header` middleware provides flexible request and response header manipulation. Add, modify, or remove headers for security, caching, or custom requirements. ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/header" ``` ## Quick Start ```go theme={null} app := mizu.New() // Set response header app.Use(header.Set("X-Powered-By", "Mizu")) ``` ## Functions ### Response Headers | Function | Description | | ----------------- | ----------------------------- | | `Set(key, value)` | Set response header | | `New(headers)` | Set multiple response headers | | `Remove(keys...)` | Remove response headers | ### Request Headers | Function | Description | | ------------------------ | ---------------------- | | `SetRequest(key, value)` | Set request header | | `RemoveRequest(keys...)` | Remove request headers | ### Security Headers | Function | Header | | ----------------------------------- | --------------------------- | | `XSSProtection()` | X-XSS-Protection | | `NoSniff()` | X-Content-Type-Options | | `FrameDeny()` | X-Frame-Options: DENY | | `FrameSameOrigin()` | X-Frame-Options: SAMEORIGIN | | `HSTS(maxAge, subdomains, preload)` | Strict-Transport-Security | | `CSP(policy)` | Content-Security-Policy | | `ReferrerPolicy(policy)` | Referrer-Policy | | `PermissionsPolicy(policy)` | Permissions-Policy | ### Content Headers | Function | Content-Type | | -------- | ---------------- | | `JSON()` | application/json | | `HTML()` | text/html | | `Text()` | text/plain | | `XML()` | application/xml | ## Examples ### Set Single Header ```go theme={null} app.Use(header.Set("X-Custom-Header", "value")) ``` ### Set Multiple Headers ```go theme={null} app.Use(header.New(map[string]string{ "X-App-Name": "MyApp", "X-App-Version": "1.0.0", })) ``` ### Remove Headers ```go theme={null} // Remove sensitive headers app.Use(header.Remove("Server", "X-Powered-By")) ``` ### Security Headers ```go theme={null} app.Use(header.XSSProtection()) app.Use(header.NoSniff()) app.Use(header.FrameDeny()) ``` ### HSTS ```go theme={null} app.Use(header.HSTS(31536000, true, true)) // 1 year, subdomains, preload ``` ### Content Security Policy ```go theme={null} app.Use(header.CSP("default-src 'self'; script-src 'self' cdn.example.com")) ``` ### Full Configuration ```go theme={null} app.Use(header.WithOptions(header.Options{ Response: map[string]string{ "X-Frame-Options": "DENY", "X-Content-Type-Options": "nosniff", }, ResponseRemove: []string{"Server"}, Request: map[string]string{ "X-Request-Source": "web", }, })) ``` ## API Reference ```go theme={null} func Set(key, value string) mizu.Middleware func SetRequest(key, value string) mizu.Middleware func New(headers map[string]string) mizu.Middleware func Remove(keys ...string) mizu.Middleware func RemoveRequest(keys ...string) mizu.Middleware func WithOptions(opts Options) mizu.Middleware ``` ## Technical Details ### Architecture The header middleware is built on the `WithOptions` function, which serves as the core implementation. All other functions (`Set`, `New`, `Remove`, etc.) are convenience wrappers that delegate to `WithOptions` with specific configurations. ### Options Structure ```go theme={null} type Options struct { Request map[string]string // Request headers to set Response map[string]string // Response headers to set RequestRemove []string // Request headers to remove ResponseRemove []string // Response headers to remove } ``` ### Execution Flow 1. **Request Phase** (before handler): * Sets request headers from `Options.Request` * Removes request headers from `Options.RequestRemove` * Sets response headers from `Options.Response` 2. **Handler Execution**: Calls the next middleware/handler 3. **Response Phase** (after handler): * Removes response headers from `Options.ResponseRemove` ### Implementation Notes * Request headers are set before the handler executes, making them available to downstream handlers * Response headers are set before the handler but can be overridden by the handler * Response header removal happens after the handler executes to ensure headers set by the handler are properly removed * The middleware uses a custom `itoa` function for integer-to-string conversion to avoid unnecessary allocations in HSTS header generation ### Best Practices * Use `WithOptions` for complex configurations involving multiple header operations * Use convenience functions (`Set`, `Remove`, etc.) for simple single-header operations * Set security headers at the application level rather than per-route * Remove sensitive headers (e.g., `Server`, `X-Powered-By`) to avoid information disclosure * Chain multiple header middlewares when you need different headers for different routes ## Testing The header middleware includes comprehensive test coverage for all functions: | Test Case | Description | Expected Behavior | | -------------------------------- | ------------------------------------------- | -------------------------------------------------------------------------------------- | | `TestNew` | Tests setting multiple response headers | Headers `X-Custom` and `X-Another` are set in response | | `TestWithOptions_RequestHeaders` | Tests setting request headers | Header `X-Injected` is available in request context | | `TestWithOptions_RemoveHeaders` | Tests removing request and response headers | Request header `X-Remove-Me` and response header `X-Server` are removed | | `TestSet` | Tests setting a single response header | Header `X-Test` is set in response | | `TestSetRequest` | Tests setting a single request header | Header `X-Request-Custom` is available in request | | `TestRemove` | Tests removing multiple response headers | Headers `Server` and `X-Powered-By` are removed from response | | `TestRemoveRequest` | Tests removing request headers | Header `Cookie` is removed from request | | `TestXSSProtection` | Tests XSS protection header | Header `X-XSS-Protection` is set to `1; mode=block` | | `TestNoSniff` | Tests content type sniffing protection | Header `X-Content-Type-Options` is set to `nosniff` | | `TestFrameDeny` | Tests frame options deny | Header `X-Frame-Options` is set to `DENY` | | `TestFrameSameOrigin` | Tests frame options same origin | Header `X-Frame-Options` is set to `SAMEORIGIN` | | `TestHSTS` | Tests HTTP Strict Transport Security | Header `Strict-Transport-Security` is set with max-age, includeSubDomains, and preload | | `TestCSP` | Tests Content Security Policy | Header `Content-Security-Policy` is set with custom policy | | `TestReferrerPolicy` | Tests referrer policy | Header `Referrer-Policy` is set to `strict-origin` | | `TestJSON` | Tests JSON content type | Header `Content-Type` is set to `application/json; charset=utf-8` | | `TestHTML` | Tests HTML content type | Header `Content-Type` is set to `text/html; charset=utf-8` | | `TestText` | Tests text content type | Header `Content-Type` is set to `text/plain; charset=utf-8` | | `TestXML` | Tests XML content type | Header `Content-Type` is set to `application/xml; charset=utf-8` | ## Related Middlewares * [helmet](/middlewares/helmet) - Comprehensive security headers # Healthcheck Source: https://docs.go-mizu.dev/middlewares/healthcheck Health check endpoints middleware for Kubernetes liveness and readiness probes. ## Overview The `healthcheck` middleware provides liveness and readiness probe endpoints for container orchestration platforms like Kubernetes. It supports custom health checks for databases, caches, and other dependencies. Use it when you need: * Kubernetes liveness/readiness probes * Load balancer health checks * Dependency health monitoring ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/healthcheck" ``` ## Quick Start ```go theme={null} app := mizu.New() // Register default endpoints healthcheck.Register(app, healthcheck.Options{}) // Or add custom checks healthcheck.Register(app, healthcheck.Options{ Checks: []healthcheck.Check{ healthcheck.DBCheck("postgres", db.Ping), }, }) ``` ## Configuration ### Options | Option | Type | Default | Description | | --------------- | --------- | ------------ | ----------------------- | | `LivenessPath` | `string` | `"/healthz"` | Liveness endpoint path | | `ReadinessPath` | `string` | `"/readyz"` | Readiness endpoint path | | `Checks` | `[]Check` | `[]` | Health checks to run | ### Check | Field | Type | Description | | --------- | ----------------------------- | ------------------------ | | `Name` | `string` | Check name for reporting | | `Check` | `func(context.Context) error` | Check function | | `Timeout` | `time.Duration` | Check timeout | ## Examples ### Basic Endpoints ```go theme={null} healthcheck.Register(app, healthcheck.Options{}) // Creates: // GET /healthz - liveness probe // GET /readyz - readiness probe ``` ### With Database Check ```go theme={null} healthcheck.Register(app, healthcheck.Options{ Checks: []healthcheck.Check{ { Name: "postgres", Check: func(ctx context.Context) error { return db.PingContext(ctx) }, Timeout: 5 * time.Second, }, }, }) ``` ### Multiple Dependencies ```go theme={null} healthcheck.Register(app, healthcheck.Options{ Checks: []healthcheck.Check{ healthcheck.DBCheck("postgres", db.Ping), { Name: "redis", Check: func(ctx context.Context) error { return redisClient.Ping(ctx).Err() }, Timeout: 2 * time.Second, }, { Name: "elasticsearch", Check: func(ctx context.Context) error { _, _, err := esClient.Ping() return err }, Timeout: 3 * time.Second, }, }, }) ``` ### Custom Paths ```go theme={null} healthcheck.Register(app, healthcheck.Options{ LivenessPath: "/health/live", ReadinessPath: "/health/ready", }) ``` ### Standalone Handlers ```go theme={null} // Just liveness app.Get("/healthz", healthcheck.Liveness()) // Just readiness with checks app.Get("/readyz", healthcheck.Readiness( healthcheck.Check{Name: "db", Check: db.Ping}, )) ``` ### Using DBCheck Helper ```go theme={null} check := healthcheck.DBCheck("postgres", func(ctx context.Context) error { return db.PingContext(ctx) }) // Creates check with 5s default timeout ``` ## API Reference ### Functions ```go theme={null} // Register adds health check routes to the app func Register(app *mizu.App, opts Options) // Liveness returns a simple liveness handler func Liveness() mizu.Handler // Readiness returns a readiness handler with checks func Readiness(checks ...Check) mizu.Handler // New creates a combined health handler func New(opts Options) mizu.Handler // DBCheck creates a database check with default timeout func DBCheck(name string, pingFn func(context.Context) error) Check ``` ## Response Format ### Liveness Response ``` ok ``` ### Readiness Response (Success) ```json theme={null} { "status": "ok", "checks": { "postgres": "ok", "redis": "ok" } } ``` ### Readiness Response (Failure) ```json theme={null} { "status": "error", "checks": { "postgres": "ok", "redis": "connection failed" } } ``` ## HTTP Status Codes | Code | Meaning | | ---- | ------------------------------------- | | 200 | Healthy | | 503 | Unhealthy (one or more checks failed) | ## Kubernetes Configuration ```yaml theme={null} livenessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 5 periodSeconds: 10 readinessProbe: httpGet: path: /readyz port: 8080 initialDelaySeconds: 5 periodSeconds: 5 ``` ## Technical Details ### Implementation Architecture The healthcheck middleware is designed with the following technical characteristics: * **Concurrent Check Execution**: All health checks run in parallel using goroutines with a `sync.WaitGroup` to coordinate execution * **Thread-Safe Results**: A `sync.Mutex` protects the results map during concurrent check execution * **Context-Based Timeouts**: Each check runs with its own `context.WithTimeout` to prevent hanging checks * **Default Timeout**: If no timeout is specified, checks default to 5 seconds * **Status Code Mapping**: Returns HTTP 200 for healthy, HTTP 503 for unhealthy states ### Core Components 1. **Check Struct**: Defines a health check with name, check function, and timeout 2. **Status Struct**: JSON response structure with overall status and individual check results 3. **Options Struct**: Configuration for endpoint paths and checks list 4. **Liveness Handler**: Simple text response ("ok") with no dependency checks 5. **Readiness Handler**: Executes all checks concurrently and aggregates results ### Helper Functions * **DBCheck**: Creates a database health check with a 5-second default timeout * **HTTPCheck**: Creates an HTTP endpoint health check with a 10-second default timeout * **New**: Creates a combined handler that routes to liveness or readiness based on path * **Register**: Convenience function to register both endpoints on a router ## Best Practices * Keep liveness checks simple (no external dependencies) * Include all critical dependencies in readiness checks * Set appropriate timeouts for each check * Use readiness to control traffic during startup/shutdown ## Testing ### Test Cases | Test Case | Description | Expected Behavior | | -------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------- | | `TestLiveness` | Basic liveness endpoint test | Returns HTTP 200 with "ok" text response | | `TestReadiness/no_checks` | Readiness with no checks configured | Returns HTTP 200 with "ok" text response | | `TestReadiness/all_healthy` | Readiness with all checks passing | Returns HTTP 200 with JSON status "ok" and all checks marked "ok" | | `TestReadiness/one_unhealthy` | Readiness with one failing check | Returns HTTP 503 with JSON status "error" and error message in checks | | `TestReadiness_Timeout` | Check that exceeds timeout | Returns HTTP 503 due to context deadline exceeded | | `TestNew/liveness` | Combined handler with custom liveness path | Routes to liveness handler and returns HTTP 200 | | `TestNew/readiness` | Combined handler with custom readiness path | Routes to readiness handler and returns HTTP 200 with checks | | `TestRegister/default_liveness` | Register with default paths (/healthz) | Liveness endpoint returns HTTP 200 | | `TestRegister/default_readiness` | Register with default paths (/readyz) | Readiness endpoint returns HTTP 200 with checks | | `TestDBCheck` | DBCheck helper function | Creates check with name "postgres", 5s timeout, and executes successfully | ## Related Middlewares * [pprof](/middlewares/pprof) - Profiling endpoints * [metrics](/middlewares/metrics) - Metrics collection * [version](/middlewares/version) - API versioning # Hedge Source: https://docs.go-mizu.dev/middlewares/hedge Hedged requests middleware for latency reduction through parallel requests. ## Overview The `hedge` middleware sends parallel backup requests after a delay, returning the first successful response. This reduces tail latency by hedging against slow responses. Use it when you need: * Reduce p99 latency * Handle slow backend responses * Improve user experience ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/hedge" ``` ## Quick Start ```go theme={null} app := mizu.New() // Send hedge request after 100ms app.Use(hedge.New(100 * time.Millisecond)) ``` ## Configuration ### Options | Option | Type | Default | Description | | ----------- | ---------------------- | -------- | ------------------- | | `Delay` | `time.Duration` | Required | Time before hedge | | `MaxHedges` | `int` | `1` | Max parallel hedges | | `Skip` | `func(*mizu.Ctx) bool` | `nil` | Skip hedging | ## Examples ### Basic Hedging ```go theme={null} // Hedge after 200ms app.Use(hedge.New(200 * time.Millisecond)) ``` ### Multiple Hedges ```go theme={null} app.Use(hedge.WithOptions(hedge.Options{ Delay: 100 * time.Millisecond, MaxHedges: 2, // Up to 2 parallel requests })) ``` ### Skip Non-Idempotent ```go theme={null} app.Use(hedge.WithOptions(hedge.Options{ Delay: 100 * time.Millisecond, Skip: func(c *mizu.Ctx) bool { // Don't hedge mutations return c.Request().Method != http.MethodGet }, })) ``` ## How It Works 1. Request arrives 2. Start processing 3. After delay, if no response, send hedge request 4. Return first successful response 5. Cancel other requests ## API Reference ### Functions ```go theme={null} // New creates hedge middleware func New(delay time.Duration) mizu.Middleware // WithOptions creates with configuration func WithOptions(opts Options) mizu.Middleware ``` ## When to Use * High-latency backends * P99 latency optimization * Read-heavy workloads ## When NOT to Use * Non-idempotent operations (POST, PUT, DELETE) * Resource-intensive operations * When backend can't handle extra load ## Technical Details ### Implementation The hedge middleware uses a sophisticated concurrent request pattern: 1. **Request Buffering**: The middleware reads and buffers the request body to enable multiple identical requests 2. **Response Recording**: Each request (original and hedges) writes to a `responseRecorder` that captures headers, status code, and body 3. **Atomic Winner Selection**: Uses atomic operations (`CompareAndSwapInt32`) to ensure only the first completing request wins 4. **Context Management**: Each request receives hedge metadata through context values (`HedgeInfo`) 5. **Graceful Cancellation**: When a winner is selected, remaining requests are cancelled via context ### Key Components * **Hedger**: Main struct managing options and statistics * **Options**: Configuration including delay, max hedges, timeout, and callbacks * **HedgeInfo**: Context data containing hedge number, total hedges, winner, and duration * **Stats**: Tracks total requests, hedged requests, hedges triggered, and win rates * **responseRecorder**: Custom `http.ResponseWriter` that buffers responses ### Statistics Tracking The middleware tracks comprehensive statistics: * Total requests processed * Requests eligible for hedging * Number of hedges actually triggered * Wins by original vs hedged requests ### Callbacks Two callback hooks are available: * `OnHedge`: Called when a hedge request is triggered * `OnComplete`: Called when the winning response is selected ### Default Values | Setting | Default Value | Purpose | | --------- | ------------- | ---------------------------- | | Delay | 100ms | Time before triggering hedge | | MaxHedges | 1 | Maximum concurrent hedges | | Timeout | 30s | Overall request timeout | ## Best Practices * Only use for idempotent operations * Set delay based on p50 latency * Monitor hedge rate * Ensure backend can handle increased load ## Testing The hedge middleware includes comprehensive test coverage: | Test Case | Description | Expected Behavior | | ------------------- | ------------------------------------------- | ---------------------------------------------- | | TestNew | Basic middleware creation | Middleware works with default options | | TestFastResponse | Fast response before hedge delay | Original request completes, no hedge triggered | | TestHedgeTriggered | Slow response triggers hedge | Hedge request is started after delay | | TestShouldHedge | Conditional hedging based on path | Only specified paths are hedged | | TestOnComplete | OnComplete callback execution | Callback receives hedge number and duration | | TestStats | Statistics tracking | Tracks total requests and hedged requests | | TestGetHedgeInfo | Retrieve hedge metadata from context | Returns HedgeInfo with hedge number | | TestIsHedge | Check if request is a hedge | Original request returns false | | TestConditional | Conditional middleware for specific methods | Only GET requests are hedged | | TestForSlowRequests | Hedge slow requests with threshold | Hedges triggered after custom delay | | TestMaxHedges | Multiple concurrent hedges | Up to MaxHedges requests run in parallel | | TestTimeout | Request timeout handling | Requests timeout after configured duration | ## Related Middlewares * [timeout](/middlewares/timeout) - Request timeout * [retry](/middlewares/retry) - Automatic retries * [circuitbreaker](/middlewares/circuitbreaker) - Circuit breaker # Helmet Source: https://docs.go-mizu.dev/middlewares/helmet Security headers middleware for protecting against common web vulnerabilities. ## Overview The `helmet` middleware sets security-related HTTP headers to protect your application from common vulnerabilities like clickjacking, XSS, and content-type sniffing. Use it when you need: * Protection against common web vulnerabilities * Content Security Policy (CSP) * HTTP Strict Transport Security (HSTS) * Compliance with security best practices ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/helmet" ``` ## Quick Start ```go theme={null} app := mizu.New() // Use recommended security headers app.Use(helmet.Default()) ``` ## Configuration ### Options | Option | Type | Default | Description | | ------------------------------- | -------------- | ----------------------------------- | --------------------------------- | | `ContentSecurityPolicy` | `string` | - | Content-Security-Policy header | | `XFrameOptions` | `string` | `"SAMEORIGIN"` | X-Frame-Options header | | `XContentTypeOptions` | `bool` | `true` | X-Content-Type-Options: nosniff | | `ReferrerPolicy` | `string` | `"strict-origin-when-cross-origin"` | Referrer-Policy header | | `StrictTransportSecurity` | `*HSTSOptions` | - | HSTS configuration | | `PermissionsPolicy` | `string` | - | Permissions-Policy header | | `CrossOriginOpenerPolicy` | `string` | `"same-origin"` | Cross-Origin-Opener-Policy | | `CrossOriginEmbedderPolicy` | `string` | - | Cross-Origin-Embedder-Policy | | `CrossOriginResourcePolicy` | `string` | `"same-origin"` | Cross-Origin-Resource-Policy | | `OriginAgentCluster` | `bool` | `true` | Origin-Agent-Cluster: ?1 | | `XDNSPrefetchControl` | `*bool` | `false` | X-DNS-Prefetch-Control | | `XDownloadOptions` | `bool` | `true` | X-Download-Options: noopen | | `XPermittedCrossDomainPolicies` | `string` | `"none"` | X-Permitted-Cross-Domain-Policies | ### HSTSOptions | Option | Type | Default | Description | | ------------------- | --------------- | ------- | --------------------------- | | `MaxAge` | `time.Duration` | - | Time to remember HTTPS-only | | `IncludeSubDomains` | `bool` | `false` | Apply to subdomains | | `Preload` | `bool` | `false` | Enable HSTS preload | ## Examples ### Default Security Headers ```go theme={null} // Recommended settings for most applications app.Use(helmet.Default()) ``` Sets these headers: ``` X-Content-Type-Options: nosniff X-Frame-Options: SAMEORIGIN X-DNS-Prefetch-Control: off X-Download-Options: noopen X-Permitted-Cross-Domain-Policies: none Referrer-Policy: strict-origin-when-cross-origin Cross-Origin-Opener-Policy: same-origin Cross-Origin-Resource-Policy: same-origin Origin-Agent-Cluster: ?1 ``` ### Content Security Policy ```go theme={null} app.Use(helmet.ContentSecurityPolicy( "default-src 'self'; " + "script-src 'self' 'unsafe-inline' cdn.example.com; " + "style-src 'self' 'unsafe-inline'; " + "img-src 'self' data: https:; " + "font-src 'self' fonts.gstatic.com", )) ``` ### HTTP Strict Transport Security ```go theme={null} app.Use(helmet.StrictTransportSecurity( 365*24*time.Hour, // 1 year true, // include subdomains true, // preload )) ``` ### Frame Options ```go theme={null} // Prevent any framing app.Use(helmet.XFrameOptions("DENY")) // Allow same origin only app.Use(helmet.XFrameOptions("SAMEORIGIN")) ``` ### Full Custom Configuration ```go theme={null} app.Use(helmet.New(helmet.Options{ ContentSecurityPolicy: "default-src 'self'", XFrameOptions: "DENY", XContentTypeOptions: true, ReferrerPolicy: "no-referrer", StrictTransportSecurity: &helmet.HSTSOptions{ MaxAge: 365 * 24 * time.Hour, IncludeSubDomains: true, Preload: true, }, PermissionsPolicy: "geolocation=(), microphone=()", CrossOriginOpenerPolicy: "same-origin-allow-popups", CrossOriginEmbedderPolicy: "require-corp", CrossOriginResourcePolicy: "same-site", })) ``` ### Individual Header Functions ```go theme={null} // Apply only specific headers app.Use(helmet.XContentTypeOptions()) // X-Content-Type-Options: nosniff app.Use(helmet.XFrameOptions("DENY")) // X-Frame-Options: DENY app.Use(helmet.ReferrerPolicy("no-referrer")) app.Use(helmet.OriginAgentCluster()) ``` ### Permissions Policy ```go theme={null} // Disable dangerous browser features app.Use(helmet.PermissionsPolicy( "geolocation=(), " + "microphone=(), " + "camera=(), " + "payment=(self), " + "usb=()", )) ``` ### DNS Prefetch Control ```go theme={null} // Enable DNS prefetching for performance on := true app.Use(helmet.XDNSPrefetchControl(true)) // Disable for privacy app.Use(helmet.XDNSPrefetchControl(false)) ``` ### Cross-Origin Policies ```go theme={null} app.Use(helmet.New(helmet.Options{ // Isolate browsing context CrossOriginOpenerPolicy: "same-origin", // Require CORP for subresources CrossOriginEmbedderPolicy: "require-corp", // Control resource sharing CrossOriginResourcePolicy: "same-origin", })) ``` ## API Reference ### Functions ```go theme={null} // Default creates middleware with recommended headers func Default() mizu.Middleware // New creates middleware with custom options func New(opts Options) mizu.Middleware ``` ### Individual Header Functions ```go theme={null} func ContentSecurityPolicy(policy string) mizu.Middleware func XFrameOptions(value string) mizu.Middleware func XContentTypeOptions() mizu.Middleware func ReferrerPolicy(policy string) mizu.Middleware func StrictTransportSecurity(maxAge time.Duration, includeSubDomains, preload bool) mizu.Middleware func PermissionsPolicy(policy string) mizu.Middleware func CrossOriginOpenerPolicy(policy string) mizu.Middleware func CrossOriginEmbedderPolicy(policy string) mizu.Middleware func CrossOriginResourcePolicy(policy string) mizu.Middleware func OriginAgentCluster() mizu.Middleware func XDNSPrefetchControl(on bool) mizu.Middleware func XDownloadOptions() mizu.Middleware func XPermittedCrossDomainPolicies(policy string) mizu.Middleware ``` ## Security Headers Explained | Header | Protection | | --------------------------- | ---------------------- | | `Content-Security-Policy` | XSS, injection attacks | | `X-Frame-Options` | Clickjacking | | `X-Content-Type-Options` | MIME type sniffing | | `Referrer-Policy` | Information leakage | | `Strict-Transport-Security` | Downgrade attacks | | `Permissions-Policy` | Feature abuse | | `Cross-Origin-*-Policy` | Cross-origin attacks | ## Technical Details ### Architecture The helmet middleware is implemented as a higher-order function that wraps request handlers and injects security headers before passing control to the next handler in the chain. The middleware follows a configuration-based approach where all security headers are controlled through the `Options` struct. ### Implementation * **Header Injection**: Headers are set conditionally based on non-empty/non-nil configuration values * **HSTS Formatting**: The `formatHSTS` helper function constructs the Strict-Transport-Security header value from duration and boolean flags, converting the duration to seconds and appending directives as needed * **DNS Prefetch Control**: Uses a pointer to `bool` to distinguish between unset (nil), enabled (true), and disabled (false) states * **Default Configuration**: The `Default()` function provides a preset configuration aligned with modern security best practices * **Individual Functions**: Convenience functions like `XFrameOptions()` and `ContentSecurityPolicy()` create middleware instances with single-header configurations ### Header Setting Logic The `New()` function checks each option field and sets the corresponding header only if: * String fields are non-empty * Boolean fields are true (or pointer fields are non-nil) * Struct pointer fields (like `HSTSOptions`) are non-nil This approach ensures headers are only set when explicitly configured, preventing empty or unwanted headers from being sent. ### Performance Considerations * Minimal overhead: Only string operations and header setting * No dynamic allocations during request processing * All configuration is computed once during middleware creation * Headers are set before calling the next handler, ensuring they're present even if the handler fails ## Best Practices 1. **Start with Default()** - Then customize as needed 2. **Content Security Policy** - Start strict, loosen as needed 3. **HSTS** - Enable for HTTPS-only sites 4. **Test thoroughly** - Security headers can break functionality 5. **Report-Only mode** - Test CSP before enforcing ### CSP Development Strategy ```go theme={null} // Development: Report-only mode app.Use(helmet.New(helmet.Options{ ContentSecurityPolicy: "default-src 'self'; report-uri /csp-report", })) // Production: Enforce app.Use(helmet.New(helmet.Options{ ContentSecurityPolicy: "default-src 'self'", })) ``` ## Testing The helmet middleware includes comprehensive test coverage for all security headers and configuration options: | Test Case | Description | Expected Behavior | | ------------------------------------------------------ | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `TestDefault` | Validates default security headers | Sets 9 recommended headers: X-Content-Type-Options (nosniff), X-Frame-Options (SAMEORIGIN), X-DNS-Prefetch-Control (off), X-Download-Options (noopen), X-Permitted-Cross-Domain-Policies (none), Referrer-Policy (strict-origin-when-cross-origin), Cross-Origin-Opener-Policy (same-origin), Cross-Origin-Resource-Policy (same-origin), Origin-Agent-Cluster (?1) | | `TestContentSecurityPolicy` | Tests CSP header setting | Sets Content-Security-Policy header with custom policy value | | `TestXFrameOptions` (DENY) | Tests frame options with DENY | Sets X-Frame-Options: DENY | | `TestXFrameOptions` (SAMEORIGIN) | Tests frame options with SAMEORIGIN | Sets X-Frame-Options: SAMEORIGIN | | `TestStrictTransportSecurity` (basic) | Tests HSTS with max-age only | Sets Strict-Transport-Security: max-age=31536000 | | `TestStrictTransportSecurity` (with subdomains) | Tests HSTS with subdomains | Sets Strict-Transport-Security: max-age=31536000; includeSubDomains | | `TestStrictTransportSecurity` (with preload) | Tests HSTS with all options | Sets Strict-Transport-Security: max-age=31536000; includeSubDomains; preload | | `TestReferrerPolicy` (no-referrer) | Tests no-referrer policy | Sets Referrer-Policy: no-referrer | | `TestReferrerPolicy` (no-referrer-when-downgrade) | Tests downgrade policy | Sets Referrer-Policy: no-referrer-when-downgrade | | `TestReferrerPolicy` (origin) | Tests origin policy | Sets Referrer-Policy: origin | | `TestReferrerPolicy` (origin-when-cross-origin) | Tests cross-origin policy | Sets Referrer-Policy: origin-when-cross-origin | | `TestReferrerPolicy` (same-origin) | Tests same-origin policy | Sets Referrer-Policy: same-origin | | `TestReferrerPolicy` (strict-origin) | Tests strict-origin policy | Sets Referrer-Policy: strict-origin | | `TestReferrerPolicy` (strict-origin-when-cross-origin) | Tests strict cross-origin policy | Sets Referrer-Policy: strict-origin-when-cross-origin | | `TestReferrerPolicy` (unsafe-url) | Tests unsafe-url policy | Sets Referrer-Policy: unsafe-url | | `TestPermissionsPolicy` | Tests permissions policy header | Sets Permissions-Policy: geolocation=(), microphone=() | | `TestCrossOriginPolicies` | Tests all cross-origin headers | Sets Cross-Origin-Opener-Policy (same-origin), Cross-Origin-Embedder-Policy (require-corp), Cross-Origin-Resource-Policy (same-site) | | `TestOriginAgentCluster` | Tests origin agent cluster header | Sets Origin-Agent-Cluster: ?1 | | `TestXDNSPrefetchControl` (on) | Tests DNS prefetch enabled | Sets X-DNS-Prefetch-Control: on | | `TestXDNSPrefetchControl` (off) | Tests DNS prefetch disabled | Sets X-DNS-Prefetch-Control: off | | `TestXDownloadOptions` | Tests download options header | Sets X-Download-Options: noopen | | `TestXPermittedCrossDomainPolicies` | Tests cross-domain policies header | Sets X-Permitted-Cross-Domain-Policies: master-only | | `TestXContentTypeOptions` | Tests content type options header | Sets X-Content-Type-Options: nosniff | ## Related Middlewares * [secure](/middlewares/secure) - HTTPS enforcement * [csrf](/middlewares/csrf) - CSRF protection # Honeypot Source: https://docs.go-mizu.dev/middlewares/honeypot Detect and block malicious requests targeting common attack paths. ## Overview The `honeypot` middleware monitors requests to commonly attacked paths (like `/admin`, `/.env`, `/wp-admin`) and blocks IPs that access them. It acts as a trap for attackers and bots scanning for vulnerabilities. Use it when you need: * Detect automated vulnerability scanners * Block malicious IPs proactively * Reduce attack surface * Log attack attempts ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/honeypot" ``` ## Quick Start ```go theme={null} app := mizu.New() // Use default honeypot paths app.Use(honeypot.New()) ``` ## Configuration ### Options | Option | Type | Default | Description | | --------------- | ----------------------- | ------------------- | ------------------------------ | | `Paths` | `[]string` | Common attack paths | Paths to monitor | | `BlockDuration` | `time.Duration` | `1h` | How long to block detected IPs | | `Response` | `func(*mizu.Ctx) error` | 404 Not Found | Response for honeypot requests | | `OnTrap` | `func(ip, path string)` | - | Callback when trap triggered | ### Default Paths ```go theme={null} var defaultPaths = []string{ "/admin", "/administrator", "/wp-admin", "/wp-login.php", "/phpmyadmin", "/pma", "/mysql", "/sql", "/.env", "/.git", "/.svn", "/config.php", "/backup", "/backup.sql", "/dump.sql", "/shell", "/cmd", "/exec", "/eval", "/xmlrpc.php", "/wp-config.php", "/server-status", "/server-info", } ``` ## Examples ### Default Configuration ```go theme={null} // Monitor default attack paths app.Use(honeypot.New()) ``` ### Custom Paths ```go theme={null} app.Use(honeypot.Paths( "/secret", "/internal", "/debug", "/test.php", )) ``` ### Admin Path Honeypot ```go theme={null} // Specifically target admin path attackers app.Use(honeypot.AdminPaths()) ``` ### Config File Honeypot ```go theme={null} // Target config file scanners app.Use(honeypot.ConfigPaths()) ``` ### Database Path Honeypot ```go theme={null} // Target database scanners app.Use(honeypot.DatabasePaths()) ``` ### With Logging ```go theme={null} app.Use(honeypot.WithOptions(honeypot.Options{ OnTrap: func(ip, path string) { log.Printf("HONEYPOT: IP %s triggered trap at %s", ip, path) // Send to security monitoring alertSecurity(ip, path) }, })) ``` ### Custom Block Duration ```go theme={null} app.Use(honeypot.WithOptions(honeypot.Options{ BlockDuration: 24 * time.Hour, // Block for 24 hours })) ``` ### Custom Response ```go theme={null} app.Use(honeypot.WithOptions(honeypot.Options{ Response: func(c *mizu.Ctx) error { // Slow down attackers with a delay time.Sleep(5 * time.Second) return c.Text(404, "Not Found") }, })) ``` ### Form Field Honeypot Detect bots that fill hidden form fields: ```go theme={null} // Add to forms that might be targeted by bots app.Post("/contact", contactHandler, honeypot.Form("website")) ``` ```html theme={null}
    ``` ### Comprehensive Protection ```go theme={null} app.Use(honeypot.WithOptions(honeypot.Options{ Paths: append( honeypot.DefaultPaths, "/api/internal", "/debug/vars", "/metrics", // If not used ), BlockDuration: 12 * time.Hour, OnTrap: func(ip, path string) { // Log to file securityLog.Printf("Blocked %s accessing %s", ip, path) // Store in database for analysis db.RecordAttackAttempt(ip, path, time.Now()) // Alert if too many attempts if count := getAttemptCount(ip); count > 10 { alertSecurityTeam(ip) } }, })) ``` ### Combined with IP Filter ```go theme={null} // First, check honeypot app.Use(honeypot.New()) // Then, additional IP filtering app.Use(ipfilter.Deny(loadBlockedIPs()...)) ``` ## API Reference ### Functions ```go theme={null} // New creates honeypot with default options func New() mizu.Middleware // WithOptions creates honeypot with custom options func WithOptions(opts Options) mizu.Middleware // Paths creates honeypot for specific paths func Paths(paths ...string) mizu.Middleware // AdminPaths creates honeypot for admin paths func AdminPaths() mizu.Middleware // ConfigPaths creates honeypot for config file paths func ConfigPaths() mizu.Middleware // DatabasePaths creates honeypot for database paths func DatabasePaths() mizu.Middleware // Form creates honeypot for form field detection func Form(field string) mizu.Middleware ``` ## How It Works 1. **Request arrives** at a honeypot path 2. **IP recorded** in block list with expiration 3. **Callback triggered** (if configured) for logging/alerting 4. **Fake response** returned to attacker 5. **Future requests** from that IP are blocked ``` Attacker → /wp-admin → IP Blocked → 403 Forbidden ↓ OnTrap callback ↓ Log/Alert ``` ## Technical Details ### Architecture The honeypot middleware uses an in-memory block list with automatic cleanup to track and block malicious IPs: ```go theme={null} type blockList struct { mu sync.RWMutex ips map[string]time.Time duration time.Duration } ``` ### Request Flow 1. **IP Extraction** - Client IP is extracted with proxy support (X-Forwarded-For, X-Real-IP) 2. **Block Check** - If IP is in block list and not expired, return 403 Forbidden 3. **Path Matching** - Current path is checked against honeypot path map (O(1) lookup) 4. **Trap Trigger** - If path matches: * IP is added to block list with expiration time * OnTrap callback is invoked (if configured) * Custom/default response is returned 5. **Pass Through** - If no match, request continues to next handler ### Implementation Details * **Concurrent Access** - Block list uses sync.RWMutex for thread-safe operations * **Automatic Cleanup** - Background goroutine runs every 10 minutes to remove expired entries * **Path Lookup** - Paths stored in map\[string]bool for O(1) lookup performance * **IP Detection Priority**: 1. X-Forwarded-For header 2. X-Real-IP header 3. Request.RemoteAddr ### Memory Management * Block list grows with unique trapped IPs * Automatic cleanup prevents unbounded growth * Each entry: IP string + time.Time (approximately 24-32 bytes) * Recommended for moderate traffic; for high-traffic consider external storage ### Form Honeypot Implementation Form honeypots use a simpler stateless approach: * Check if hidden field contains any value * No IP blocking or state management * Returns 400 Bad Request immediately if filled ## Security Considerations 1. **False Positives** - Legitimate users might accidentally hit honeypot paths 2. **IP Rotation** - Attackers may rotate IPs 3. **Proxy Detection** - Consider X-Forwarded-For for accurate blocking 4. **Log Analysis** - Monitor trapped IPs for patterns ## Best Practices * Choose honeypot paths that legitimate users won't access * Set appropriate block duration based on attack patterns * Log all triggered traps for security analysis * Combine with other security measures * Don't use paths that might be legitimately requested ## Testing The honeypot middleware includes comprehensive test coverage for all functionality: | Test Case | Description | Expected Behavior | | -------------------------------------------------------- | --------------------------------------------- | --------------------------------------------- | | `TestNew` - normal path | Request to non-honeypot path | Returns 200 OK, request passes through | | `TestNew` - honeypot path | Request to default honeypot path (/admin) | Returns 404 Not Found, IP blocked | | `TestWithOptions_CustomPaths` - custom trap path | Request to custom honeypot path | Returns 404 Not Found for custom paths | | `TestWithOptions_CustomPaths` - default path not trapped | Request to default path when custom paths set | Passes through (default paths not active) | | `TestWithOptions_OnTrap` | Honeypot triggered with OnTrap callback | Callback invoked with IP and path | | `TestWithOptions_CustomResponse` | Honeypot with custom response function | Returns custom status (418 Teapot) and JSON | | `TestBlockedIP` - first request | Initial request to honeypot path | Returns 404, IP added to block list | | `TestBlockedIP` - subsequent request | Normal request from blocked IP | Returns 403 Forbidden | | `TestPaths` | Custom paths using Paths() function | Custom paths trigger honeypot | | `TestAdminPaths` | AdminPaths() preset paths | Admin-related paths trigger honeypot | | `TestConfigPaths` | ConfigPaths() preset paths | Config file paths (/.env) trigger honeypot | | `TestDatabasePaths` | DatabasePaths() preset paths | Database paths (/phpmyadmin) trigger honeypot | | `TestForm` - empty honeypot field | Form submission with empty hidden field | Returns 200 OK, request passes | | `TestForm` - filled honeypot field | Form submission with filled hidden field | Returns 400 Bad Request, blocks bot | ### Running Tests ```bash theme={null} # Run all honeypot tests go test -v ./middlewares/honeypot # Run with coverage go test -cover ./middlewares/honeypot # Run specific test go test -v -run TestBlockedIP ./middlewares/honeypot ``` ## Related Middlewares * [ipfilter](/middlewares/ipfilter) - IP whitelist/blacklist * [ratelimit](/middlewares/ratelimit) - Rate limiting * [captcha](/middlewares/captcha) - Bot detection # Hypermedia Source: https://docs.go-mizu.dev/middlewares/hypermedia Hypermedia response helpers for HATEOAS APIs. ## Overview The `hypermedia` middleware provides helpers for building HATEOAS (Hypermedia as the Engine of Application State) APIs with links and embedded resources. Use it when you need: * RESTful hypermedia APIs * Self-documenting responses * Discoverable APIs ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/hypermedia" ``` ## Quick Start ```go theme={null} app := mizu.New() app.Use(hypermedia.New()) app.Get("/users/:id", func(c *mizu.Ctx) error { user := getUser(c.Param("id")) return hypermedia.JSON(c, 200, user, hypermedia.Self("/users/"+user.ID), hypermedia.Link("orders", "/users/"+user.ID+"/orders"), ) }) ``` ## Configuration ### Options | Option | Type | Default | Description | | --------- | -------- | ------- | ------------------ | | `BaseURL` | `string` | `""` | Base URL for links | | `Format` | `string` | `"hal"` | HAL or JSON:API | ## Examples ### HAL Format ```go theme={null} app.Get("/users/:id", func(c *mizu.Ctx) error { return hypermedia.HAL(c, 200, user, hypermedia.Self("/users/1"), hypermedia.Link("orders", "/users/1/orders"), ) }) // { // "id": 1, // "name": "John", // "_links": { // "self": {"href": "/users/1"}, // "orders": {"href": "/users/1/orders"} // } // } ``` ### Embedded Resources ```go theme={null} app.Get("/users/:id", func(c *mizu.Ctx) error { return hypermedia.HAL(c, 200, user, hypermedia.Self("/users/1"), hypermedia.Embed("orders", orders), ) }) ``` ### Collection with Pagination ```go theme={null} app.Get("/users", func(c *mizu.Ctx) error { return hypermedia.Collection(c, 200, users, hypermedia.Self("/users"), hypermedia.Link("next", "/users?page=2"), hypermedia.Link("prev", "/users?page=0"), ) }) ``` ## API Reference ### Functions ```go theme={null} // New creates hypermedia middleware func New() mizu.Middleware // HAL returns HAL+JSON response func HAL(c *mizu.Ctx, status int, data any, links ...Link) error // Collection returns HAL collection func Collection(c *mizu.Ctx, status int, items any, links ...Link) error // Link helpers func Self(href string) Link func Link(rel, href string) Link func Embed(rel string, data any) Link ``` ## Technical Details ### Architecture The hypermedia middleware uses a response recorder pattern to intercept and modify JSON responses: 1. **Context Storage**: Links are stored in the request context using a private context key 2. **Response Recording**: A custom `responseRecorder` captures the response body and status code 3. **JSON Modification**: The middleware parses the JSON response, injects links, and re-encodes it 4. **Content-Type Filtering**: Only processes responses with `application/json` content type ### Implementation Details **Link Structure**: * `Href`: The URL of the linked resource * `Rel`: The relationship type (e.g., "self", "next", "prev") * `Method`: Optional HTTP method for the link * `Title`: Optional human-readable description * `Type`: Optional media type hint **Response Recorder**: The middleware implements a custom `responseRecorder` that wraps the original `http.ResponseWriter` to: * Capture response body in a buffer * Record status code * Allow modification before final write **HAL+JSON Support**: The `HAL` type provides full HAL+JSON specification support with: * Properties for resource data * Links map for hypermedia links * Embedded map for nested resources * Custom JSON marshaling to flatten properties into the root object ### Performance Considerations * Links are stored as pointers in context to avoid copying * JSON parsing only occurs for `application/json` responses * Non-JSON responses pass through without modification * Response buffering adds minimal overhead ### Security Considerations * Base URL validation prevents injection attacks * Links are added server-side, not from user input * TLS detection for automatic scheme selection * No sensitive data should be exposed in link URLs ## Best Practices * Always include self link * Use consistent link relations * Document link relations * Consider client library support ## Testing The hypermedia middleware includes comprehensive test coverage for all functionality: | Test Case | Description | Expected Behavior | | ------------------------- | ------------------------------------ | -------------------------------------------------------------------------------------------- | | `TestNew` | Basic middleware initialization | Creates middleware with default options and adds self link to JSON response | | `TestSelfLink` | Self link generation with base URL | Automatically adds self link with correct base URL and rel="self" | | `TestAddLink` | Adding single link to response | Successfully adds custom link to response links array | | `TestAddLinks` | Adding multiple links at once | Adds multiple links in a single call to the response | | `TestLinkProvider` | Dynamic link generation via provider | Invokes link provider function to generate context-aware links | | `TestNonJSONResponse` | Non-JSON response handling | Passes through non-JSON responses unchanged without adding links | | `TestCustomLinksKey` | Custom links key configuration | Uses custom key instead of default "\_links" for link storage | | `TestResource` | Resource wrapper with links | Creates resource structure with data and links fields | | `TestCollection` | Paginated collection with links | Generates collection with pagination metadata and navigation links (first, prev, next, last) | | `TestCollectionFirstPage` | First page pagination | Omits "prev" link on first page of results | | `TestCollectionLastPage` | Last page pagination | Omits "next" link on last page of results | | `TestHAL` | HAL+JSON resource creation | Creates HAL resource with properties and links, marshals correctly | | `TestHALEmbedded` | HAL embedded resources | Embeds related resources within HAL response structure | | `TestGetLinks` | Retrieving current links | Returns current links from context during request handling | ## Related Middlewares * [envelope](/middlewares/envelope) - Response wrapping * [version](/middlewares/version) - API versioning # Idempotency Source: https://docs.go-mizu.dev/middlewares/idempotency Idempotency key middleware for safe request retries and duplicate prevention. ## Overview The `idempotency` middleware ensures that repeated requests with the same idempotency key return the same response, preventing duplicate operations like double charges or duplicate records. Use it when you need: * Safe payment processing retries * Duplicate request prevention * Reliable webhook handling ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/idempotency" ``` ## Quick Start ```go theme={null} app := mizu.New() // Enable idempotency app.Use(idempotency.New()) app.Post("/payments", func(c *mizu.Ctx) error { // This will only execute once per idempotency key result := processPayment() return c.JSON(200, result) }) ``` Client sends: ``` POST /payments Idempotency-Key: unique-key-123 ``` ## Configuration ### Options | Option | Type | Default | Description | | -------------- | -------------------------------- | -------------------------- | --------------------------- | | `KeyHeader` | `string` | `"Idempotency-Key"` | Header name for key | | `Methods` | `[]string` | `["POST", "PUT", "PATCH"]` | Methods to track | | `TTL` | `time.Duration` | `24h` | How long to store responses | | `KeyGenerator` | `func(string, *mizu.Ctx) string` | Identity | Custom key generation | ## Examples ### Basic Usage ```go theme={null} app.Use(idempotency.New()) ``` ### Custom Header ```go theme={null} app.Use(idempotency.WithOptions(idempotency.Options{ KeyHeader: "X-Request-Id", })) ``` ### Specific Methods Only ```go theme={null} app.Use(idempotency.WithOptions(idempotency.Options{ Methods: []string{http.MethodPost}, // Only POST })) ``` ### User-Scoped Keys ```go theme={null} app.Use(idempotency.WithOptions(idempotency.Options{ KeyGenerator: func(key string, c *mizu.Ctx) string { userID := c.Request().Header.Get("X-User-ID") return userID + ":" + key }, })) ``` ### Custom Store ```go theme={null} store := idempotency.NewMemoryStore() defer store.Close() app.Use(idempotency.WithStore(store, idempotency.Options{})) ``` ## How It Works 1. Client sends request with `Idempotency-Key` header 2. Middleware checks if key exists in store 3. If exists: returns cached response with `Idempotent-Replayed: true` 4. If not: executes handler, caches response, returns normally 5. Subsequent requests with same key get cached response ## API Reference ### Functions ```go theme={null} // New creates idempotency middleware func New() mizu.Middleware // WithOptions creates middleware with configuration func WithOptions(opts Options) mizu.Middleware // WithStore creates middleware with custom store func WithStore(store Store, opts Options) mizu.Middleware // NewMemoryStore creates in-memory response store func NewMemoryStore() *MemoryStore ``` ### Store Interface ```go theme={null} type Store interface { Get(key string) (*Response, error) Set(key string, resp *Response) error Delete(key string) error } type Response struct { StatusCode int Headers http.Header Body []byte ExpiresAt time.Time } ``` ## Response Headers | Header | Description | | --------------------------- | -------------------------------- | | `Idempotent-Replayed: true` | Response was replayed from cache | ## Technical Details ### Cache Key Generation By default, the middleware generates cache keys using SHA-256 hashing of: * The idempotency key from the header * The HTTP method (POST, PUT, etc.) * The request URL path This ensures that the same idempotency key can be safely reused across different endpoints and methods. ### Response Capture The middleware uses a custom `responseCapture` wrapper that: 1. Captures the response status code (defaults to 200 OK) 2. Clones all response headers 3. Buffers the response body in memory 4. Writes everything to both the buffer and the underlying ResponseWriter ### Memory Store Implementation The built-in `MemoryStore`: * Uses `sync.RWMutex` for thread-safe concurrent access * Runs a background cleanup goroutine that removes expired entries every 10 minutes * Stores responses with their expiration timestamps * Returns `nil` for expired entries automatically ### Request Flow 1. **Method Check**: Verifies the HTTP method is in the configured Methods list 2. **Key Extraction**: Retrieves the idempotency key from the specified header 3. **Cache Lookup**: Checks if a response exists for the generated cache key 4. **Cache Hit**: If found and not expired, replays the cached response with `Idempotent-Replayed: true` header 5. **Cache Miss**: Wraps the ResponseWriter, executes the handler, captures the response, and stores it in the cache 6. **Expiration**: Responses are stored with a TTL and automatically cleaned up ## Best Practices * Always use idempotency keys for payment operations * Generate unique keys client-side (UUIDs work well) * Include user context in key generation for multi-tenant apps * Set appropriate TTL based on your use case * Use distributed store (Redis) for multi-instance deployments ## Testing ### Test Coverage | Test Case | Description | Expected Behavior | | ------------------------------------ | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `TestNew` | Basic idempotency with default configuration | First request executes handler, second request with same key returns cached response with `Idempotent-Replayed: true` header | | `TestWithOptions_NoKey` | Requests without idempotency key | All requests execute handler normally (no caching) | | `TestWithOptions_DifferentKeys` | Multiple requests with different keys | Each unique key executes handler independently | | `TestWithOptions_CustomHeader` | Custom header name configuration | Middleware uses custom header (e.g., `X-Request-Id`) for idempotency key | | `TestWithOptions_Methods` | Method filtering (POST only) | POST requests are cached, PUT requests are not cached | | `TestWithOptions_GET` | GET requests with idempotency key | GET requests bypass middleware (not cached by default) | | `TestWithOptions_ResponseHeaders` | Custom response headers | Cached response includes all custom headers from original response | | `TestWithStore` | Custom store integration | Middleware works with custom Store implementation | | `TestMemoryStore` | Memory store operations | Set, Get, and Delete operations work correctly | | `TestMemoryStore_Expiry` | Expired entries | Store returns `nil` for expired entries | | `TestWithOptions_CustomKeyGenerator` | User-scoped keys | Same idempotency key with different user IDs creates separate cache entries | | `TestWithOptions_ResponseBody` | JSON response caching | Complete response body (including JSON) is cached and replayed | ## Client Example ```javascript theme={null} // JavaScript client const response = await fetch('/api/payments', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Idempotency-Key': crypto.randomUUID() }, body: JSON.stringify({ amount: 100 }) }); ``` ## Related Middlewares * [requestid](/middlewares/requestid) - Request ID generation * [retry](/middlewares/retry) - Automatic retries * [ratelimit](/middlewares/ratelimit) - Rate limiting # IP Filter Source: https://docs.go-mizu.dev/middlewares/ipfilter IP whitelist and blacklist middleware for access control. ## Overview The `ipfilter` middleware filters requests based on client IP addresses. Use it to allow or deny access from specific IPs or IP ranges (CIDR notation). Use it when you need: * Whitelist access to admin panels * Block known malicious IPs * Restrict access to internal networks * Geo-blocking based on IP ranges ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/ipfilter" ``` ## Quick Start ```go theme={null} app := mizu.New() // Allow only specific IPs app.Use(ipfilter.Allow("192.168.1.0/24", "10.0.0.1")) // Block specific IPs app.Use(ipfilter.Deny("192.168.1.100", "10.0.0.0/8")) ``` ## Configuration ### Options | Option | Type | Default | Description | | --------------- | ----------------------- | ------- | ------------------------- | | `AllowList` | `[]string` | - | IPs/CIDRs to allow | | `DenyList` | `[]string` | - | IPs/CIDRs to deny | | `DenyByDefault` | `bool` | `false` | Deny unless in allow list | | `TrustProxy` | `bool` | `false` | Use X-Forwarded-For | | `ErrorHandler` | `func(*mizu.Ctx) error` | - | Custom denial handler | ## Examples ### Whitelist Mode Only allow specific IPs, deny everything else: ```go theme={null} app.Use(ipfilter.Allow( "192.168.1.0/24", // Office network "10.0.0.5", // Specific server "203.0.113.50", // Remote developer )) ``` ### Blacklist Mode Allow everything except specific IPs: ```go theme={null} app.Use(ipfilter.Deny( "192.168.1.100", // Blocked user "10.0.0.0/8", // Blocked network )) ``` ### Combined Allow and Deny ```go theme={null} app.Use(ipfilter.New(ipfilter.Options{ AllowList: []string{"192.168.1.0/24"}, DenyList: []string{"192.168.1.100"}, // Even within allowed range })) ``` ### Localhost Only ```go theme={null} // Only allow localhost connections app.Use(ipfilter.Localhost()) ``` ### Private Networks Only ```go theme={null} // Only allow private network IPs app.Use(ipfilter.Private()) ``` ### Behind a Proxy ```go theme={null} app.Use(ipfilter.New(ipfilter.Options{ AllowList: []string{"203.0.113.0/24"}, TrustProxy: true, // Use X-Forwarded-For header })) ``` ### Custom Error Handler ```go theme={null} app.Use(ipfilter.New(ipfilter.Options{ AllowList: []string{"192.168.1.0/24"}, DenyByDefault: true, ErrorHandler: func(c *mizu.Ctx) error { return c.JSON(403, map[string]string{ "error": "Access denied from your location", "ip": c.ClientIP(), }) }, })) ``` ### Route-Specific Filtering ```go theme={null} // Public routes app.Get("/", publicHandler) app.Get("/api", apiHandler) // Admin routes - localhost only adminFilter := ipfilter.Localhost() app.Get("/admin", adminHandler, adminFilter) app.Post("/admin/config", configHandler, adminFilter) ``` ### Group Filtering ```go theme={null} admin := app.Group("/admin") admin.Use(ipfilter.Allow("192.168.1.0/24")) admin.Get("/", adminDashboard) admin.Get("/users", listUsers) ``` ### Dynamic IP List ```go theme={null} func dynamicFilter() mizu.Middleware { var blockedIPs sync.Map // Background goroutine to update blocked IPs go func() { for { ips := fetchBlockedIPs() // From database/API for _, ip := range ips { blockedIPs.Store(ip, true) } time.Sleep(5 * time.Minute) } }() return func(next mizu.Handler) mizu.Handler { return func(c *mizu.Ctx) error { if _, blocked := blockedIPs.Load(c.ClientIP()); blocked { return c.Text(403, "Forbidden") } return next(c) } } } ``` ## API Reference ### Functions ```go theme={null} // Allow creates whitelist middleware (deny by default) func Allow(ips ...string) mizu.Middleware // Deny creates blacklist middleware func Deny(ips ...string) mizu.Middleware // New creates middleware with full options func New(opts Options) mizu.Middleware // Private allows only private network IPs func Private() mizu.Middleware // Localhost allows only localhost func Localhost() mizu.Middleware ``` ## CIDR Notation The middleware supports both single IPs and CIDR notation: | Format | Description | Example | | --------- | ------------- | ---------------- | | Single IP | Exact match | `192.168.1.100` | | IPv4 CIDR | Network range | `192.168.1.0/24` | | IPv6 | IPv6 address | `::1` | | IPv6 CIDR | IPv6 range | `fc00::/7` | ### Common CIDR Blocks ```go theme={null} "10.0.0.0/8" // 10.x.x.x (Class A private) "172.16.0.0/12" // 172.16-31.x.x (Class B private) "192.168.0.0/16" // 192.168.x.x (Class C private) "127.0.0.0/8" // Localhost "0.0.0.0/0" // All IPv4 "::/0" // All IPv6 ``` ## Technical Details ### Implementation Overview The ipfilter middleware uses Go's `net` package to parse and match IP addresses and CIDR ranges efficiently. The implementation follows these key principles: 1. **Network Parsing**: IP addresses and CIDR blocks are parsed at initialization time using `parseNetworks()`, converting them to `*net.IPNet` structures for efficient matching. 2. **IP Extraction**: Client IPs are extracted using one of two methods: * Direct extraction from `RemoteAddr` using `extractIP()` when not behind a proxy * Using `c.ClientIP()` when `TrustProxy` is enabled (reads X-Forwarded-For header) 3. **Filtering Logic**: The middleware applies a two-phase filtering process: * **Phase 1 - Deny List Check**: First checks if the IP is in the deny list. If found, immediately denies access. * **Phase 2 - Allow List Check**: If `DenyByDefault` is true, verifies the IP is in the allow list. Denies if not found. 4. **Single IP Normalization**: Single IP addresses are automatically converted to CIDR notation: * IPv4 addresses: converted to `/32` (e.g., `192.168.1.100/32`) * IPv6 addresses: converted to `/128` (e.g., `::1/128`) 5. **Error Handling**: Uses a custom `handleDenied()` function that either calls the user-provided `ErrorHandler` or returns a default 403 Forbidden response. ### Performance Considerations * IP parsing happens once during middleware initialization, not on every request * Network matching uses efficient `net.IPNet.Contains()` method * Deny list is checked before allow list to fail fast on blocked IPs * No regex or string manipulation on hot path ## Security Considerations 1. **Proxy Headers** - Only enable `TrustProxy` if behind a trusted proxy 2. **IP Spoofing** - X-Forwarded-For can be spoofed if not behind trusted proxy 3. **IPv6** - Consider both IPv4 and IPv6 addresses 4. **VPNs/Proxies** - Users may bypass IP restrictions using VPNs ## Best Practices * Use CIDR notation for ranges to simplify management * Enable `TrustProxy` only behind trusted load balancers * Combine with other authentication for sensitive areas * Log denied attempts for security monitoring ## Testing The ipfilter middleware includes comprehensive test coverage for all filtering scenarios: | Test Case | Description | Expected Behavior | | ------------------------------------ | --------------------------------------------------------------------------- | --------------------------------------------- | | `TestAllow/allows listed IP` | Request from IP within allowed CIDR range (192.168.1.100 in 192.168.1.0/24) | Returns 200 OK | | `TestAllow/allows specific IP` | Request from specific allowed IP (10.0.0.1) | Returns 200 OK | | `TestAllow/denies unlisted IP` | Request from IP not in allow list (203.0.113.1) | Returns 403 Forbidden | | `TestDeny/denies listed IP` | Request from IP in deny list CIDR range (192.168.1.100 in 192.168.1.0/24) | Returns 403 Forbidden | | `TestDeny/allows unlisted IP` | Request from IP not in deny list (10.0.0.1) | Returns 200 OK | | `TestNew_DenyTakesPrecedence` | IP present in both allow and deny lists (192.168.1.100) | Returns 403 Forbidden (deny wins) | | `TestNew_TrustProxy` | Request with X-Forwarded-For header when TrustProxy enabled | Uses X-Forwarded-For IP for filtering | | `TestNew_ErrorHandler` | Custom error handler when IP is blocked | Calls custom error handler with JSON response | | `TestPrivate` (192.168.1.1) | Request from private network IP (Class C) | Returns 200 OK | | `TestPrivate` (10.0.0.1) | Request from private network IP (Class A) | Returns 200 OK | | `TestPrivate` (172.16.0.1) | Request from private network IP (Class B) | Returns 200 OK | | `TestPrivate` (127.0.0.1) | Request from localhost | Returns 200 OK | | `TestPrivate` (8.8.8.8) | Request from public IP | Returns 403 Forbidden | | `TestLocalhost/allows localhost` | Request from 127.0.0.1 | Returns 200 OK | | `TestLocalhost/denies non-localhost` | Request from non-localhost IP (192.168.1.1) | Returns 403 Forbidden | ## Related Middlewares * [ratelimit](/middlewares/ratelimit) - Rate limiting by IP * [honeypot](/middlewares/honeypot) - Detect malicious IPs * [realip](/middlewares/realip) - Extract real client IP # JSON-RPC Source: https://docs.go-mizu.dev/middlewares/jsonrpc JSON-RPC protocol middleware for RPC-style APIs. ## Overview The `jsonrpc` middleware provides JSON-RPC 2.0 protocol support for building RPC-style APIs over HTTP. Use it when you need: * JSON-RPC 2.0 APIs * RPC-style communication * Batch request support ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/jsonrpc" ``` ## Quick Start ```go theme={null} app := mizu.New() rpc := jsonrpc.New() rpc.Register("add", func(params []int) (int, error) { return params[0] + params[1], nil }) app.Post("/rpc", rpc.Handler()) ``` ## Examples ### Register Methods ```go theme={null} rpc := jsonrpc.New() rpc.Register("math.add", func(a, b int) int { return a + b }) rpc.Register("math.multiply", func(a, b int) int { return a * b }) app.Post("/rpc", rpc.Handler()) ``` ### With Context ```go theme={null} rpc.RegisterWithContext("user.get", func(ctx context.Context, id string) (*User, error) { return userService.Get(ctx, id) }) ``` ### Batch Requests Automatically supported: ```json theme={null} [ {"jsonrpc": "2.0", "method": "add", "params": [1, 2], "id": 1}, {"jsonrpc": "2.0", "method": "add", "params": [3, 4], "id": 2} ] ``` ### Error Handling ```go theme={null} rpc.Register("divide", func(a, b int) (int, error) { if b == 0 { return 0, jsonrpc.Error(-32000, "Division by zero") } return a / b, nil }) ``` ## API Reference ### Functions ```go theme={null} // New creates JSON-RPC handler func New() *JSONRPC // Register adds a method func (j *JSONRPC) Register(name string, fn any) // RegisterWithContext adds a method with context func (j *JSONRPC) RegisterWithContext(name string, fn any) // Handler returns HTTP handler func (j *JSONRPC) Handler() mizu.Handler // Error creates JSON-RPC error func Error(code int, message string) error ``` ## Standard Error Codes | Code | Description | | ------ | ---------------- | | -32700 | Parse error | | -32600 | Invalid request | | -32601 | Method not found | | -32602 | Invalid params | | -32603 | Internal error | ## Technical Details ### Architecture The JSON-RPC middleware consists of three main components: 1. **Server**: The core `Server` struct manages method registration and request routing * Maintains a method registry (`map[string]Handler`) * Handles both single and batch requests * Validates JSON-RPC 2.0 protocol compliance 2. **Request/Response Types**: Structured types for protocol compliance * `Request`: Represents incoming JSON-RPC 2.0 requests with JSONRPC version, method, params, and optional ID * `Response`: Represents outgoing responses with result or error * `Error`: Standard JSON-RPC error structure with code, message, and optional data 3. **Handler Functions**: Method handlers accept params and return results or errors * Type: `func(params map[string]any) (any, error)` * Support custom error types via `NewError()` ### Request Processing Flow 1. **Request Reception**: HTTP POST body is read and parsed 2. **Batch Detection**: Checks if request starts with `[` to identify batch requests 3. **Validation**: Verifies JSON-RPC version is "2.0" 4. **Method Lookup**: Finds registered handler for the requested method 5. **Execution**: Invokes handler with parsed parameters 6. **Response Generation**: Returns result or error in JSON-RPC format 7. **Notification Handling**: Returns HTTP 204 No Content for requests without ID ### Batch Request Processing Batch requests are processed sequentially: * Each request in the array is validated and executed independently * Errors in one request don't affect others * Notifications (requests without ID) are excluded from the response array * Empty response arrays return HTTP 204 No Content ### Error Handling The middleware uses standard JSON-RPC error codes: * **-32700**: Parse error - Invalid JSON received * **-32600**: Invalid request - Request doesn't conform to JSON-RPC spec * **-32601**: Method not found - Requested method doesn't exist * **-32602**: Invalid params - Invalid method parameters * **-32603**: Internal error - Handler execution error Custom errors can be created using `NewError(code, message)` and will be properly serialized in responses. ## Best Practices * Use meaningful method names * Validate parameters * Return proper error codes * Document available methods ## Testing The middleware includes comprehensive test coverage for all core functionality: | Test Case | Description | Expected Behavior | | ------------------------------------- | ------------------------------------------------ | -------------------------------------------------------------------------------- | | `TestServer_Register` | Tests basic method registration and execution | Method handler executes successfully and returns correct result (3 for add(1,2)) | | `TestServer_MethodNotFound` | Tests calling an unregistered method | Returns JSON-RPC error with code -32601 (Method not found) | | `TestServer_InvalidJSONRPCVersion` | Tests request with wrong JSON-RPC version (1.0) | Returns JSON-RPC error with code -32600 (Invalid Request) | | `TestServer_Notification` | Tests notification handling (request without ID) | Handler executes but returns HTTP 204 No Content with no response body | | `TestServer_BatchRequest` | Tests batch request with multiple method calls | Processes all requests and returns array of responses with correct results | | `TestServer_ErrorInHandler` | Tests handler that returns custom error | Returns JSON-RPC error response with custom error message | | `TestMiddleware` (wrong method) | Tests middleware with non-POST HTTP method | Returns JSON-RPC error indicating method must be POST | | `TestMiddleware` (wrong content type) | Tests middleware with non-JSON content type | Returns JSON-RPC error indicating Content-Type must be application/json | | `TestParseError` | Tests invalid JSON in request body | Returns JSON-RPC error with code -32700 (Parse error) | | `TestNewError` | Tests custom error creation | Error object created with correct code and message | ## Related Middlewares * [graphql](/middlewares/graphql) - GraphQL support * [validator](/middlewares/validator) - Input validation # JWT Source: https://docs.go-mizu.dev/middlewares/jwt JSON Web Token authentication middleware for secure API access. ## Overview The `jwt` middleware provides JSON Web Token (JWT) authentication. It validates tokens from the Authorization header and makes claims available to handlers. Use it when you need: * Stateless API authentication * Token-based user sessions * Microservices authentication ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/jwt" ``` ## Quick Start ```go theme={null} app := mizu.New() secret := []byte("your-secret-key-at-least-32-bytes") // Protect routes with JWT app.Use(jwt.New(secret)) app.Get("/protected", func(c *mizu.Ctx) error { // Access claims claims := jwt.GetClaims(c) userID := jwt.Subject(c) return c.JSON(200, map[string]string{"user": userID}) }) ``` ## Configuration ### Options | Option | Type | Default | Description | | ---------------- | ------------------------------ | ------------------------ | ----------------------- | | `Secret` | `[]byte` | Required | HMAC secret key | | `TokenLookup` | `string` | `"header:Authorization"` | Where to find token | | `AuthScheme` | `string` | `"Bearer"` | Authorization scheme | | `Issuer` | `string` | `""` | Expected token issuer | | `ErrorHandler` | `func(*mizu.Ctx, error) error` | - | Custom error handler | | `SuccessHandler` | `func(*mizu.Ctx) error` | - | Called after validation | ### TokenLookup Formats * `header:Authorization` - From Authorization header * `query:token` - From query parameter * `cookie:jwt` - From cookie ## Examples ### Basic Usage ```go theme={null} secret := []byte("your-secret-key-at-least-32-bytes") app.Use(jwt.New(secret)) ``` ### With Issuer Validation ```go theme={null} app.Use(jwt.WithOptions(jwt.Options{ Secret: secret, Issuer: "my-app", })) ``` ### Token from Query Parameter ```go theme={null} app.Use(jwt.WithOptions(jwt.Options{ Secret: secret, TokenLookup: "query:token", AuthScheme: "", // No scheme for query params })) ``` ### Token from Cookie ```go theme={null} app.Use(jwt.WithOptions(jwt.Options{ Secret: secret, TokenLookup: "cookie:auth_token", })) ``` ### Custom Error Handler ```go theme={null} app.Use(jwt.WithOptions(jwt.Options{ Secret: secret, ErrorHandler: func(c *mizu.Ctx, err error) error { return c.JSON(401, map[string]string{ "error": "Invalid or expired token", }) }, })) ``` ### Accessing Claims ```go theme={null} app.Get("/profile", func(c *mizu.Ctx) error { // Get all claims claims := jwt.GetClaims(c) // Get specific claim email := claims["email"].(string) // Get subject (user ID) userID := jwt.Subject(c) return c.JSON(200, map[string]any{ "user_id": userID, "email": email, }) }) ``` ### Protected Group ```go theme={null} // Public routes app.Get("/", publicHandler) // Protected API routes api := app.Group("/api") api.Use(jwt.New(secret)) api.Get("/users", listUsers) api.Post("/users", createUser) ``` ## API Reference ### Functions ```go theme={null} // New creates JWT middleware with secret func New(secret []byte) mizu.Middleware // WithOptions creates JWT middleware with full configuration func WithOptions(opts Options) mizu.Middleware // GetClaims returns claims from context func GetClaims(c *mizu.Ctx) map[string]any // Subject returns the "sub" claim func Subject(c *mizu.Ctx) string ``` ## Standard Claims | Claim | Description | | ----- | ----------------- | | `sub` | Subject (user ID) | | `iss` | Issuer | | `exp` | Expiration time | | `iat` | Issued at | | `aud` | Audience | ## Technical Details ### Token Validation Process The JWT middleware implements a complete token validation workflow: 1. **Token Extraction**: The middleware supports three token sources: * Header: Extracts from Authorization header with configurable scheme (default: "Bearer") * Query: Extracts from URL query parameters * Cookie: Extracts from HTTP cookies 2. **Token Structure Validation**: Validates the token has exactly three parts (header.payload.signature) separated by dots. 3. **Signature Verification**: Uses HMAC-SHA256 to verify the token signature: * Decodes the signature from base64url encoding * Computes expected signature using the provided secret * Performs constant-time comparison to prevent timing attacks 4. **Payload Decoding**: Decodes the base64url-encoded payload and unmarshals JSON claims. 5. **Claims Validation**: Validates standard JWT claims: * `exp` (expiration): Rejects tokens past their expiration time * `nbf` (not before): Rejects tokens used before their valid time * `iss` (issuer): Validates against configured issuer if specified * `aud` (audience): Validates against configured audience list if specified (supports both string and array formats) ### Context Storage Claims are stored in the request context using a private context key. This prevents key collisions and ensures type safety. The `GetClaims()` and `Subject()` helper functions provide convenient access to the claims. ### Error Handling The middleware defines specific error types for different failure scenarios: * `ErrTokenMissing`: Returns 401 Unauthorized * `ErrTokenMalformed`, `ErrTokenInvalid`, `ErrTokenExpired`, `ErrTokenNotYetValid`, `ErrInvalidScheme`, `ErrInvalidIssuer`, `ErrInvalidAudience`: Return 403 Forbidden Custom error handlers can be configured via the `ErrorHandler` option for application-specific error responses. ## Security Considerations 1. **Use strong secrets** - At least 32 bytes for HS256 2. **Always validate expiration** - The middleware checks `exp` automatically 3. **Use HTTPS** - Never transmit tokens over HTTP 4. **Short expiration times** - Use refresh tokens for long sessions 5. **Validate issuer** - Prevent tokens from other applications ## Best Practices * Store secrets in environment variables * Use short-lived access tokens with refresh tokens * Include only necessary data in claims * Validate additional claims in handlers when needed ## Testing The JWT middleware includes comprehensive test coverage for all scenarios: | Test Case | Description | Expected Behavior | | ------------------------ | ------------------------------------------ | ------------------------------------------------- | | Valid Token | Token with valid signature and expiration | Returns 200 OK, request proceeds | | Missing Token | No Authorization header provided | Returns 401 Unauthorized | | Invalid Token | Malformed token structure | Returns 403 Forbidden | | Wrong Secret | Token signed with different secret | Returns 403 Forbidden, signature validation fails | | Expired Token | Token with past expiration time | Returns 403 Forbidden | | Valid Issuer | Token with matching issuer claim | Returns 200 OK | | Invalid Issuer | Token with non-matching issuer | Returns 403 Forbidden | | Missing Issuer | Token without issuer when required | Returns 403 Forbidden | | GetClaims | Extract all claims from context | Returns map with all token claims | | Subject | Extract subject claim | Returns "sub" claim value | | Query Lookup | Token in query parameter | Returns 200 OK, extracts from query string | | Cookie Lookup | Token in HTTP cookie | Returns 200 OK, extracts from cookie | | Cookie Missing | No cookie when expected | Returns 401 Unauthorized | | Not Before (Invalid) | Token with future "nbf" claim | Returns 403 Forbidden | | Not Before (Valid) | Token with past "nbf" claim | Returns 200 OK | | Valid Audience (String) | Token with matching audience string | Returns 200 OK | | Valid Audience (Array) | Token with matching audience in array | Returns 200 OK | | Invalid Audience | Token with non-matching audience | Returns 403 Forbidden | | Missing Audience | Token without audience when required | Returns 403 Forbidden | | GetClaims (No Claims) | Call GetClaims without JWT middleware | Returns nil | | Subject (No Claims) | Call Subject without JWT middleware | Returns empty string | | Subject (No Sub Claim) | Token without "sub" claim | Returns empty string | | Invalid Scheme | Wrong authorization scheme (e.g., "Basic") | Returns 403 Forbidden | | Custom Error Handler | Custom error handling function | Calls custom handler with custom status | | Custom Auth Scheme | Non-default scheme (e.g., "Token") | Returns 200 OK with custom scheme | | Malformed Parts | Token with wrong number of parts | Returns 403 Forbidden | | Invalid Base64 Signature | Signature not properly base64 encoded | Returns 403 Forbidden | | Invalid Base64 Payload | Payload not properly base64 encoded | Returns 403 Forbidden | | Invalid JSON Payload | Payload is not valid JSON | Returns 403 Forbidden | | Query Missing | No query parameter when expected | Returns 401 Unauthorized | | Panic (No Secret) | Options without secret | Panics with error message | | Panic (Invalid Lookup) | Invalid TokenLookup format | Panics with error message | ## Related Middlewares * [bearerauth](/middlewares/bearerauth) - Simple bearer token auth * [keyauth](/middlewares/keyauth) - API key authentication * [session](/middlewares/session) - Session-based authentication # Keep-Alive Source: https://docs.go-mizu.dev/middlewares/keepalive Connection keep-alive control middleware for HTTP connections. ## Overview The `keepalive` middleware controls HTTP keep-alive behavior, managing connection persistence for performance optimization. Use it when you need: * Connection persistence control * Resource management * Performance optimization ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/keepalive" ``` ## Quick Start ```go theme={null} app := mizu.New() // Enable keep-alive with defaults app.Use(keepalive.New()) ``` ## Configuration ### Options | Option | Type | Default | Description | | ------------- | --------------- | ------- | --------------------------- | | `Enabled` | `bool` | `true` | Enable keep-alive | | `MaxRequests` | `int` | `100` | Max requests per connection | | `Timeout` | `time.Duration` | `5s` | Keep-alive timeout | ## Examples ### Enable Keep-Alive ```go theme={null} app.Use(keepalive.New()) ``` ### Custom Settings ```go theme={null} app.Use(keepalive.New(keepalive.Options{ Timeout: 10 * time.Second, MaxRequests: 200, })) ``` ### Disable Keep-Alive ```go theme={null} app.Use(keepalive.New(keepalive.Options{ Enabled: false, })) ``` ### Per-Route Control ```go theme={null} // Disable for long-polling app.Get("/events", handler, keepalive.Disable()) // Enable for API app.Get("/api", handler, keepalive.New()) ``` ## API Reference ### Functions ```go theme={null} // New creates keep-alive middleware func New(opts ...Options) mizu.Middleware // Disable creates middleware that disables keep-alive func Disable() mizu.Middleware ``` ## HTTP Headers ``` Connection: keep-alive Keep-Alive: timeout=5, max=100 ``` ## Technical Details ### Implementation Overview The keepalive middleware manages HTTP connection persistence by controlling the `Connection` and `Keep-Alive` headers. The middleware follows a request-response flow: 1. **Configuration Initialization**: Default values are set during middleware creation (60s timeout, 100 max requests) 2. **Request Processing**: For each incoming request, the middleware: * Checks if keep-alive is disabled via `DisableKeepAlive` option * Inspects the client's `Connection` header to detect if the client requested connection closure * Sets appropriate response headers based on configuration and client preferences 3. **Header Management**: The middleware sets: * `Connection: keep-alive` to indicate persistent connection support * `Keep-Alive: timeout={seconds}, max={requests}` to specify connection parameters ### Key Components * **Options struct**: Configures timeout duration, maximum requests per connection, and enable/disable flag * **Default values**: 60 seconds timeout, 100 maximum requests when not specified * **Client negotiation**: Respects client's `Connection: close` header to gracefully close connections * **Helper functions**: Provides `Disable()`, `WithTimeout()`, and `WithMax()` for common configurations ### Header Format The `Keep-Alive` header uses the format: `timeout={seconds}, max={requests}` where: * `timeout`: Duration in seconds that the connection should remain open * `max`: Maximum number of requests allowed on this connection ## Best Practices * Use for high-traffic APIs * Set reasonable timeouts * Monitor connection counts * Consider disabling for long-polling ## Testing The keepalive middleware includes comprehensive test coverage: | Test Case | Description | Expected Behavior | | ------------------------------- | --------------------------------- | ----------------------------------------------------------------------------------------- | | `TestNew` | Default middleware creation | Sets `Connection: keep-alive` and `Keep-Alive` headers with default values (60s, 100 max) | | `TestWithOptions_CustomTimeout` | Custom timeout configuration | Sets `Keep-Alive: timeout=120, max=100` when 120s timeout is specified | | `TestWithOptions_CustomMax` | Custom max requests configuration | Sets `Keep-Alive: timeout=60, max=500` when 500 max requests is specified | | `TestWithOptions_Disable` | Disable keep-alive option | Sets `Connection: close` header when `DisableKeepAlive` is true | | `TestWithOptions_ClientClose` | Client requests connection close | Honors client's `Connection: close` header and responds with `Connection: close` | | `TestDisable` | Disable() helper function | Sets `Connection: close` header using convenience function | | `TestWithTimeout` | WithTimeout() helper function | Sets `Keep-Alive: timeout=30, max=100` when using 30s timeout helper | | `TestWithMax` | WithMax() helper function | Sets `Keep-Alive: timeout=60, max=200` when using 200 max requests helper | ## Related Middlewares * [timeout](/middlewares/timeout) - Request timeout * [maxconns](/middlewares/maxconns) - Connection limits # Key Auth Source: https://docs.go-mizu.dev/middlewares/keyauth API key authentication middleware for securing APIs. ## Overview The `keyauth` middleware provides API key authentication. It validates API keys from headers, query parameters, or cookies. Perfect for machine-to-machine authentication and public APIs. Use it when you need: * API key authentication * Service-to-service authentication * Third-party API access control * Simple API protection without user sessions ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/keyauth" ``` ## Quick Start ```go theme={null} app := mizu.New() // Validate against a list of valid keys app.Use(keyauth.New(keyauth.ValidateKeys( "key-abc123", "key-def456", ))) ``` ## Configuration ### Options | Option | Type | Default | Description | | -------------- | ------------------------------ | -------------------- | ------------------------------- | | `Validator` | `KeyValidator` | - | Function to validate API keys | | `KeyLookup` | `string` | `"header:X-API-Key"` | Where to find the key | | `AuthScheme` | `string` | `""` | Scheme prefix for header lookup | | `ErrorHandler` | `func(*mizu.Ctx, error) error` | - | Custom error handler | ### KeyLookup Format The `KeyLookup` option uses the format `"source:name"`: | Source | Example | Description | | -------- | -------------------- | ------------------------- | | `header` | `"header:X-API-Key"` | Read from header | | `query` | `"query:api_key"` | Read from query parameter | | `cookie` | `"cookie:api_key"` | Read from cookie | ## Examples ### Static API Keys ```go theme={null} validKeys := keyauth.ValidateKeys( "production-key-abc", "production-key-xyz", ) app.Use(keyauth.New(validKeys)) ``` ### Database Lookup ```go theme={null} app.Use(keyauth.New(func(key string) (bool, error) { // Look up key in database apiKey, err := db.GetAPIKey(key) if err != nil { if err == sql.ErrNoRows { return false, nil // Key not found } return false, err // Database error } // Check if key is active and not expired if !apiKey.Active || apiKey.ExpiresAt.Before(time.Now()) { return false, nil } return true, nil })) ``` ### Query Parameter ```go theme={null} // Accept key via ?api_key=xxx app.Use(keyauth.WithOptions(keyauth.Options{ KeyLookup: "query:api_key", Validator: func(key string) (bool, error) { return key == "valid-key", nil }, })) ``` ### Multiple Lookup Sources ```go theme={null} // Try header first, then query parameter app.Use(keyauth.WithOptions(keyauth.Options{ KeyLookup: "header:X-API-Key", Validator: func(key string) (bool, error) { return validateKey(key) }, })) // Add fallback for query app.Use(keyauth.WithOptions(keyauth.Options{ KeyLookup: "query:api_key", Validator: func(key string) (bool, error) { return validateKey(key) }, })) ``` ### With Auth Scheme ```go theme={null} // Expect "ApiKey xxx" in Authorization header app.Use(keyauth.WithOptions(keyauth.Options{ KeyLookup: "header:Authorization", AuthScheme: "ApiKey", Validator: validateKey, })) ``` ### Custom Error Handler ```go theme={null} app.Use(keyauth.WithOptions(keyauth.Options{ Validator: validateKey, ErrorHandler: func(c *mizu.Ctx, err error) error { if err == keyauth.ErrKeyMissing { return c.JSON(401, map[string]string{ "error": "API key required", "hint": "Include X-API-Key header", }) } return c.JSON(403, map[string]string{ "error": "Invalid API key", }) }, })) ``` ### Access API Key in Handler ```go theme={null} func handler(c *mizu.Ctx) error { // Get the validated API key key := keyauth.Get(c) // Log for audit c.Logger().Info("API request", "key", key[:8]+"...") return c.JSON(200, data) } ``` ### Rate Limiting Per Key ```go theme={null} // Custom rate limiter based on API key func rateLimit() mizu.Middleware { return func(next mizu.Handler) mizu.Handler { return func(c *mizu.Ctx) error { key := keyauth.Get(c) if !rateLimiter.Allow(key) { return c.Text(429, "Rate limit exceeded") } return next(c) } } } app.Use(keyauth.New(validateKey)) app.Use(rateLimit()) ``` ## API Reference ### Functions ```go theme={null} // New creates middleware with validator func New(validator KeyValidator) mizu.Middleware // WithOptions creates middleware with full configuration func WithOptions(opts Options) mizu.Middleware // ValidateKeys creates validator for static key list func ValidateKeys(keys ...string) KeyValidator ``` ### Extracting Key ```go theme={null} // FromContext extracts API key from context func FromContext(c *mizu.Ctx) string // Get is an alias for FromContext func Get(c *mizu.Ctx) string ``` ### Types ```go theme={null} // KeyValidator validates an API key type KeyValidator func(key string) (valid bool, err error) ``` ### Error Types ```go theme={null} var ( ErrKeyMissing // API key not found in request ErrKeyInvalid // API key validation failed ) ``` ## Technical Details ### Implementation Overview The keyauth middleware implements a flexible API key authentication system with support for multiple key sources (headers, query parameters, and cookies) and customizable validation logic. **Key Components:** 1. **Key Extraction**: The middleware parses the `KeyLookup` option to determine where to extract the API key from: * Header extraction with optional auth scheme prefix support * Query parameter extraction * Cookie extraction 2. **Validation Pipeline**: * Extract key from the configured source * Return 401 Unauthorized if key is missing * Execute custom validator function * Return 403 Forbidden if validation fails or returns error * Store validated key in request context for downstream handlers 3. **Context Storage**: Validated keys are stored using a private `contextKey` type, ensuring type-safe retrieval and preventing context key collisions. 4. **Error Handling**: The middleware provides two error handling paths: * Custom error handler (if configured) * Default handler: 401 for missing keys, 403 for invalid keys or validation errors ### Key Extraction Logic ``` KeyLookup Format: "source:name" Header Source: - Reads from request.Header.Get(name) - If AuthScheme is set, strips the scheme prefix - Example: "header:Authorization" with AuthScheme="ApiKey" "ApiKey abc123" -> "abc123" Query Source: - Reads from URL query parameter - Example: "query:api_key" -> ?api_key=value Cookie Source: - Reads from HTTP cookie - Example: "cookie:auth_token" ``` ### Validator Function The `KeyValidator` function signature: ```go theme={null} type KeyValidator func(key string) (valid bool, err error) ``` **Return Values:** * `(true, nil)` - Key is valid, allow request * `(false, nil)` - Key is invalid, return 403 Forbidden * `(_, error)` - Validation error occurred, return 403 with error ### Static Key Validation The `ValidateKeys` helper creates a validator using a map for O(1) lookup: ```go theme={null} func ValidateKeys(keys ...string) KeyValidator { keySet := make(map[string]bool) for _, k := range keys { keySet[k] = true } return func(key string) (bool, error) { return keySet[key], nil } } ``` ## Security Considerations 1. **Key Generation** - Use cryptographically secure random keys 2. **Key Storage** - Hash keys in database, never store plain text 3. **Key Rotation** - Support key rotation without downtime 4. **Scoping** - Consider scoped keys with limited permissions 5. **Logging** - Log key usage but not full keys ### Secure Key Generation ```go theme={null} import "crypto/rand" func generateAPIKey() string { b := make([]byte, 32) rand.Read(b) return "sk_" + hex.EncodeToString(b) } ``` ## Best Practices * Use different keys for different environments * Implement key expiration * Add rate limiting per key * Log API key usage for auditing * Support multiple keys per user/service * Provide key management API ## Testing The keyauth middleware includes comprehensive test coverage for all functionality: | Test Case | Description | Expected Behavior | | -------------------------------------------- | ---------------------------------------------------- | ------------------------------------------------------------- | | **TestNew - valid key** | Valid API key provided in X-API-Key header | Returns 200 OK, allows request through | | **TestNew - invalid key** | Invalid API key provided | Returns 403 Forbidden, blocks request | | **TestNew - missing key** | No API key provided | Returns 401 Unauthorized, blocks request | | **TestWithOptions\_QueryLookup** | API key provided via query parameter | Returns 200 OK when key is valid | | **TestWithOptions\_CookieLookup** | API key provided via cookie | Returns 200 OK when key is valid | | **TestWithOptions\_AuthScheme** | API key with "ApiKey" scheme in Authorization header | Strips scheme prefix, validates key, returns 200 OK | | **TestWithOptions\_ErrorHandler** | Custom error handler configured | Custom error handler is invoked, returns custom JSON response | | **TestWithOptions\_ValidatorError** | Validator returns error (e.g., database error) | Returns 403 Forbidden with error message | | **TestFromContext** | Retrieve API key from context after validation | Returns the validated API key string | | **TestGet** | Test Get() function as alias for FromContext() | Both functions return the same key value | | **TestValidateKeys - key1** | Static validator with "key1" in allowed list | Returns valid=true | | **TestValidateKeys - key2** | Static validator with "key2" in allowed list | Returns valid=true | | **TestValidateKeys - key3** | Static validator with "key3" in allowed list | Returns valid=true | | **TestValidateKeys - key4** | Static validator with "key4" not in allowed list | Returns valid=false | | **TestValidateKeys - empty** | Static validator with empty string | Returns valid=false | | **TestWithOptions\_Panics - no validator** | Create middleware without validator | Panics with error message | | **TestWithOptions\_Panics - invalid lookup** | Create middleware with invalid KeyLookup format | Panics with error message | | **TestErrors** | Verify error constants | ErrKeyMissing and ErrKeyInvalid return correct messages | ### Running Tests ```bash theme={null} # Run all keyauth tests go test ./middlewares/keyauth # Run with coverage go test -cover ./middlewares/keyauth # Run specific test go test -run TestNew ./middlewares/keyauth ``` ## Related Middlewares * [basicauth](/middlewares/basicauth) - Username/password authentication * [bearerauth](/middlewares/bearerauth) - Bearer token authentication * [ratelimit](/middlewares/ratelimit) - Rate limiting # Language Source: https://docs.go-mizu.dev/middlewares/language Language detection middleware for internationalization. ## Overview The `language` middleware detects user language preference from Accept-Language header, cookies, or query parameters. Use it when you need: * Multi-language support * Content localization * Language-based routing ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/language" ``` ## Quick Start ```go theme={null} app := mizu.New() app.Use(language.New(language.Options{ Supported: []string{"en", "es", "fr", "de"}, Default: "en", })) app.Get("/", func(c *mizu.Ctx) error { lang := language.Get(c) return c.JSON(200, map[string]string{"language": lang}) }) ``` ## Configuration ### Options | Option | Type | Default | Description | | ------------ | ---------- | --------------- | ------------------- | | `Supported` | `[]string` | Required | Supported languages | | `Default` | `string` | First supported | Default language | | `QueryKey` | `string` | `"lang"` | Query parameter | | `CookieName` | `string` | `"lang"` | Cookie name | ## Examples ### Basic Detection ```go theme={null} app.Use(language.New(language.Options{ Supported: []string{"en", "es", "fr"}, Default: "en", })) ``` ### From Query Parameter ```go theme={null} // GET /page?lang=es app.Use(language.New(language.Options{ Supported: []string{"en", "es"}, QueryKey: "language", })) ``` ### Persist in Cookie ```go theme={null} app.Use(language.New(language.Options{ Supported: []string{"en", "es", "fr"}, CookieName: "preferred_language", })) ``` ### Language-Based Routing ```go theme={null} app.Get("/:lang/page", func(c *mizu.Ctx) error { lang := c.Param("lang") // Validate and use language return c.JSON(200, getContent(lang)) }) ``` ## Detection Order 1. Query parameter (?lang=es) 2. Cookie (lang=es) 3. Accept-Language header 4. Default language ## API Reference ### Functions ```go theme={null} // New creates language middleware func New(opts Options) mizu.Middleware // Get returns detected language func Get(c *mizu.Ctx) string // Set sets language preference func Set(c *mizu.Ctx, lang string) ``` ## Technical Details ### Implementation Architecture The language middleware uses a multi-source detection strategy with priority-based resolution: 1. **Context Storage**: Detected language is stored in the request context using a private `contextKey` type 2. **Case-Insensitive Matching**: All language comparisons are performed case-insensitively using lowercase normalization 3. **Regional Support**: Handles both base language codes (e.g., "en") and regional variants (e.g., "en-US", "en-GB") 4. **Quality-Based Parsing**: Accept-Language header parsing respects quality values (q-values) for prioritization ### Key Components * **isSupported()**: Validates if a language code exists in the supported languages map * **normalize()**: Converts detected language codes to the exact format defined in supported languages * **parseAcceptLanguage()**: Parses Accept-Language header with quality values, sorts by quality descending * **Path Prefix Handling**: When enabled, strips the language prefix from the URL path (e.g., `/fr/page` becomes `/page`) ### Configuration Defaults ```go theme={null} // Default values when not specified QueryParam: "lang" // Query parameter name CookieName: "lang" // Cookie name Header: "Accept-Language" // HTTP header PathPrefix: false // Path prefix detection disabled Default: supported[0] // First supported language ``` ## Best Practices * Support common language codes * Provide language switcher UI * Persist user preference * Use ISO 639-1 codes (en, es, fr) ## Testing The middleware includes comprehensive test coverage for all detection mechanisms and edge cases: | Test Case | Description | Expected Behavior | | -------------------------------------- | ----------------------------------------------------- | ----------------------------------------------------------------- | | TestNew | Basic middleware creation with Accept-Language header | Detects language from Accept-Language header ("es") | | TestWithOptions\_QueryParam | Custom query parameter detection | Detects language from query parameter "language=de" | | TestWithOptions\_Cookie | Custom cookie name detection | Detects language from cookie "user\_lang=ja" | | TestWithOptions\_PathPrefix | Path prefix language detection | Detects "fr" from "/fr/page" and strips prefix to "/page" | | TestWithOptions\_AcceptLanguageQuality | Quality value parsing in Accept-Language | Selects "fr" with q=0.9 over "en" with q=0.5 | | TestWithOptions\_AcceptLanguageRegion | Regional language variant detection | Detects exact match "en-GB" from regional variants | | TestWithOptions\_Fallback | Default language fallback | Falls back to "en" when unsupported language "zh" requested | | TestWithOptions\_Priority | Detection priority order | Query parameter ("es") takes priority over Accept-Language ("fr") | | TestFromContext | Context retrieval alias function | Get() and FromContext() return identical values | | TestWithOptions\_NoHeader | No language hints provided | Falls back to default language "en" | | TestWithOptions\_CaseInsensitive | Case-insensitive language matching | Matches "es" query to "ES" in supported languages | ## Related Middlewares * [timezone](/middlewares/timezone) - Timezone detection * [vary](/middlewares/vary) - Vary headers # Last Modified Source: https://docs.go-mizu.dev/middlewares/lastmodified Last-Modified header middleware for conditional requests. ## Overview The `lastmodified` middleware handles Last-Modified headers and If-Modified-Since conditional requests for efficient caching. Use it when you need: * Date-based caching * Conditional GET requests * Bandwidth optimization ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/lastmodified" ``` ## Quick Start ```go theme={null} app := mizu.New() app.Use(lastmodified.New()) app.Get("/resource", func(c *mizu.Ctx) error { lastmodified.Set(c, resource.UpdatedAt) return c.JSON(200, resource) }) ``` ## Examples ### Set Last-Modified ```go theme={null} app.Get("/article/:id", func(c *mizu.Ctx) error { article := getArticle(c.Param("id")) lastmodified.Set(c, article.UpdatedAt) return c.JSON(200, article) }) ``` ### Auto Last-Modified ```go theme={null} app.Use(lastmodified.Auto(func(c *mizu.Ctx) time.Time { // Return modification time based on request return getModTime(c.Request().URL.Path) })) ``` ### Static Files ```go theme={null} // Last-Modified is automatic for static files app.Use(static.New("./public")) ``` ## How It Works 1. Handler sets Last-Modified 2. Client receives Last-Modified header 3. Next request includes If-Modified-Since 4. If not modified, returns 304 Not Modified 5. If modified, returns full response ## API Reference ### Functions ```go theme={null} // New creates Last-Modified middleware func New() mizu.Middleware // Set sets Last-Modified for response func Set(c *mizu.Ctx, t time.Time) // Auto creates auto Last-Modified middleware func Auto(getModTime func(*mizu.Ctx) time.Time) mizu.Middleware ``` ## Technical Details The middleware implementation follows HTTP RFC 7232 specifications for conditional requests: * **Time Format**: Uses HTTP-date format (RFC 5322) for Last-Modified headers * **Time Comparison**: Truncates to seconds precision for accurate comparison * **Method Filtering**: Only processes GET and HEAD requests * **Header Parsing**: Uses `http.ParseTime` which supports multiple date formats * **304 Response**: Returns Not Modified when If-Modified-Since >= Last-Modified ### Implementation Features * **Options Configuration**: `Options` struct allows customization with `TimeFunc` and `SkipPaths` * **Path Skipping**: Can skip specific paths using the `SkipPaths` option * **Zero Time Handling**: Skips middleware processing when time function returns zero time * **UTC Normalization**: Automatically converts times to UTC before setting headers ### Helper Functions * `Static(t time.Time)`: Creates middleware with a fixed modification time * `Now()`: Uses current time for each request (useful for development/testing) * `StartupTime()`: Uses application startup time (useful for versioned assets) * `FromHeader(header string)`: Reads modification time from a custom request header ## Best Practices * Use with ETag for robust caching * Set accurate modification times * Use for static and semi-static content * Combine with Cache-Control ## Testing The middleware includes comprehensive test coverage for various scenarios: | Test Case | Description | Expected Behavior | | -------------------------------- | --------------------------------------- | -------------------------------------------- | | TestNew | Basic middleware initialization | Sets Last-Modified header on response | | TestIfModifiedSince\_NotModified | Request with matching If-Modified-Since | Returns 304 Not Modified status | | TestIfModifiedSince\_Modified | Request with older If-Modified-Since | Returns 200 OK with full response | | TestWithOptions\_SkipPaths | Path configured to be skipped | No Last-Modified header set | | TestSkipPostMethod | POST request to endpoint | No Last-Modified header (only GET/HEAD) | | TestStatic | Static time middleware | Returns fixed Last-Modified time | | TestNow | Current time middleware | Sets Last-Modified to current time | | TestStartupTime | Startup time middleware | Consistent time across multiple requests | | TestFromHeader | Custom header as time source | Reads and sets time from X-Resource-Modified | | TestZeroTime | Time function returns zero value | No Last-Modified header set | ## Related Middlewares * [etag](/middlewares/etag) - ETag generation * [cache](/middlewares/cache) - Cache-Control headers * [static](/middlewares/static) - Static files # Logger Source: https://docs.go-mizu.dev/middlewares/logger Request logging middleware with customizable format and output. ## Overview The `logger` middleware logs HTTP requests with configurable format, output destination, and filtering. It captures method, path, status, latency, and custom fields. Use it when you need: * HTTP request logging * Access logs for debugging * Request timing metrics ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/logger" ``` ## Quick Start ```go theme={null} app := mizu.New() // Default logger (stdout) app.Use(logger.New()) app.Get("/", func(c *mizu.Ctx) error { return c.Text(200, "Hello!") }) // Output: 2024/01/15 10:30:00 | 200 | 1.2ms | GET / ``` ## Configuration ### Options | Option | Type | Default | Description | | -------- | ---------------------- | -------------- | ------------------------- | | `Output` | `io.Writer` | `os.Stdout` | Log output destination | | `Format` | `string` | Default format | Log format template | | `Skip` | `func(*mizu.Ctx) bool` | `nil` | Skip logging for requests | ### Format Tags | Tag | Description | | ------------------ | -------------------- | | `${method}` | HTTP method | | `${path}` | Request path | | `${status}` | Response status code | | `${latency}` | Request duration | | `${ip}` | Client IP address | | `${host}` | Request host | | `${protocol}` | HTTP protocol | | `${referer}` | Referer header | | `${user_agent}` | User-Agent header | | `${bytes_out}` | Response size | | `${query}` | Query string | | `${header:X-Name}` | Custom header value | ## Examples ### Default Logger ```go theme={null} app.Use(logger.New()) ``` ### Custom Format ```go theme={null} app.Use(logger.WithOptions(logger.Options{ Format: "[${method}] ${path} -> ${status}\n", })) // Output: [GET] /api/users -> 200 ``` ### JSON Format ```go theme={null} app.Use(logger.WithOptions(logger.Options{ Format: `{"method":"${method}","path":"${path}","status":${status},"latency":"${latency}"}` + "\n", })) ``` ### Write to File ```go theme={null} file, _ := os.OpenFile("access.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) app.Use(logger.WithOptions(logger.Options{ Output: file, })) ``` ### Skip Health Checks ```go theme={null} app.Use(logger.WithOptions(logger.Options{ Skip: func(c *mizu.Ctx) bool { return c.Request().URL.Path == "/health" }, })) ``` ### Include Request ID ```go theme={null} app.Use(logger.WithOptions(logger.Options{ Format: "${header:X-Request-ID} | ${method} ${path} | ${status}\n", })) ``` ### Full Access Log ```go theme={null} app.Use(logger.WithOptions(logger.Options{ Format: `${ip} - [${time}] "${method} ${path} ${protocol}" ${status} ${bytes_out} "${referer}" "${user_agent}"` + "\n", })) ``` ## API Reference ### Functions ```go theme={null} // New creates logger middleware with defaults func New() mizu.Middleware // WithOptions creates logger middleware with configuration func WithOptions(opts Options) mizu.Middleware ``` ## Technical Details ### Implementation Architecture The logger middleware captures HTTP request metrics by wrapping the response writer and measuring execution time: 1. **Response Writer Wrapping**: A custom `responseWriter` type wraps the standard `http.ResponseWriter` to capture: * HTTP status code (defaults to 200) * Response size in bytes 2. **Timing Measurement**: Uses `time.Now()` before request processing and `time.Since()` after to calculate latency with nanosecond precision. 3. **Client IP Detection**: Implements intelligent IP extraction with fallback chain: * Checks `X-Forwarded-For` header (uses first IP if comma-separated list) * Checks `X-Real-IP` header * Falls back to `RemoteAddr` from the request 4. **Format String Processing**: The `formatLog` function performs template tag replacement: * Basic tags use `strings.ReplaceAll()` for simple substitution * Dynamic tags (`${header:name}`, `${form:name}`) use index-based parsing to extract custom values * Supports all standard HTTP request/response attributes 5. **Skip Function**: Optional predicate function allows conditional logging based on request context, executed before timing starts to avoid overhead. ### Performance Characteristics * Minimal overhead: Single writer wrap and string replacement operations * Zero allocations for skipped requests * String concatenation deferred until after request completion * No buffering of response body (streaming-friendly) ## Best Practices * Skip logging for health check endpoints * Use structured (JSON) format for log aggregation * Include request IDs for tracing * Log to stderr in containers for proper log handling ## Testing The logger middleware includes comprehensive test coverage for all features and edge cases: | Test Case | Description | Expected Behavior | | ------------------------------- | ----------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | | `TestNew` | Default logger with standard output | Logs contain status code (200), HTTP method (GET), and request path (/test) | | `TestWithOptions_CustomFormat` | Custom format string with method, path, and status | Output matches exact format: `[GET] /api -> 201\n` | | `TestWithOptions_Skip` | Skip function filtering health check endpoint | No log output for /health endpoint, logs /api endpoint normally | | `TestWithOptions_Headers` | Custom header extraction using `${header:X-Request-ID}` | Logs the exact value of the X-Request-ID header (test-123) | | `TestWithOptions_AllTags` | All available format tags (host, protocol, referer, user\_agent, bytes\_out, query) | Log contains all requested tag values from the HTTP request | | `TestWithOptions_XForwardedFor` | Client IP extraction from X-Forwarded-For header | Extracts first IP from comma-separated list (1.2.3.4) | | `TestWithOptions_XRealIP` | Client IP extraction from X-Real-IP header | Uses X-Real-IP value (10.0.0.1) when X-Forwarded-For not present | | `TestDefaultOutput` | Default output configuration (stdout) | Middleware executes without panic, returns 200 status | ## Related Middlewares * [requestid](/middlewares/requestid) - Request ID generation * [requestlog](/middlewares/requestlog) - Detailed request logging * [audit](/middlewares/audit) - Audit logging * [timing](/middlewares/timing) - Server-Timing header # Maintenance Source: https://docs.go-mizu.dev/middlewares/maintenance Maintenance mode middleware for scheduled downtime. ## Overview The `maintenance` middleware enables maintenance mode for your application, returning a 503 Service Unavailable response with optional whitelist support for IPs and paths. ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/maintenance" ``` ## Quick Start ```go theme={null} app := mizu.New() // Enable maintenance mode app.Use(maintenance.New(true)) ``` ## Configuration | Option | Type | Default | Description | | ---------------- | -------------- | -------------------------------- | ----------------------------- | | `Enabled` | `bool` | `false` | Enable maintenance mode | | `Message` | `string` | `"Service is under maintenance"` | Response message | | `RetryAfter` | `int` | `3600` | Retry-After header in seconds | | `StatusCode` | `int` | `503` | HTTP status code | | `Handler` | `mizu.Handler` | - | Custom maintenance handler | | `Whitelist` | `[]string` | - | Allowed IP addresses | | `WhitelistPaths` | `[]string` | - | Paths that bypass maintenance | | `Check` | `func() bool` | - | Dynamic maintenance check | ## Examples ### Basic Maintenance Mode ```go theme={null} // Enabled from start app.Use(maintenance.New(true)) // All requests return 503 ``` ### Custom Message ```go theme={null} app.Use(maintenance.WithOptions(maintenance.Options{ Enabled: true, Message: "We're upgrading our systems. Back soon!", })) ``` ### Custom Handler ```go theme={null} app.Use(maintenance.WithOptions(maintenance.Options{ Enabled: true, Handler: func(c *mizu.Ctx) error { return c.HTML(503, ` Maintenance

    Under Maintenance

    We'll be back shortly.

    `) }, })) ``` ### Whitelist IPs ```go theme={null} app.Use(maintenance.WithOptions(maintenance.Options{ Enabled: true, Whitelist: []string{ "192.168.1.100", // Admin workstation "10.0.0.0/8", // Internal network }, })) ``` ### Whitelist Paths ```go theme={null} app.Use(maintenance.WithOptions(maintenance.Options{ Enabled: true, WhitelistPaths: []string{ "/health", // Health checks "/status", // Status endpoint "/api/webhooks", // Critical webhooks }, })) ``` ### Dynamic Control ```go theme={null} mode := maintenance.NewMode(maintenance.Options{ Message: "Maintenance in progress", }) app.Use(mode.Middleware()) // Enable/disable dynamically app.Post("/admin/maintenance/enable", func(c *mizu.Ctx) error { mode.Enable() return c.Text(200, "Maintenance enabled") }) app.Post("/admin/maintenance/disable", func(c *mizu.Ctx) error { mode.Disable() return c.Text(200, "Maintenance disabled") }) app.Get("/admin/maintenance/status", func(c *mizu.Ctx) error { return c.JSON(200, map[string]bool{ "enabled": mode.IsEnabled(), }) }) ``` ### Toggle Maintenance ```go theme={null} mode := maintenance.NewMode(maintenance.Options{}) app.Post("/admin/maintenance/toggle", func(c *mizu.Ctx) error { mode.Toggle() return c.JSON(200, map[string]bool{ "enabled": mode.IsEnabled(), }) }) ``` ### Scheduled Maintenance ```go theme={null} // Maintenance window: Dec 15, 2024 2:00 AM - 4:00 AM UTC start := time.Date(2024, 12, 15, 2, 0, 0, 0, time.UTC) end := time.Date(2024, 12, 15, 4, 0, 0, 0, time.UTC) app.Use(maintenance.ScheduledMaintenance(start, end)) ``` ### Check Function ```go theme={null} app.Use(maintenance.WithOptions(maintenance.Options{ Check: func() bool { // Check external flag (e.g., from database or config) return config.IsMaintenanceMode() }, })) ``` ### Environment-Based ```go theme={null} app.Use(maintenance.WithOptions(maintenance.Options{ Check: func() bool { return os.Getenv("MAINTENANCE_MODE") == "true" }, WhitelistPaths: []string{"/health"}, })) ``` ### With Retry-After ```go theme={null} app.Use(maintenance.WithOptions(maintenance.Options{ Enabled: true, RetryAfter: 1800, // 30 minutes })) // Response includes: Retry-After: 1800 ``` ### JSON Response ```go theme={null} app.Use(maintenance.WithOptions(maintenance.Options{ Enabled: true, Handler: func(c *mizu.Ctx) error { c.Writer().Header().Set("Retry-After", "3600") return c.JSON(503, map[string]any{ "status": "maintenance", "message": "Service is under maintenance", "retry_after": 3600, }) }, })) ``` ## API Reference ```go theme={null} func New(enabled bool) mizu.Middleware func WithOptions(opts Options) mizu.Middleware func ScheduledMaintenance(start, end time.Time) mizu.Middleware func NewMode(opts Options) *Mode // Mode methods func (m *Mode) Enable() func (m *Mode) Disable() func (m *Mode) Toggle() func (m *Mode) IsEnabled() bool func (m *Mode) Middleware() mizu.Middleware ``` ## Response Headers ``` HTTP/1.1 503 Service Unavailable Retry-After: 3600 Content-Type: text/plain Service is under maintenance ``` ## Technical Details ### Implementation Architecture The maintenance middleware is implemented with a layered approach for flexibility and performance: 1. **Static Configuration** - Uses `Options` struct to configure enabled state, messages, status codes, and retry headers 2. **Dynamic Mode Control** - `Mode` type provides thread-safe atomic operations for runtime enable/disable toggling 3. **Bypass Mechanisms** - Implements two bypass layers: * IP-based whitelisting via `Whitelist` option (checks X-Forwarded-For, X-Real-IP, and RemoteAddr) * Path-based whitelisting via `WhitelistPaths` option (exact path matching) 4. **Custom Check Function** - Supports dynamic maintenance state via `Check` function, which takes precedence over static `Enabled` flag 5. **Scheduled Maintenance** - Uses time-based checks to automatically enable/disable maintenance during specific windows ### Key Components * **Options**: Configuration struct with fields for enabled state, messages, status codes, handlers, and whitelists * **Mode**: Thread-safe maintenance mode controller using `atomic.Int32` for concurrent read/write operations * **WithOptions**: Core middleware factory that processes configuration and returns the middleware function * **ScheduledMaintenance**: Helper function that creates time-windowed maintenance middleware * **getClientIP**: Helper function to extract client IP from various headers (X-Forwarded-For, X-Real-IP, RemoteAddr) * **itoa**: Custom integer-to-string conversion for Retry-After header (avoids fmt package overhead) ### Execution Flow 1. Check if maintenance is enabled (via `Check` function or `Enabled` flag) 2. If disabled, pass request to next handler 3. If enabled, check IP whitelist (bypass if matched) 4. Check path whitelist (bypass if matched) 5. Execute custom handler if provided, or return default 503 response with Retry-After header ### Thread Safety The `Mode` type uses `atomic` operations for thread-safe state management: * `Enable()` and `Disable()` use `atomic.StoreInt32` * `IsEnabled()` uses `atomic.LoadInt32` * `Toggle()` uses `atomic.CompareAndSwapInt32` for lock-free toggling ## Best Practices * Whitelist health check endpoints for monitoring * Use scheduled maintenance for planned downtime * Whitelist admin IPs for debugging * Set appropriate Retry-After values * Provide helpful maintenance messages * Use dynamic control for emergency maintenance ## Testing The maintenance middleware includes comprehensive test coverage for all scenarios: | Test Case | Description | Expected Behavior | | ------------------------------------------------------- | --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | `TestNew` | Basic maintenance mode enabled | Returns 503 with default message "Service is under maintenance" | | `TestNew_Disabled` | Maintenance mode disabled | Returns 200 and passes through to handler | | `TestWithOptions_CustomMessage` | Custom maintenance message | Returns 503 with custom message "We'll be back soon!" | | `TestWithOptions_RetryAfter` | Custom retry-after value | Sets Retry-After header to 7200 seconds | | `TestWithOptions_Whitelist` (whitelisted IP) | IP in whitelist accesses endpoint | Returns 200 and bypasses maintenance mode | | `TestWithOptions_Whitelist` (non-whitelisted IP) | IP not in whitelist accesses endpoint | Returns 503 in maintenance mode | | `TestWithOptions_WhitelistPaths` (whitelisted path) | Access to /health whitelisted path | Returns 200 and bypasses maintenance mode | | `TestWithOptions_WhitelistPaths` (non-whitelisted path) | Access to /api non-whitelisted path | Returns 503 in maintenance mode | | `TestWithOptions_CustomHandler` | Custom handler for maintenance response | Returns JSON response with custom handler | | `TestMode` | Mode creation, enable, disable, toggle | Correctly changes state: disabled by default, enabled after Enable(), disabled after Disable(), enabled after Toggle() | | `TestMode_Middleware` (disabled) | Middleware with Mode disabled | Returns 200 and passes through | | `TestMode_Middleware` (enabled) | Middleware with Mode enabled | Returns 503 in maintenance mode | | `TestScheduledMaintenance` | Scheduled maintenance in the past | Returns 200 as maintenance period has ended | ## Related Middlewares * [version](/middlewares/version) - API versioning * [ratelimit](/middlewares/ratelimit) - Rate limiting # Max Connections Source: https://docs.go-mizu.dev/middlewares/maxconns Maximum connections limit middleware for resource protection. ## Overview The `maxconns` middleware limits the maximum number of concurrent connections to protect server resources. Use it when you need: * Connection limiting * Resource protection * DoS mitigation ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/maxconns" ``` ## Quick Start ```go theme={null} app := mizu.New() // Limit to 1000 concurrent connections app.Use(maxconns.New(1000)) ``` ## Configuration ### Options | Option | Type | Default | Description | | -------------- | ----------------------- | -------- | -------------------- | | `Max` | `int` | Required | Max connections | | `ErrorHandler` | `func(*mizu.Ctx) error` | 503 | Custom error handler | ## Examples ### Basic Limit ```go theme={null} app.Use(maxconns.New(1000)) ``` ### Custom Error ```go theme={null} app.Use(maxconns.New(maxconns.Options{ Max: 1000, ErrorHandler: func(c *mizu.Ctx) error { return c.JSON(503, map[string]string{ "error": "Server at capacity", }) }, })) ``` ### With Monitoring ```go theme={null} m := maxconns.NewWithMetrics(1000) app.Use(m.Middleware()) // Monitor in handler app.Get("/stats", func(c *mizu.Ctx) error { return c.JSON(200, map[string]int{ "active": m.Active(), "max": m.Max(), }) }) ``` ## API Reference ### Functions ```go theme={null} // New creates max connections middleware func New(max int) mizu.Middleware // NewWithOptions creates with configuration func NewWithOptions(opts Options) mizu.Middleware // NewWithMetrics creates with metrics tracking func NewWithMetrics(max int) *MaxConns ``` ## Technical Details ### Implementation Architecture The maxconns middleware uses atomic operations and mutexes to safely manage concurrent connections: **Global Connection Tracking:** * Uses `atomic.LoadInt64` and `atomic.AddInt64` for lock-free global connection counting * Atomic operations ensure thread-safe increments/decrements without mutex overhead * Counter is incremented before processing and decremented via defer after completion **Per-IP Connection Limiting:** * Maintains a `map[string]int` for per-IP connection counts * Protected by `sync.RWMutex` to allow concurrent reads with exclusive writes * IP addresses extracted from `X-Forwarded-For`, `X-Real-IP`, or `RemoteAddr` headers * Map entries automatically cleaned up when IP count reaches zero **Connection Lifecycle:** 1. Check global limit using atomic load operation 2. If PerIP enabled, check IP-specific limit under read lock 3. Increment both counters (atomic for global, locked for per-IP) 4. Process request through handler chain 5. Decrement counters via defer (guarantees cleanup even on errors) **Zero/Negative Max Handling:** * When `Max <= 0`, middleware immediately rejects all requests * Optimized fast path that bypasses counter management **Error Handling:** * Default response: 503 Service Unavailable with "Retry-After: 60" header * Custom error handlers can be configured via Options * Error handler receives full context for custom responses ### Key Components **Counter Type:** * Provides observable connection metrics * Exposes `Current()` and `Max()` methods for monitoring * Can be used standalone for custom monitoring solutions **Helper Functions:** * `Global(max)`: Alias for `New(max)` for semantic clarity * `PerIP(max)`: Convenience function setting per-IP limit with high global max (1,000,000) * `getClientIP()`: Extracts client IP from standard proxy headers ## Best Practices * Set based on server capacity * Monitor connection counts * Use with rate limiting * Return appropriate error response ## Testing The maxconns middleware includes comprehensive test coverage for all major scenarios: | Test Case | Description | Expected Behavior | | ------------------------------------ | ---------------------------------------------------------- | ------------------------------------------------------- | | `TestNew` | Concurrent requests exceeding max limit | Accepts up to max connections, rejects excess with 503 | | `TestWithOptions_PerIP` | Multiple concurrent requests from same IP with PerIP limit | Only allows configured number per IP, rejects others | | `TestWithOptions_CustomErrorHandler` | Custom error handler with Max=0 | Uses custom handler returning custom status/response | | `TestPerIP` | Sequential requests with PerIP limit | Sequential requests succeed as connections are released | | `TestGlobal` | Global limit using Global() helper | Request succeeds within global limit | | `TestCounter` | Counter type for monitoring | Counter tracks current connections and exposes metrics | | `TestRetryAfterHeader` | Rejected request header validation | Sets "Retry-After: 60" header on 503 responses | ## Related Middlewares * [bulkhead](/middlewares/bulkhead) - Bulkhead pattern * [ratelimit](/middlewares/ratelimit) - Rate limiting * [keepalive](/middlewares/keepalive) - Connection persistence # Method Override Source: https://docs.go-mizu.dev/middlewares/methodoverride HTTP method override middleware for HTML forms. ## Overview The `methodoverride` middleware allows HTML forms to use HTTP methods other than GET and POST. Since HTML forms only support GET and POST, this middleware lets you override the method using a header or form field. ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/methodoverride" ``` ## Quick Start ```go theme={null} app := mizu.New() app.Use(methodoverride.New()) ``` ## Configuration | Option | Type | Default | Description | | ----------- | ---------- | ---------------------------- | ------------------------ | | `Header` | `string` | `"X-HTTP-Method-Override"` | Override header | | `FormField` | `string` | `"_method"` | Form field name | | `Methods` | `[]string` | `["PUT", "PATCH", "DELETE"]` | Allowed override methods | ## Examples ### HTML Form with PUT ```html theme={null}
    ``` ```go theme={null} app.Use(methodoverride.New()) app.Put("/users/:id", updateUser) // Handles the form above ``` ### DELETE via Form ```html theme={null}
    ``` ### Header Override (AJAX) ```javascript theme={null} fetch('/users/123', { method: 'POST', headers: { 'X-HTTP-Method-Override': 'PUT', 'Content-Type': 'application/json' }, body: JSON.stringify({name: 'John'}) }); ``` ### Custom Configuration ```go theme={null} app.Use(methodoverride.WithOptions(methodoverride.Options{ Header: "X-Method", FormField: "method", Methods: []string{"PUT", "DELETE"}, // Only allow PUT/DELETE })) ``` ## How It Works 1. Only processes POST requests 2. Checks header, then query, then form field for override value 3. If valid method found, changes request method 4. Subsequent handlers see the overridden method ## API Reference ```go theme={null} func New() mizu.Middleware func WithOptions(opts Options) mizu.Middleware ``` ## Technical Details ### Implementation Overview The method override middleware works by intercepting POST requests and checking for method override indicators in the following priority order: 1. **Header Check**: First checks the override header (default: `X-HTTP-Method-Override`) 2. **Query Parameter**: If no header is found, checks the query parameter with the form field name 3. **Form Field**: For form submissions (content types `application/x-www-form-urlencoded` or `multipart/form-data`), checks the form field value ### Method Validation * Override values are converted to uppercase for case-insensitive matching * Only methods specified in the `Methods` configuration are allowed (default: PUT, PATCH, DELETE) * Invalid or disallowed methods are silently ignored, leaving the original POST method intact * The allowed methods are stored in a map for O(1) lookup performance ### Request Processing The middleware only processes POST requests. Any other HTTP method (GET, PUT, DELETE, etc.) passes through unchanged, even if override headers or parameters are present. This prevents potential security issues from unexpected method changes. ### Content Type Handling Form field checking only occurs when the request has an appropriate content type: * `application/x-www-form-urlencoded` * `multipart/form-data` This ensures form parsing is only attempted for actual form submissions, avoiding unnecessary processing for JSON or other content types. ## Best Practices ### Use CSRF Protection Always combine method override with CSRF protection when handling form submissions: ```go theme={null} app.Use(csrf.New()) app.Use(methodoverride.New()) ``` ### Restrict Allowed Methods Only enable the HTTP methods your application actually needs: ```go theme={null} app.Use(methodoverride.WithOptions(methodoverride.Options{ Methods: []string{"PUT", "DELETE"}, // Exclude PATCH if not needed })) ``` ### Header Priority When using AJAX requests, prefer the header approach for cleaner code and better separation from form data. ## Testing The middleware includes comprehensive test coverage for all override scenarios: | Test Case | Description | Expected Behavior | | ------------------------ | ------------------------------------------------------------------------ | --------------------------------------------- | | Header Override | POST request with `X-HTTP-Method-Override: PUT` header | Method changed to PUT | | Query Override | POST request with `?_method=DELETE` query parameter | Method changed to DELETE | | Form Override | POST request with `_method=PATCH` form field | Method changed to PATCH | | Non-POST Ignored | GET request with override header | Method remains GET, override ignored | | Invalid Method | POST request with `X-HTTP-Method-Override: INVALID` | Method remains POST, invalid override ignored | | Custom Header | POST request with custom header name (`X-Method: PUT`) | Method changed to PUT using custom header | | Custom Methods - Allowed | POST request with allowed custom method (GET) | Method changed to GET | | Custom Methods - Blocked | POST request with non-allowed method (DELETE) when only GET/HEAD allowed | Method remains POST | | Case Insensitive | POST request with lowercase override value (`delete`) | Method changed to DELETE (uppercase) | ## Security Notes * Only POST requests can be overridden * Only allowed methods (PUT, PATCH, DELETE by default) can be used * Use with CSRF protection for forms ## Related Middlewares * [csrf](/middlewares/csrf) - Form protection # Metrics Source: https://docs.go-mizu.dev/middlewares/metrics Custom metrics collection middleware for application monitoring. ## Overview The `metrics` middleware collects custom application metrics including request counts, latencies, and error rates. Use it when you need: * Application monitoring * Custom metric collection * Performance tracking ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/metrics" ``` ## Quick Start ```go theme={null} app := mizu.New() m := metrics.New() app.Use(m.Middleware()) // Expose metrics endpoint app.Get("/metrics", m.Handler()) ``` ## Configuration ### Options | Option | Type | Default | Description | | ----------- | ----------- | -------- | ---------------- | | `Namespace` | `string` | `"http"` | Metric namespace | | `Buckets` | `[]float64` | Standard | Latency buckets | ## Examples ### Basic Metrics ```go theme={null} m := metrics.New() app.Use(m.Middleware()) app.Get("/metrics", m.Handler()) ``` ### Custom Namespace ```go theme={null} m := metrics.New(metrics.Options{ Namespace: "myapp", }) ``` ### Custom Buckets ```go theme={null} m := metrics.New(metrics.Options{ Buckets: []float64{0.01, 0.05, 0.1, 0.5, 1, 5}, }) ``` ### Add Custom Metrics ```go theme={null} m := metrics.New() // Counter m.Counter("jobs_processed", "Total jobs processed") m.Inc("jobs_processed") // Gauge m.Gauge("active_connections", "Active connections") m.Set("active_connections", 42) // Histogram m.Histogram("job_duration", "Job duration", buckets) m.Observe("job_duration", 1.5) ``` ## Default Metrics | Metric | Type | Description | | ------------------------------- | --------- | --------------- | | `http_requests_total` | Counter | Total requests | | `http_request_duration_seconds` | Histogram | Request latency | | `http_requests_in_flight` | Gauge | Active requests | ## API Reference ### Functions ```go theme={null} // New creates metrics collector func New(opts ...Options) *Metrics // Middleware returns metrics middleware func (m *Metrics) Middleware() mizu.Middleware // Handler returns metrics HTTP handler func (m *Metrics) Handler() mizu.Handler // Custom metrics func (m *Metrics) Counter(name, help string) func (m *Metrics) Gauge(name, help string) func (m *Metrics) Histogram(name, help string, buckets []float64) func (m *Metrics) Inc(name string) func (m *Metrics) Set(name string, value float64) func (m *Metrics) Observe(name string, value float64) ``` ## Technical Details ### Core Components The metrics middleware consists of the following key components: #### Metrics Structure * **RequestCount**: Total number of HTTP requests processed (atomic int64) * **ErrorCount**: Total number of errors (4xx and 5xx status codes, atomic int64) * **TotalDuration**: Cumulative duration of all requests in nanoseconds (atomic int64) * **ActiveRequests**: Number of currently in-flight requests (atomic int64) * **statusCodes**: Map tracking counts for each HTTP status code * **pathCounts**: Map tracking request counts per URL path #### Thread Safety All counters use atomic operations (`atomic.AddInt64`, `atomic.LoadInt64`, `atomic.StoreInt64`) to ensure thread-safe concurrent access without explicit locking for the main counters. Maps (`statusCodes` and `pathCounts`) are protected by a `sync.RWMutex` to safely handle concurrent reads and writes. #### Status Capture Mechanism The middleware uses a custom `statusCapture` wrapper that implements `http.ResponseWriter` to intercept the status code before writing the response. This allows tracking of status codes even when they're set by downstream handlers. #### Metrics Calculation * **Average Duration**: Calculated as `TotalDuration / RequestCount` and converted to milliseconds * **Error Detection**: Requests are counted as errors when `err != nil` or `statusCode >= 400` ### Output Formats The middleware supports two output formats: 1. **JSON Format** (`Handler()`): Returns statistics as JSON with fields: * `request_count`, `error_count`, `active_requests` * `average_duration_ms`, `status_codes`, `path_counts` 2. **Prometheus Format** (`Prometheus()`): Exports metrics in Prometheus text format: * `http_requests_total` (counter) * `http_errors_total` (counter) * `http_active_requests` (gauge) ### Implementation Notes * Uses custom `itoa()` function for integer to string conversion to avoid allocations * Metrics can be reset using `Reset()` method which clears all counters and maps * The middleware does not affect the response or request flow, only observes them ## Best Practices * Use meaningful metric names * Add labels for dimensions * Set appropriate histogram buckets * Monitor metric cardinality ## Testing The metrics middleware includes comprehensive test coverage for all functionality: | Test Case | Description | Expected Behavior | | ----------------------------- | ----------------------------------------- | ---------------------------------------------------------------- | | `TestNew` | Basic initialization and request counting | Creates middleware, counts single request correctly | | `TestMetrics_StatusCodes` | HTTP status code tracking | Tracks 200 and 500 status codes separately, counts 500 as error | | `TestMetrics_PathCounts` | URL path request counting | Counts requests per path (3 to /api/users, 1 to /api/posts) | | `TestMetrics_Handler` | JSON metrics endpoint | Returns metrics as JSON with correct Content-Type header | | `TestMetrics_Prometheus` | Prometheus format export | Returns Prometheus-formatted metrics with http\_requests\_total | | `TestMetrics_Reset` | Metrics reset functionality | Resets all counters to zero after processing requests | | `TestMetrics_Concurrent` | Thread-safe concurrent access | Correctly counts 100 concurrent requests without race conditions | | `TestMetrics_AverageDuration` | Duration measurement | Calculates positive average duration for requests | | `TestMetrics_JSON` | JSON serialization | Marshals stats to JSON with request\_count field | ## Related Middlewares * [prometheus](/middlewares/prometheus) - Prometheus format * [timing](/middlewares/timing) - Server-Timing header * [healthcheck](/middlewares/healthcheck) - Health checks # Mirror Source: https://docs.go-mizu.dev/middlewares/mirror Request mirroring middleware for traffic shadowing. ## Overview The `mirror` middleware duplicates requests to one or more target servers for traffic shadowing, testing, and analysis. Mirrored requests run asynchronously and don't affect the original response. ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/mirror" ``` ## Quick Start ```go theme={null} app := mizu.New() // Mirror all traffic to staging server app.Use(mirror.New("https://staging.example.com")) ``` ## Configuration | Option | Type | Default | Description | | ----------- | ------------------------------ | ------- | ---------------------------------- | | `Targets` | `[]Target` | - | Mirror target URLs | | `Timeout` | `time.Duration` | `5s` | Timeout for mirrored requests | | `Async` | `bool` | `true` | Run mirror requests asynchronously | | `CopyBody` | `bool` | `true` | Copy request body | | `OnError` | `func(string, error)` | - | Error callback | | `OnSuccess` | `func(string, *http.Response)` | - | Success callback | ## Examples ### Single Target ```go theme={null} app.Use(mirror.New("https://staging.example.com")) // All requests are mirrored to staging app.Get("/api/users", usersHandler) ``` ### Multiple Targets ```go theme={null} app.Use(mirror.New( "https://staging.example.com", "https://analytics.example.com", )) ``` ### Percentage-Based Mirroring ```go theme={null} app.Use(mirror.WithOptions(mirror.Options{ Targets: []mirror.Target{ mirror.Percentage("https://staging.example.com", 50), // 50% of traffic mirror.Percentage("https://canary.example.com", 10), // 10% of traffic }, })) ``` ### Custom Timeout ```go theme={null} app.Use(mirror.WithOptions(mirror.Options{ Targets: []mirror.Target{ {URL: "https://staging.example.com", Percentage: 100}, }, Timeout: 10 * time.Second, })) ``` ### Synchronous Mirroring ```go theme={null} // Wait for mirror requests to complete app.Use(mirror.WithOptions(mirror.Options{ Targets: []mirror.Target{ {URL: "https://backup.example.com", Percentage: 100}, }, Async: false, })) ``` ### Error Handling ```go theme={null} app.Use(mirror.WithOptions(mirror.Options{ Targets: []mirror.Target{ {URL: "https://staging.example.com", Percentage: 100}, }, OnError: func(target string, err error) { log.Printf("Mirror to %s failed: %v", target, err) metrics.IncrementCounter("mirror_errors", target) }, })) ``` ### Success Callback ```go theme={null} app.Use(mirror.WithOptions(mirror.Options{ Targets: []mirror.Target{ {URL: "https://staging.example.com", Percentage: 100}, }, OnSuccess: func(target string, resp *http.Response) { log.Printf("Mirror to %s: status %d", target, resp.StatusCode) // Compare responses if resp.StatusCode >= 500 { alertOps("Staging returned error") } }, })) ``` ### Response Comparison ```go theme={null} var mu sync.Mutex var prodResponses = make(map[string]int) var stagingResponses = make(map[string]int) app.Use(mirror.WithOptions(mirror.Options{ Targets: []mirror.Target{ {URL: "https://staging.example.com", Percentage: 100}, }, OnSuccess: func(target string, resp *http.Response) { mu.Lock() defer mu.Unlock() stagingResponses[resp.Request.URL.Path] = resp.StatusCode }, })) // Middleware to track production responses app.Use(func(next mizu.Handler) mizu.Handler { return func(c *mizu.Ctx) error { err := next(c) // Track and compare... return err } }) ``` ### Route-Specific Mirroring ```go theme={null} // Only mirror specific routes api := app.Group("/api") api.Use(mirror.New("https://staging.example.com")) // Other routes are not mirrored app.Get("/health", healthHandler) ``` ### Canary Testing ```go theme={null} // Mirror small percentage to canary for testing app.Use(mirror.WithOptions(mirror.Options{ Targets: []mirror.Target{ mirror.Percentage("https://canary.example.com", 5), }, OnSuccess: func(target string, resp *http.Response) { if resp.StatusCode >= 500 { // Alert on canary errors alertCanaryError(target, resp.StatusCode) } }, OnError: func(target string, err error) { alertCanaryError(target, -1) }, })) ``` ### A/B Testing Analysis ```go theme={null} app.Use(mirror.WithOptions(mirror.Options{ Targets: []mirror.Target{ {URL: "https://experiment-a.example.com", Percentage: 50}, {URL: "https://experiment-b.example.com", Percentage: 50}, }, OnSuccess: func(target string, resp *http.Response) { // Log response times for analysis latency := resp.Header.Get("X-Response-Time") analytics.TrackLatency(target, latency) }, })) ``` ### Skip Body for GET Requests ```go theme={null} app.Use(mirror.WithOptions(mirror.Options{ Targets: []mirror.Target{ {URL: "https://staging.example.com", Percentage: 100}, }, CopyBody: false, // Don't copy body (faster for GET) })) ``` ## Target Structure ```go theme={null} type Target struct { URL string // Target base URL Percentage int // Percentage of requests to mirror (0-100) } ``` ## Helper Functions ```go theme={null} // Create target with percentage mirror.Percentage("https://example.com", 50) // 50% of traffic ``` ## Mirrored Request Headers Each mirrored request includes: * All original headers * `X-Mirrored-From: ` ## API Reference ```go theme={null} func New(targets ...string) mizu.Middleware func WithOptions(opts Options) mizu.Middleware func Percentage(url string, pct int) Target ``` ## Use Cases 1. **Shadow Testing**: Test new versions with production traffic 2. **Load Testing**: Send copies to test infrastructure 3. **Analytics**: Duplicate to analytics services 4. **Backup**: Sync to backup systems 5. **Canary Deployment**: Route small percentage to canary ## Technical Details ### Implementation Architecture The mirror middleware is built with the following core components: 1. **Request Cloning**: The middleware reads the original request body (if `CopyBody` is enabled) and stores it in memory to allow multiple sends without consuming the original request stream. 2. **HTTP Client**: A dedicated `http.Client` is created with the configured timeout and redirect policy (`http.ErrUseLastResponse` to prevent following redirects). 3. **Percentage-Based Sampling**: Uses a simple counter-based modulo operation to determine whether to mirror a request based on the configured percentage (e.g., 50% mirrors every other request). 4. **Asynchronous Execution**: When `Async` is true (default), mirrored requests are executed in separate goroutines, ensuring zero impact on the main request latency. ### Request Flow 1. Original request arrives at the middleware 2. If `CopyBody` is enabled and body exists, read body into memory and restore it to the original request 3. For each target: * Check percentage threshold using counter-based sampling * If threshold met, execute mirror request (async or sync based on configuration) 4. Continue to next middleware/handler with original request unchanged ### Mirror Request Details Each mirrored request includes: * All original HTTP headers copied verbatim * `X-Mirrored-From` header set to original request host * Same HTTP method as original request * Same request URI (path + query string) appended to target base URL * Request body (if `CopyBody` is enabled) ### Performance Characteristics * **Async mode**: Zero latency impact on original request (default) * **Sync mode**: Adds mirror request latency to original request processing * **Memory overhead**: Proportional to request body size when `CopyBody` is enabled * **Goroutine cost**: One goroutine per target per mirrored request in async mode ### Error Handling * Mirror failures never affect the original request/response * Errors are silently ignored unless `OnError` callback is provided * HTTP client respects the configured timeout to prevent hanging requests * Response bodies are properly closed to prevent resource leaks ## Best Practices * Use async mode (default) to avoid latency * Set appropriate timeouts * Monitor error rates on mirror targets * Use percentage-based mirroring for gradual rollouts * Log and compare responses for validation * Don't mirror to targets with side effects (unless intended) ## Testing The mirror middleware includes comprehensive test coverage for all features: | Test Case | Description | Expected Behavior | | -------------------------------- | --------------------------------------------- | --------------------------------------------------------------------------- | | TestNew | Basic mirror functionality with single target | Request is successfully mirrored to target server | | TestWithOptions\_Async | Asynchronous mirroring behavior | Original response returns immediately without waiting for mirror completion | | TestWithOptions\_Percentage | Percentage-based traffic sampling | Approximately 50% of requests are mirrored when percentage is set to 50 | | TestWithOptions\_MultipleTargets | Mirroring to multiple targets simultaneously | All configured targets receive mirrored requests | | TestWithOptions\_OnError | Error callback invocation | OnError callback is called when mirror request fails | | TestWithOptions\_OnSuccess | Success callback invocation | OnSuccess callback is called with response when mirror succeeds | | TestWithOptions\_MirroredHeader | X-Mirrored-From header injection | Mirror request includes X-Mirrored-From header with original host | | TestPercentage | Percentage helper function | Returns Target with correct URL and percentage values | | TestWithOptions\_CopyBody | Request body copying enabled | Mirror request receives the same body as original request | | TestWithOptions\_NoCopyBody | Request body copying disabled | Mirror request is sent without body content | | TestNew\_MultipleTargets | Multiple targets via New() helper | All targets receive mirrored requests with async execution | ## Related Middlewares * [proxy](/middlewares/proxy) - Reverse proxy * [chaos](/middlewares/chaos) - Chaos engineering # Mock Source: https://docs.go-mizu.dev/middlewares/mock Request mocking middleware for testing and development. ## Overview The `mock` middleware provides request mocking capabilities for testing and development, returning predefined responses for specified routes. Use it when you need: * API mocking for frontend development * Test fixtures * Stubbing external services ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/mock" ``` ## Quick Start ```go theme={null} app := mizu.New() // Mock a response app.Use(mock.New(mock.Routes{ "GET /api/users": mock.JSON(200, users), })) ``` ## Configuration ### Routes Map ```go theme={null} type Routes map[string]Response // Pattern: "METHOD /path" ``` ## Examples ### JSON Response ```go theme={null} app.Use(mock.New(mock.Routes{ "GET /api/users": mock.JSON(200, []User{ {ID: 1, Name: "John"}, {ID: 2, Name: "Jane"}, }), })) ``` ### Multiple Routes ```go theme={null} app.Use(mock.New(mock.Routes{ "GET /api/users": mock.JSON(200, users), "GET /api/users/1": mock.JSON(200, user), "POST /api/users": mock.JSON(201, newUser), "DELETE /api/users/1": mock.Status(204), })) ``` ### With Headers ```go theme={null} app.Use(mock.New(mock.Routes{ "GET /api/data": mock.Response{ Status: 200, Body: `{"data": "test"}`, Headers: map[string]string{ "Content-Type": "application/json", "X-Custom": "value", }, }, })) ``` ### From File ```go theme={null} app.Use(mock.New(mock.Routes{ "GET /api/users": mock.File("testdata/users.json"), })) ``` ### Conditional Mocking ```go theme={null} app.Use(mock.WithOptions(mock.Options{ Routes: mockRoutes, Enabled: func() bool { return os.Getenv("MOCK_API") == "true" }, })) ``` ### Delay Response ```go theme={null} app.Use(mock.New(mock.Routes{ "GET /api/slow": mock.Response{ Status: 200, Body: `{"data": "slow"}`, Delay: 2 * time.Second, }, })) ``` ## API Reference ### Functions ```go theme={null} // New creates mock middleware func New(routes Routes) mizu.Middleware // WithOptions creates with configuration func WithOptions(opts Options) mizu.Middleware // Response helpers func JSON(status int, data any) Response func Text(status int, body string) Response func Status(status int) Response func File(path string) Response ``` ## Technical Details ### Implementation Overview The mock middleware is built on a thread-safe architecture using `sync.RWMutex` for concurrent access to mock responses. It supports both path-only and method-specific mocking patterns. **Core Components:** * **Mock struct**: Central registry holding mock responses with thread-safe access * **Response matching**: Two-tier matching system (method+path, then path-only) * **Passthrough mode**: Configurable behavior for unmatched requests **Request Flow:** 1. Extract request path and method from incoming request 2. Acquire read lock for concurrent-safe access 3. Check method-specific mocks first (`methods[method][path]`) 4. Fall back to path-only mocks (`mocks[path]`) 5. Apply default response if configured 6. Either pass through to next handler or return 404 **Response Types:** The middleware provides helper functions for common response types: * `JSON()`: Marshal Go data structures to JSON with appropriate headers * `Text()`: Plain text responses * `HTML()`: HTML responses with proper content type * `File()`: Generic file responses with custom content types * `Redirect()`: HTTP redirects with location headers * `Error()`: Standardized error responses in JSON format **Thread Safety:** All mock registration and retrieval operations are protected by read/write locks, making the middleware safe for concurrent use in multi-threaded environments. ## Best Practices * Use environment variable to enable/disable * Keep mock data in separate files * Match production response structure * Add realistic delays for latency testing ## Testing The mock middleware includes comprehensive test coverage for all major functionality: | Test Case | Description | Expected Behavior | | -------------------------------- | ----------------------------------------- | -------------------------------------------------------- | | TestNew | Creates middleware with predefined mocks | Returns mocked JSON response instead of real handler | | TestMock\_Register | Registers a mock response for a path | Mock response is served for registered path | | TestMock\_RegisterMethod | Registers method-specific mock | POST request returns mocked response with status 201 | | TestMock\_Clear | Clears all registered mocks | After clear, real handler is called instead of mock | | TestWithOptions\_DefaultResponse | Uses default response for unmatched paths | Returns default response (503) for any unmatched path | | TestJSON | Creates JSON response helper | Response has correct status and JSON content type | | TestText | Creates text response helper | Response contains correct text body and content type | | TestHTML | Creates HTML response helper | Response has text/html content type | | TestRedirect | Creates redirect response | Response has correct redirect status and Location header | | TestError | Creates error response helper | Returns JSON error response with correct status | | TestPrefix | Mocks all paths matching prefix | Prefixed paths return mock, non-prefixed pass through | | TestPassthrough | Passes unmatched requests to next handler | Real handler is called when no mock matches | ## Related Middlewares * [static](/middlewares/static) - Static file serving * [proxy](/middlewares/proxy) - Reverse proxy # MessagePack Source: https://docs.go-mizu.dev/middlewares/msgpack MessagePack content handling middleware for binary serialization. ## Overview The `msgpack` middleware provides MessagePack content negotiation and serialization for efficient binary data transfer. Use it when you need: * Binary serialization * Reduced payload size * Faster parsing ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/msgpack" ``` ## Quick Start ```go theme={null} app := mizu.New() app.Use(msgpack.New()) app.Get("/data", func(c *mizu.Ctx) error { // Auto-negotiates based on Accept header return c.Negotiate(200, data) }) ``` ## Examples ### Enable MessagePack ```go theme={null} app.Use(msgpack.New()) ``` ### Explicit MessagePack Response ```go theme={null} app.Get("/data", func(c *mizu.Ctx) error { return msgpack.Send(c, 200, data) }) ``` ### Parse MessagePack Request ```go theme={null} app.Post("/data", func(c *mizu.Ctx) error { var data MyStruct if err := msgpack.Bind(c, &data); err != nil { return err } return c.JSON(200, data) }) ``` ### Content Negotiation ```go theme={null} app.Use(msgpack.New()) app.Get("/data", func(c *mizu.Ctx) error { // Returns MessagePack if Accept: application/msgpack // Returns JSON if Accept: application/json return c.Negotiate(200, data) }) ``` ## API Reference ### Functions ```go theme={null} // New creates MessagePack middleware func New() mizu.Middleware // Send sends MessagePack response func Send(c *mizu.Ctx, status int, data any) error // Bind parses MessagePack request body func Bind(c *mizu.Ctx, v any) error ``` ## Content Types | Type | Description | | ----------------------- | -------------------- | | `application/msgpack` | Standard MessagePack | | `application/x-msgpack` | Alternative | ## Technical Details ### Architecture The msgpack middleware implements a lightweight MessagePack encoder/decoder without external dependencies. It consists of three main components: #### Middleware Handler * Intercepts requests with MessagePack content types * Stores raw request body in context for later parsing * Supports configurable content types (default: `application/msgpack`, `application/x-msgpack`) #### Encoder * Implements MessagePack binary format specification * Uses fixed-size optimizations for common value ranges * Supports fixint, int8/16/32/64, uint8/16/32/64 encoding * Handles fixstr, str8/16/32 for strings with automatic size selection * Encodes binary data with bin8/16/32 formats * Supports fixarray, array16/32 and fixmap, map16/32 structures #### Decoder * Parses MessagePack binary format to Go values * Returns generic `any` types (string, int64, uint64, float32/64, \[]any, map\[string]any) * Validates buffer boundaries to prevent out-of-bounds access * Handles all MessagePack type codes including fixint, negative fixint, and type-specific codes ### Type Mappings | Go Type | MessagePack Format | Notes | | --------------- | -------------------- | ------------------------ | | nil | 0xc0 | Null value | | bool | 0xc2, 0xc3 | false, true | | int, int8-64 | fixint, int8-64 | Optimized by value range | | uint, uint8-64 | fixint, uint8-64 | Optimized by value range | | float32 | float 32 (0xca) | 4-byte IEEE 754 | | float64 | float 64 (0xcb) | 8-byte IEEE 754 | | string | fixstr, str8-32 | Length-prefixed | | \[]byte | bin8-32 | Binary data | | \[]any | fixarray, array16-32 | Recursive encoding | | map\[string]any | fixmap, map16-32 | Key-value pairs | ### Error Handling The middleware defines three error types: * `ErrUnsupportedType`: Returned when attempting to encode unsupported Go types * `ErrInvalidFormat`: Returned when decoding encounters invalid MessagePack data * `ErrBufferTooSmall`: Returned when the input buffer is insufficient for the declared data ### Context Storage The middleware stores the raw MessagePack request body in the request context using a private context key. This allows: * Multiple reads of the request body * Access via the `Body()` helper function * Preservation of the original request body for middleware chain ## Best Practices * Use for internal APIs * Fall back to JSON for browsers * Consider client library support * Benchmark for your use case ## Testing ### Test Coverage The msgpack middleware includes comprehensive test coverage for all functionality: | Test Case | Description | Expected Behavior | | ------------------------------------- | --------------------------- | --------------------------------------------------------------- | | TestNew | Middleware initialization | Middleware successfully initializes and passes requests through | | TestMarshalUnmarshal - nil | Nil value encoding/decoding | Correctly encodes and decodes nil values | | TestMarshalUnmarshal - true/false | Boolean encoding/decoding | Correctly encodes and decodes true/false values | | TestMarshalUnmarshal - positive int | Positive integer encoding | Correctly encodes and decodes positive integers | | TestMarshalUnmarshal - negative int | Negative integer encoding | Correctly encodes and decodes negative integers | | TestMarshalUnmarshal - zero | Zero value encoding | Correctly encodes and decodes zero | | TestMarshalUnmarshal - large int | Large integer encoding | Correctly encodes and decodes large integers (1000000) | | TestMarshalUnmarshal - uint | Unsigned integer encoding | Correctly encodes and decodes unsigned integers | | TestMarshalUnmarshal - float64 | Float64 encoding | Correctly encodes and decodes float64 values | | TestMarshalUnmarshal - string | String encoding | Correctly encodes and decodes strings | | TestMarshalUnmarshal - empty string | Empty string encoding | Correctly encodes and decodes empty strings | | TestMarshalUnmarshal - long string | Long string encoding | Correctly encodes and decodes strings exceeding 31 bytes | | TestMarshalArray | Array encoding/decoding | Correctly encodes and decodes arrays with multiple elements | | TestMarshalMap | Map encoding/decoding | Correctly encodes and decodes maps with string keys | | TestMarshalBinary | Binary data encoding | Correctly encodes and decodes raw byte slices | | TestResponse | HTTP response generation | Sets correct Content-Type header and encodes response data | | TestBind | Request body parsing | Correctly parses MessagePack request bodies | | TestBody | Raw body retrieval | Retrieves raw MessagePack body from context | | TestEncoderIntegers - zero | Zero integer | Encodes/decodes zero correctly | | TestEncoderIntegers - positive fixint | Positive fixint (100) | Uses optimized fixint encoding | | TestEncoderIntegers - negative fixint | Negative fixint (-20) | Uses optimized negative fixint encoding | | TestEncoderIntegers - int8 positive | int8 range (127) | Uses int8 encoding | | TestEncoderIntegers - int8 negative | int8 range (-128) | Uses int8 encoding | | TestEncoderIntegers - int16 positive | int16 range (1000) | Uses int16 encoding | | TestEncoderIntegers - int16 negative | int16 range (-1000) | Uses int16 encoding | | TestEncoderIntegers - int32 positive | int32 range (100000) | Uses int32 encoding | | TestEncoderIntegers - int32 negative | int32 range (-100000) | Uses int32 encoding | | TestEncoderIntegers - int64 positive | int64 range (5000000000) | Uses int64 encoding | | TestEncoderIntegers - int64 negative | int64 range (-5000000000) | Uses int64 encoding | | TestEncoderFloats - float32 | float32 encoding | Encodes/decodes float32 with correct precision | | TestEncoderFloats - float64 | float64 encoding | Encodes/decodes float64 with correct precision | | TestUnsupportedType | Unsupported type error | Returns ErrUnsupportedType for custom structs | | TestEmptyBuffer | Empty buffer error | Returns error when unmarshaling empty buffer | ## Related Middlewares * [contenttype](/middlewares/contenttype) - Content-Type validation * [compress](/middlewares/compress) - Response compression * [xml](/middlewares/xml) - XML handling # Multi-tenancy Source: https://docs.go-mizu.dev/middlewares/multitenancy Multi-tenant middleware for SaaS applications. ## Overview The `multitenancy` middleware extracts and provides tenant information for multi-tenant SaaS applications. It supports various resolution strategies including subdomain, header, path, and query parameters. ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/multitenancy" ``` ## Quick Start ```go theme={null} app := mizu.New() // Resolve tenant from subdomain app.Use(multitenancy.New(multitenancy.SubdomainResolver())) app.Get("/", func(c *mizu.Ctx) error { tenant := multitenancy.Get(c) return c.JSON(200, map[string]string{ "tenant_id": tenant.ID, }) }) ``` ## Configuration | Option | Type | Default | Description | | -------------- | ------------------------------ | --------- | -------------------------- | | `Resolver` | `Resolver` | Subdomain | Tenant resolution function | | `ErrorHandler` | `func(*mizu.Ctx, error) error` | - | Error handler | | `Required` | `bool` | `true` | Require tenant resolution | ## Tenant Structure ```go theme={null} type Tenant struct { ID string Name string Domain string Metadata map[string]any } ``` ## Resolution Strategies ### Subdomain Resolution ```go theme={null} // tenant1.example.com → tenant_id: "tenant1" app.Use(multitenancy.New(multitenancy.SubdomainResolver())) ``` ### Header Resolution ```go theme={null} // X-Tenant-ID: tenant1 → tenant_id: "tenant1" app.Use(multitenancy.New(multitenancy.HeaderResolver("X-Tenant-ID"))) ``` ### Path Resolution ```go theme={null} // /tenant1/users → tenant_id: "tenant1", path: /users app.Use(multitenancy.New(multitenancy.PathResolver())) ``` ### Query Parameter Resolution ```go theme={null} // /users?tenant=tenant1 → tenant_id: "tenant1" app.Use(multitenancy.New(multitenancy.QueryResolver("tenant"))) ``` ## Examples ### Basic Subdomain Tenant ```go theme={null} app.Use(multitenancy.New(multitenancy.SubdomainResolver())) app.Get("/dashboard", func(c *mizu.Ctx) error { tenant := multitenancy.Get(c) data := loadDashboard(tenant.ID) return c.JSON(200, data) }) ``` ### Database Lookup ```go theme={null} resolver := multitenancy.LookupResolver( multitenancy.SubdomainResolver(), func(id string) (*multitenancy.Tenant, error) { // Look up full tenant info from database var tenant Tenant err := db.QueryRow( "SELECT id, name, plan FROM tenants WHERE slug = ?", id, ).Scan(&tenant.ID, &tenant.Name, &tenant.Plan) if err != nil { return nil, multitenancy.ErrTenantNotFound } return &multitenancy.Tenant{ ID: tenant.ID, Name: tenant.Name, Metadata: map[string]any{ "plan": tenant.Plan, }, }, nil }, ) app.Use(multitenancy.New(resolver)) ``` ### Chain Multiple Resolvers ```go theme={null} // Try subdomain first, then header, then query param resolver := multitenancy.ChainResolver( multitenancy.SubdomainResolver(), multitenancy.HeaderResolver("X-Tenant-ID"), multitenancy.QueryResolver("tenant"), ) app.Use(multitenancy.New(resolver)) ``` ### Custom Error Handler ```go theme={null} app.Use(multitenancy.WithOptions(multitenancy.Options{ Resolver: multitenancy.SubdomainResolver(), ErrorHandler: func(c *mizu.Ctx, err error) error { return c.JSON(400, map[string]string{ "error": "Invalid tenant", "message": err.Error(), }) }, })) ``` ### Optional Tenant ```go theme={null} app.Use(multitenancy.WithOptions(multitenancy.Options{ Resolver: multitenancy.SubdomainResolver(), Required: false, // Allow requests without tenant })) app.Get("/", func(c *mizu.Ctx) error { tenant := multitenancy.Get(c) if tenant == nil { return c.JSON(200, "Welcome to the platform") } return c.JSON(200, "Welcome, "+tenant.Name) }) ``` ### MustGet (Panic on Missing) ```go theme={null} app.Get("/settings", func(c *mizu.Ctx) error { // Panics if tenant not found - use in required tenant routes tenant := multitenancy.MustGet(c) settings := loadSettings(tenant.ID) return c.JSON(200, settings) }) ``` ### Custom Resolver ```go theme={null} func JWTTenantResolver() multitenancy.Resolver { return func(c *mizu.Ctx) (*multitenancy.Tenant, error) { claims := c.Get("jwt_claims").(jwt.MapClaims) tenantID, ok := claims["tenant_id"].(string) if !ok || tenantID == "" { return nil, multitenancy.ErrTenantNotFound } return &multitenancy.Tenant{ ID: tenantID, Name: tenantID, }, nil } } app.Use(jwtauth.New(jwtSecret)) app.Use(multitenancy.New(JWTTenantResolver())) ``` ### Tenant-Scoped Database ```go theme={null} app.Get("/users", func(c *mizu.Ctx) error { tenant := multitenancy.Get(c) // Use tenant ID for database scoping users, err := db.Query( "SELECT * FROM users WHERE tenant_id = ?", tenant.ID, ) if err != nil { return err } return c.JSON(200, users) }) ``` ### Tenant Middleware Chain ```go theme={null} // Create tenant-aware middleware func TenantRateLimit() mizu.Middleware { return func(next mizu.Handler) mizu.Handler { return func(c *mizu.Ctx) error { tenant := multitenancy.Get(c) limiter := rateLimiters[tenant.ID] if !limiter.Allow() { return c.Text(429, "Rate limit exceeded") } return next(c) } } } app.Use(multitenancy.New(multitenancy.SubdomainResolver())) app.Use(TenantRateLimit()) ``` ### Tenant Metadata ```go theme={null} resolver := multitenancy.LookupResolver( multitenancy.SubdomainResolver(), func(id string) (*multitenancy.Tenant, error) { tenant := loadTenantFromDB(id) return &multitenancy.Tenant{ ID: tenant.ID, Name: tenant.Name, Domain: tenant.Domain, Metadata: map[string]any{ "plan": tenant.Plan, "max_users": tenant.MaxUsers, "features": tenant.Features, "custom_domain": tenant.CustomDomain, }, }, nil }, ) // Access metadata in handlers app.Get("/plan", func(c *mizu.Ctx) error { tenant := multitenancy.Get(c) plan := tenant.Metadata["plan"].(string) return c.JSON(200, map[string]string{"plan": plan}) }) ``` ## Built-in Resolvers | Resolver | Source | Example | | ----------------------- | ----------- | ---------------------- | | `SubdomainResolver()` | Subdomain | `tenant1.example.com` | | `HeaderResolver(h)` | HTTP Header | `X-Tenant-ID: tenant1` | | `PathResolver()` | URL Path | `/tenant1/api/users` | | `QueryResolver(p)` | Query Param | `?tenant=tenant1` | | `ChainResolver(...)` | Multiple | Tries each in order | | `LookupResolver(r, fn)` | Database | Enriches with lookup | ## API Reference ```go theme={null} func New(resolver Resolver) mizu.Middleware func WithOptions(opts Options) mizu.Middleware func Get(c *mizu.Ctx) *Tenant func FromContext(c *mizu.Ctx) *Tenant // Alias func MustGet(c *mizu.Ctx) *Tenant // Resolvers func SubdomainResolver() Resolver func HeaderResolver(header string) Resolver func PathResolver() Resolver func QueryResolver(param string) Resolver func LookupResolver(resolver Resolver, lookup func(id string) (*Tenant, error)) Resolver func ChainResolver(resolvers ...Resolver) Resolver // Errors var ErrTenantNotFound = errors.New("tenant not found") var ErrTenantInvalid = errors.New("tenant invalid") ``` ## Technical Details ### Architecture The multitenancy middleware uses Go's context package to store and retrieve tenant information throughout the request lifecycle. It implements a resolver pattern that allows flexible tenant identification strategies. ### Context Storage The middleware stores tenant information in the request context using a private `contextKey{}` type. This ensures type safety and prevents key collisions with other middleware or application code. ```go theme={null} type contextKey struct{} // Stored in context ctx := context.WithValue(c.Context(), contextKey{}, tenant) ``` ### Resolver Chain The resolver pattern allows composing multiple tenant identification strategies: 1. Each resolver implements the `Resolver` function type 2. Resolvers return `(*Tenant, error)` allowing error propagation 3. `ChainResolver` tries resolvers sequentially until one succeeds 4. `LookupResolver` wraps a resolver with a database lookup function ### Path Rewriting The `PathResolver` automatically rewrites the request path to remove the tenant prefix: ```go theme={null} // Original: /tenant1/api/users // Rewritten: /api/users // Tenant ID: "tenant1" ``` This allows routes to be defined without tenant prefixes while maintaining tenant isolation. ### Error Handling The middleware supports both required and optional tenant resolution: * **Required mode (default)**: Returns error via `ErrorHandler` when tenant not found * **Optional mode**: Continues request processing with `nil` tenant * **Custom error handler**: Allows application-specific error responses ### Performance Considerations * Context lookups are O(1) operations * Subdomain parsing uses efficient string operations * Header lookups use Go's optimized HTTP header map * Consider caching with `LookupResolver` for database-backed tenants ## Best Practices * Use subdomain resolution for user-friendly URLs * Implement database lookup for rich tenant data * Use chain resolver for flexibility * Always validate tenant access to resources * Include tenant ID in all database queries * Cache tenant lookups for performance ## Testing The middleware includes comprehensive test coverage for all resolvers and scenarios: | Test Case | Description | Expected Behavior | | ------------------------------ | ---------------------------------------------- | ------------------------------------------------------------------------ | | `TestNew` | Basic middleware creation with custom resolver | Tenant stored in context and accessible via `Get()` | | `TestWithOptions_Required` | Required tenant resolution fails | Returns 400 Bad Request when tenant not found | | `TestWithOptions_NotRequired` | Optional tenant resolution | Continues without tenant, `Get()` returns nil | | `TestSubdomainResolver` | Extract tenant from subdomain | Resolves "acme" from "acme.example.com" | | `TestHeaderResolver` | Extract tenant from HTTP header | Resolves tenant from "X-Tenant-Id" header | | `TestPathResolver` | Extract tenant from URL path prefix | Resolves "acme" from "/acme/api/users" and rewrites path to "/api/users" | | `TestQueryResolver` | Extract tenant from query parameter | Resolves tenant from "?tenant=my-tenant" | | `TestLookupResolver` | Database lookup enrichment | Enhances basic tenant with full data from lookup function | | `TestChainResolver` | Multiple resolver fallback | Tries header, then query, then subdomain in order | | `TestFromContext` | Alias function for Get | `FromContext()` returns same tenant as `Get()` | | `TestMustGet` | Get tenant or panic | Returns tenant when present | | `TestMustGet_Panic` | Panic on missing tenant | Panics with "tenant not found" message when tenant absent | | `TestErrors` | Error constants | Validates error messages for `ErrTenantNotFound` and `ErrTenantInvalid` | | `TestWithOptions_ErrorHandler` | Custom error handling | Uses custom error handler to return 401 Unauthorized | ## Related Middlewares * [feature](/middlewares/feature) - Feature flags * [ratelimit](/middlewares/ratelimit) - Rate limiting # No Cache Source: https://docs.go-mizu.dev/middlewares/nocache Prevent caching middleware for sensitive or dynamic content. ## Overview The `nocache` middleware sets headers to prevent browsers and proxies from caching responses. Use it for sensitive data or frequently changing content. ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/nocache" ``` ## Quick Start ```go theme={null} app := mizu.New() app.Use(nocache.New()) ``` ## Headers Set ``` Cache-Control: no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0 Pragma: no-cache Expires: 0 Surrogate-Control: no-store ``` ## Examples ### Global No-Cache ```go theme={null} app.Use(nocache.New()) ``` ### Specific Routes ```go theme={null} // Don't cache sensitive endpoints app.Get("/api/user", userHandler, nocache.New()) app.Get("/api/account", accountHandler, nocache.New()) ``` ### API Group ```go theme={null} api := app.Group("/api") api.Use(nocache.New()) api.Get("/users", listUsers) api.Get("/data", getData) ``` ## API Reference ```go theme={null} func New() mizu.Middleware ``` ## When to Use * User-specific data * Authentication responses * Frequently changing data * Sensitive information * Real-time data ## Technical Details The nocache middleware is implemented as a simple wrapper that sets anti-caching headers before passing control to the next handler in the chain. ### Implementation The middleware: 1. Retrieves the response header object from the context 2. Sets four HTTP headers to prevent caching at multiple levels: * `Cache-Control`: Provides comprehensive cache directives for HTTP/1.1 * `Pragma`: Backward compatibility with HTTP/1.0 caches * `Expires`: Sets expiration to prevent caching * `Surrogate-Control`: Controls CDN and proxy caching behavior 3. Calls the next handler in the middleware chain ### Performance * Zero-allocation implementation * Minimal overhead (4 header writes per request) * No configuration or state management required * Thread-safe for concurrent requests ## Best Practices * Apply to routes serving sensitive or user-specific data * Use sparingly on static content to avoid performance degradation * Consider combining with HTTPS for maximum security * Test cache behavior with browser developer tools * Apply at the route level rather than globally when possible ## Testing The middleware includes comprehensive tests to verify correct header configuration. | Test Case | Description | Expected Behavior | | ------------------------ | --------------------------------------------- | ----------------------------------------------------------------------- | | Cache-Control Header | Validates the Cache-Control header is set | Sets "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" | | Pragma Header | Validates the Pragma header is set | Sets "no-cache" for HTTP/1.0 compatibility | | Expires Header | Validates the Expires header is set | Sets "0" to indicate immediate expiration | | Surrogate-Control Header | Validates the Surrogate-Control header is set | Sets "no-store" to prevent CDN/proxy caching | ## Related Middlewares * [cache](/middlewares/cache) - Enable caching # Nonce Source: https://docs.go-mizu.dev/middlewares/nonce Cryptographic nonce generation middleware for CSP and security. ## Overview The `nonce` middleware generates cryptographic nonces for Content Security Policy (CSP) inline scripts and styles. Use it when you need: * CSP nonce-based security * Inline script authorization * Dynamic nonce generation ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/nonce" ``` ## Quick Start ```go theme={null} app := mizu.New() app.Use(nonce.New()) app.Get("/", func(c *mizu.Ctx) error { n := nonce.Get(c) return c.HTML(200, ``) }) ``` ## Configuration ### Options | Option | Type | Default | Description | | -------- | -------- | ------- | ----------------- | | `Length` | `int` | `16` | Nonce byte length | | `Header` | `string` | `""` | Expose in header | ## Examples ### Basic Usage ```go theme={null} app.Use(nonce.New()) app.Get("/", func(c *mizu.Ctx) error { n := nonce.Get(c) html := fmt.Sprintf(` `, n, n) return c.HTML(200, html) }) ``` ### With CSP Header ```go theme={null} app.Use(nonce.New()) app.Use(helmet.WithOptions(helmet.Options{ ContentSecurityPolicy: func(c *mizu.Ctx) string { n := nonce.Get(c) return fmt.Sprintf("script-src 'nonce-%s'", n) }, })) ``` ### Expose in Header ```go theme={null} app.Use(nonce.New(nonce.Options{ Header: "X-Nonce", })) // Response header: X-Nonce: abc123... ``` ## API Reference ### Functions ```go theme={null} // New creates nonce middleware func New(opts ...Options) mizu.Middleware // Get returns nonce from context func Get(c *mizu.Ctx) string ``` ## Technical Details ### Implementation The nonce middleware generates cryptographically secure random nonces for each HTTP request and integrates them into Content Security Policy headers. The implementation follows these key patterns: #### Nonce Generation * Uses `crypto/rand` for cryptographically secure random byte generation * Default nonce length: 16 bytes (produces 22 character base64 strings) * Base64 encoding with raw standard encoding (no padding) * Custom generators can be provided via the `Generator` option #### Context Storage * Nonces are stored in the request context using a private `contextKey` type * Retrieved via the `Get(c *mizu.Ctx)` function * Context key is type-safe and collision-resistant #### CSP Header Construction The middleware builds Content Security Policy headers by: 1. Parsing existing base policies (if provided via `BasePolicy` option) 2. Adding nonce values to specified directives (`script-src`, `style-src` by default) 3. Using the format `'nonce-{base64-value}'` as per CSP specification 4. Merging with existing directive values when a base policy is present 5. Setting the header (default: `Content-Security-Policy`) #### Helper Functions * `ScriptTag(c *mizu.Ctx)`: Returns `nonce="..."` attribute for script tags * `StyleTag(c *mizu.Ctx)`: Returns `nonce="..."` attribute for style tags * Both return empty strings if no nonce is available in context ### Preset Middleware Functions The package provides several convenience constructors: * `New()`: Default configuration with script-src and style-src * `ForScripts()`: Nonce only for script-src directive * `ForStyles()`: Nonce only for style-src directive * `WithBasePolicy(policy string)`: Extends an existing CSP policy * `ReportOnly()`: Uses `Content-Security-Policy-Report-Only` header ## Security * Nonces are cryptographically random * New nonce per request * Base64-encoded for HTML safety * Use with strict CSP ## Best Practices * Use with Content-Security-Policy * Generate new nonce per request * Include in all inline scripts/styles * Don't reuse nonces ## Testing The nonce middleware includes comprehensive test coverage for all functionality: | Test Case | Description | Expected Behavior | | ---------------------------------- | --------------------------------------- | ----------------------------------------------------------------------- | | `TestNew` | Basic middleware initialization | Generates nonce, stores in context, sets CSP header with nonce | | `TestWithOptions_CustomLength` | Custom nonce byte length (32 bytes) | Generates longer nonce (\~43 base64 characters) | | `TestWithOptions_CustomDirectives` | Custom CSP directives (script-src only) | CSP includes only script-src, excludes style-src | | `TestWithOptions_BasePolicy` | Extending existing CSP policy | Preserves base policy directives and adds nonce to specified directives | | `TestWithOptions_CustomGenerator` | Custom nonce generator function | Uses provided generator instead of default random generation | | `TestScriptTag` | Script tag nonce attribute helper | Returns properly formatted `nonce="..."` attribute | | `TestStyleTag` | Style tag nonce attribute helper | Returns properly formatted `nonce="..."` attribute | | `TestForScripts` | Scripts-only preset middleware | CSP contains script-src directive with nonce | | `TestForStyles` | Styles-only preset middleware | CSP contains style-src directive with nonce | | `TestReportOnly` | Report-only mode | Sets `Content-Security-Policy-Report-Only` header instead | | `TestUniqueNonce` | Nonce uniqueness across requests | Each request generates a unique nonce (no duplicates in 10 requests) | | `TestGetWithoutMiddleware` | Get() without middleware | Returns empty string when middleware not applied | ## Related Middlewares * [helmet](/middlewares/helmet) - Security headers * [csrf](/middlewares/csrf) - CSRF protection * [secure](/middlewares/secure) - Security settings # OAuth2 Source: https://docs.go-mizu.dev/middlewares/oauth2 OAuth 2.0 authentication middleware for third-party login integration. ## Overview The `oauth2` middleware provides OAuth 2.0 authentication flows for integrating with providers like Google, GitHub, Facebook, and custom OAuth servers. Use it when you need: * Social login (Google, GitHub, etc.) * Third-party authentication * OAuth 2.0 authorization code flow ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/oauth2" ``` ## Quick Start ```go theme={null} app := mizu.New() // Configure OAuth provider config := oauth2.Config{ ClientID: "your-client-id", ClientSecret: "your-client-secret", RedirectURL: "http://localhost:3000/callback", AuthURL: "https://provider.com/oauth/authorize", TokenURL: "https://provider.com/oauth/token", Scopes: []string{"email", "profile"}, } app.Get("/login", oauth2.Login(config)) app.Get("/callback", oauth2.Callback(config, handleUser)) ``` ## Configuration ### Config | Field | Type | Description | | -------------- | ---------- | ------------------------- | | `ClientID` | `string` | OAuth client ID | | `ClientSecret` | `string` | OAuth client secret | | `RedirectURL` | `string` | Callback URL | | `AuthURL` | `string` | Provider's auth endpoint | | `TokenURL` | `string` | Provider's token endpoint | | `Scopes` | `[]string` | Requested scopes | ## Examples ### Google OAuth ```go theme={null} config := oauth2.Google(oauth2.Config{ ClientID: os.Getenv("GOOGLE_CLIENT_ID"), ClientSecret: os.Getenv("GOOGLE_CLIENT_SECRET"), RedirectURL: "http://localhost:3000/auth/google/callback", Scopes: []string{"email", "profile"}, }) app.Get("/auth/google", oauth2.Login(config)) app.Get("/auth/google/callback", oauth2.Callback(config, func(c *mizu.Ctx, token *oauth2.Token, user *oauth2.User) error { // User authenticated successfully session.Get(c).Set("user_id", user.ID) return c.Redirect(302, "/dashboard") })) ``` ### GitHub OAuth ```go theme={null} config := oauth2.GitHub(oauth2.Config{ ClientID: os.Getenv("GITHUB_CLIENT_ID"), ClientSecret: os.Getenv("GITHUB_CLIENT_SECRET"), RedirectURL: "http://localhost:3000/auth/github/callback", Scopes: []string{"user:email"}, }) app.Get("/auth/github", oauth2.Login(config)) app.Get("/auth/github/callback", oauth2.Callback(config, handleGitHubUser)) ``` ### Custom Provider ```go theme={null} config := oauth2.Config{ ClientID: "client-id", ClientSecret: "client-secret", RedirectURL: "http://localhost:3000/callback", AuthURL: "https://auth.example.com/authorize", TokenURL: "https://auth.example.com/token", UserInfoURL: "https://api.example.com/userinfo", Scopes: []string{"openid", "profile"}, } ``` ### State Parameter ```go theme={null} app.Get("/login", oauth2.LoginWithState(config, func(c *mizu.Ctx) string { // Generate state with return URL return base64.URLEncoding.EncodeToString([]byte(c.Query("return_url"))) })) ``` ## API Reference ### Functions ```go theme={null} // Login starts OAuth flow func Login(config Config) mizu.Handler // LoginWithState starts flow with custom state func LoginWithState(config Config, stateFn func(*mizu.Ctx) string) mizu.Handler // Callback handles OAuth callback func Callback(config Config, handler CallbackHandler) mizu.Handler // Provider presets func Google(config Config) Config func GitHub(config Config) Config func Facebook(config Config) Config ``` ### Types ```go theme={null} type Token struct { AccessToken string RefreshToken string Expiry time.Time } type User struct { ID string Email string Name string Picture string Provider string } type CallbackHandler func(*mizu.Ctx, *Token, *User) error ``` ## Technical Details ### Architecture The OAuth2 middleware is designed as a resource server implementation that validates OAuth 2.0 access tokens. It follows a flexible architecture that supports multiple validation strategies: 1. **Token Extraction**: Supports multiple token sources (header, query, form) via configurable `TokenLookup` parameter 2. **Token Validation**: Two validation strategies: * Custom validator function for flexibility * Token introspection endpoint (RFC 7662) for standard OAuth servers 3. **Scope Verification**: Automatic scope checking against required scopes 4. **Context Storage**: Validated token information is stored in request context for downstream handlers ### Token Validation Flow 1. Extract token from request based on `TokenLookup` configuration 2. Validate token using: * Custom `Validator` function if provided, or * OAuth 2.0 token introspection endpoint 3. Check token expiration if `ExpiresAt` is set 4. Verify required scopes match token scopes 5. Store validated token in context 6. Pass request to next handler ### Token Introspection When using the introspection endpoint, the middleware: * Sends POST request to `IntrospectionURL` with token * Uses Basic authentication with `ClientID` and `ClientSecret` if provided * Parses introspection response according to RFC 7662 * Validates the `active` field and extracts token metadata (subject, scopes, expiry) ### Error Handling The middleware returns appropriate HTTP status codes: * `401 Unauthorized`: Missing, invalid, or expired tokens * `403 Forbidden`: Insufficient scopes * Sets `WWW-Authenticate: Bearer` header on errors Custom error handling can be implemented via the `ErrorHandler` option. ### Context Access Helper functions provide convenient access to token information: * `Get(c)`: Retrieves full token object * `Subject(c)`: Gets token subject * `Scopes(c)`: Returns token scopes * `HasScope(c, scope)`: Checks for specific scope ### Scope Middleware The `RequireScopes` middleware can be chained after the main OAuth2 middleware to enforce scope requirements at specific routes or route groups. ## Security Considerations 1. **Store secrets securely** - Use environment variables 2. **Validate state parameter** - Prevents CSRF attacks 3. **Use HTTPS** - Required for production OAuth 4. **Verify tokens** - Check expiration and validity 5. **Limit scopes** - Request only needed permissions ## Best Practices * Store tokens securely (encrypted if stored) * Implement token refresh for long sessions * Handle callback errors gracefully * Log OAuth events for security monitoring ## Testing The OAuth2 middleware includes comprehensive test coverage for all functionality: | Test Case | Description | Expected Behavior | | ------------------------------------------------- | ------------------------------------ | --------------------------------------- | | `TestNew` - valid token | Request with valid Bearer token | Returns 200 OK, token validated | | `TestNew` - invalid token | Request with invalid Bearer token | Returns 401 Unauthorized | | `TestNew` - missing token | Request without Authorization header | Returns 401 Unauthorized | | `TestWithOptions_RequiredScopes` | Token with insufficient scopes | Returns 403 Forbidden | | `TestWithOptions_ExpiredToken` | Token with past expiration time | Returns 401 Unauthorized | | `TestGet` | Retrieve token from context | Token object accessible via Get() | | `TestSubject` | Extract subject from token | Subject field returned correctly | | `TestHasScope` | Check for specific scope | Returns true/false based on presence | | `TestRequireScopes` | Scope enforcement middleware | Blocks request if scopes missing | | `TestWWWAuthenticateHeader` | Invalid token response | WWW-Authenticate header present | | `TestExtractToken_Query` | Token in query parameter | Token extracted from URL query | | `TestExtractToken_Form` | Token in form body | Token extracted from form data | | `TestExtractToken_InvalidLookup` | Malformed TokenLookup config | Returns 401, no token found | | `TestExtractToken_UnknownSource` | Unknown token source type | Returns 401, no token found | | `TestExtractToken_HeaderWithoutBearer` | Plain token without Bearer prefix | Token extracted and validated | | `TestWithOptions_NoValidator` | No validator configured | Returns 401 with ErrNoValidator | | `TestWithOptions_CustomErrorHandler` | Custom error handler set | Custom handler invoked on error | | `TestIntrospectToken` - active | Introspection returns active token | Returns 200 OK | | `TestIntrospectToken` - inactive | Introspection returns inactive token | Returns 401 Unauthorized | | `TestIntrospectToken_ServerError` | Introspection endpoint error | Returns 401 Unauthorized | | `TestIntrospectToken_InvalidJSON` | Malformed introspection response | Returns 401 Unauthorized | | `TestIntrospectToken_ConnectionError` | Cannot connect to endpoint | Returns 401 Unauthorized | | `TestGet_NoToken` | Get() without middleware | Returns nil | | `TestSubject_NoToken` | Subject() without token | Returns empty string | | `TestScopes` | Retrieve scopes list | Returns all token scopes | | `TestScopes_NoToken` | Scopes() without token | Returns nil | | `TestRequireScopes_NoToken` | RequireScopes without auth | Returns 401 Unauthorized | | `TestRequireScopes_Sufficient` | Token has required scopes | Returns 200 OK, access granted | | `TestHasScope_NoToken` | HasScope() without token | Returns false | | `TestErrorTypes` | Error message strings | All error types return correct messages | | `TestWithOptions_IntrospectionWithoutClientCreds` | Introspection without client auth | No Basic auth sent, request succeeds | ## Related Middlewares * [oidc](/middlewares/oidc) - OpenID Connect * [jwt](/middlewares/jwt) - JWT authentication * [session](/middlewares/session) - Session management # OIDC Source: https://docs.go-mizu.dev/middlewares/oidc OpenID Connect authentication middleware for identity provider integration. ## Overview The `oidc` middleware provides OpenID Connect authentication, extending OAuth 2.0 with standardized identity tokens and user information endpoints. Use it when you need: * Enterprise SSO integration * Standardized identity claims * ID token validation ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/oidc" ``` ## Quick Start ```go theme={null} app := mizu.New() // Configure OIDC provider provider, _ := oidc.NewProvider("https://accounts.google.com") config := oidc.Config{ Provider: provider, ClientID: "your-client-id", ClientSecret: "your-client-secret", RedirectURL: "http://localhost:3000/callback", Scopes: []string{"openid", "email", "profile"}, } app.Get("/login", oidc.Login(config)) app.Get("/callback", oidc.Callback(config, handleUser)) ``` ## Configuration ### Config | Field | Type | Description | | -------------- | ----------- | ---------------- | | `Provider` | `*Provider` | OIDC provider | | `ClientID` | `string` | Client ID | | `ClientSecret` | `string` | Client secret | | `RedirectURL` | `string` | Callback URL | | `Scopes` | `[]string` | Requested scopes | ## Examples ### Google OIDC ```go theme={null} provider, _ := oidc.NewProvider("https://accounts.google.com") config := oidc.Config{ Provider: provider, ClientID: os.Getenv("GOOGLE_CLIENT_ID"), ClientSecret: os.Getenv("GOOGLE_CLIENT_SECRET"), RedirectURL: "http://localhost:3000/auth/callback", Scopes: []string{"openid", "email", "profile"}, } app.Get("/auth/login", oidc.Login(config)) app.Get("/auth/callback", oidc.Callback(config, func(c *mizu.Ctx, claims *oidc.Claims) error { // claims.Subject - User ID // claims.Email - User email // claims.Name - User name return c.Redirect(302, "/dashboard") })) ``` ### Auth0 ```go theme={null} provider, _ := oidc.NewProvider("https://your-tenant.auth0.com/") config := oidc.Config{ Provider: provider, ClientID: os.Getenv("AUTH0_CLIENT_ID"), ClientSecret: os.Getenv("AUTH0_CLIENT_SECRET"), RedirectURL: "http://localhost:3000/callback", Scopes: []string{"openid", "email", "profile"}, } ``` ### Keycloak ```go theme={null} provider, _ := oidc.NewProvider("https://keycloak.example.com/realms/myrealm") config := oidc.Config{ Provider: provider, ClientID: "my-app", ClientSecret: os.Getenv("KEYCLOAK_SECRET"), RedirectURL: "http://localhost:3000/callback", Scopes: []string{"openid", "email", "roles"}, } ``` ### Token Validation Middleware ```go theme={null} verifier := oidc.NewVerifier(provider, oidc.VerifierConfig{ ClientID: "your-client-id", }) // Protect routes with ID token app.Use(oidc.Verify(verifier)) app.Get("/api/me", func(c *mizu.Ctx) error { claims := oidc.GetClaims(c) return c.JSON(200, claims) }) ``` ## API Reference ### Functions ```go theme={null} // NewProvider creates OIDC provider from issuer URL func NewProvider(issuer string) (*Provider, error) // Login starts OIDC flow func Login(config Config) mizu.Handler // Callback handles OIDC callback func Callback(config Config, handler func(*mizu.Ctx, *Claims) error) mizu.Handler // Verify creates ID token verification middleware func Verify(verifier *Verifier) mizu.Middleware // GetClaims returns claims from context func GetClaims(c *mizu.Ctx) *Claims ``` ### Standard Claims ```go theme={null} type Claims struct { Subject string // sub Issuer string // iss Audience []string // aud Email string // email EmailVerified bool // email_verified Name string // name PreferredUsername string // preferred_username Picture string // picture ExpiresAt time.Time } ``` ## Technical Details ### Token Verification Process The OIDC middleware implements a comprehensive JWT token verification process: 1. **Token Extraction**: Tokens are extracted from the `Authorization` header using the Bearer scheme by default. A custom `TokenExtractor` function can be provided for alternative extraction methods (e.g., from query parameters or cookies). 2. **Token Structure Validation**: The token is split into three parts (header, payload, signature) and validated for correct JWT format. 3. **Header Decoding**: The JWT header is decoded to extract the algorithm (`alg`), key ID (`kid`), and token type (`typ`). 4. **Claims Decoding**: The payload is decoded into standard OIDC claims including issuer, subject, audience, expiration, and custom claims. 5. **Validation Checks**: * **Issuer Validation**: Verifies the token was issued by the expected OIDC provider * **Audience Validation**: Ensures the token is intended for this application (supports both string and array audiences) * **Expiration Check**: Validates the token has not expired using the `exp` claim * **Not Before Check**: Validates the token is valid using the `nbf` claim if present 6. **Context Storage**: Validated claims are stored in the request context for downstream handlers to access via `GetClaims()`. ### Claims Structure The middleware supports both standard OIDC claims and custom claims: * **Standard Claims**: `iss`, `sub`, `aud`, `exp`, `iat`, `nbf`, `email`, `name` * **Authorization Claims**: `groups`, `roles`, `scope` * **Raw Claims**: All claims are also stored in a `Raw` map for accessing custom provider-specific claims ### Authorization Helpers Three specialized middleware functions provide fine-grained access control: * **RequireGroup**: Validates the user belongs to a specific group (checks `groups` claim) * **RequireRole**: Validates the user has a specific role (checks `roles` claim) * **RequireScope**: Validates the token includes a specific OAuth scope (checks space-delimited `scope` claim) ### Error Handling The middleware provides customizable error handling through the `OnError` callback: * **Default Behavior**: Returns a 401 Unauthorized JSON response with error details * **Custom Handler**: Can be configured to log errors, return custom responses, or redirect to login ### Path Skipping The `SkipPaths` option allows certain routes to bypass authentication (e.g., health checks, public endpoints) while keeping the middleware globally applied. ## Best Practices * Use OIDC discovery (/.well-known/openid-configuration) * Validate ID tokens for security * Store only necessary claims in sessions * Implement proper logout with end\_session\_endpoint * Configure appropriate `RefreshInterval` for JWKS key rotation * Use `SkipPaths` for public endpoints instead of conditional middleware application * Implement custom `OnError` handlers for security-sensitive logging * Leverage `RequireGroup`, `RequireRole`, and `RequireScope` for authorization ## Testing The OIDC middleware includes comprehensive test coverage for various scenarios: | Test Case | Description | Expected Behavior | | -------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------ | | `TestNew` | Basic middleware initialization without token | Returns 401 Unauthorized | | `TestWithValidToken` | Valid token with correct issuer, audience, and expiration | Returns 200 OK, claims accessible via GetClaims | | `TestExpiredToken` | Token with expiration time in the past | Returns 401 Unauthorized | | `TestInvalidIssuer` | Token with incorrect issuer claim | Returns 401 Unauthorized | | `TestInvalidAudience` | Token with incorrect audience claim | Returns 401 Unauthorized | | `TestSkipPaths` | Configured paths that bypass authentication | Skipped paths return 200 OK without token, protected paths require token | | `TestCustomTokenExtractor` | Custom token extraction from query parameters | Successfully authenticates using token from query string | | `TestCustomOnError` | Custom error handler configuration | Returns custom error response (403 Forbidden) | | `TestClaimsHasAudience` | String and array audience validation | Correctly identifies audience in both string and array formats | | `TestClaimsHasGroup` | Group membership validation | Correctly identifies user group membership | | `TestClaimsHasRole` | Role membership validation | Correctly identifies user role membership | | `TestRequireGroup` | Group-based authorization middleware | Allows access with required group, denies without (403 Forbidden) | | `TestRequireScope` | Scope-based authorization middleware | Allows access with required scope, denies without (403 Forbidden) | | `TestInvalidToken` | Malformed token format | Returns 401 Unauthorized | ## Related Middlewares * [oauth2](/middlewares/oauth2) - OAuth 2.0 authentication * [jwt](/middlewares/jwt) - JWT validation * [session](/middlewares/session) - Session management # OpenTelemetry Source: https://docs.go-mizu.dev/middlewares/otel OpenTelemetry integration middleware for distributed tracing and metrics. ## Overview The `otel` middleware integrates with OpenTelemetry for distributed tracing, metrics, and observability. Use it when you need: * Distributed tracing * OpenTelemetry integration * Observability platforms (Jaeger, Zipkin, etc.) ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/otel" ``` ## Quick Start ```go theme={null} app := mizu.New() // Initialize OpenTelemetry shutdown := otel.Init(otel.Options{ ServiceName: "my-service", Endpoint: "localhost:4317", }) defer shutdown() app.Use(otel.Middleware()) ``` ## Configuration ### Options | Option | Type | Default | Description | | ------------- | ---------- | -------- | ------------------- | | `ServiceName` | `string` | Required | Service name | | `Endpoint` | `string` | Required | Collector endpoint | | `Insecure` | `bool` | `false` | Skip TLS | | `Propagators` | `[]string` | W3C | Propagation formats | ## Examples ### Basic Setup ```go theme={null} shutdown := otel.Init(otel.Options{ ServiceName: "api", Endpoint: "localhost:4317", }) defer shutdown() app.Use(otel.Middleware()) ``` ### With Jaeger ```go theme={null} shutdown := otel.Init(otel.Options{ ServiceName: "api", Endpoint: "jaeger:4317", Propagators: []string{"jaeger", "w3c"}, }) ``` ### Custom Spans ```go theme={null} app.Get("/process", func(c *mizu.Ctx) error { ctx, span := otel.StartSpan(c, "process-data") defer span.End() // Add attributes span.SetAttributes( attribute.String("user.id", userID), ) // Do work result := processData(ctx) return c.JSON(200, result) }) ``` ### Propagate to Downstream ```go theme={null} app.Get("/", func(c *mizu.Ctx) error { req, _ := http.NewRequestWithContext(c.Context(), "GET", url, nil) otel.Inject(c.Context(), req.Header) // Make request... }) ``` ## API Reference ### Functions ```go theme={null} // Init initializes OpenTelemetry func Init(opts Options) func() // Middleware creates OTel middleware func Middleware() mizu.Middleware // StartSpan creates child span func StartSpan(c *mizu.Ctx, name string) (context.Context, trace.Span) // Inject propagates context func Inject(ctx context.Context, headers http.Header) ``` ## Technical Details ### Architecture The otel middleware is a lightweight OpenTelemetry-compatible implementation that provides distributed tracing without external dependencies. It implements the OpenTelemetry specification for trace context propagation. ### Components **SpanContext**: Holds the trace context with TraceID (16 bytes), SpanID (8 bytes), TraceFlags, and optional TraceState. **Span**: Represents a single operation with metadata including: * Name (HTTP method + path) * Parent/child relationships * Start/end timestamps * Status (Unset, OK, Error) * Attributes (key-value pairs) * Events (timestamped logs) * Links (connections to other spans) **SpanProcessor**: Interface for processing completed spans. Implementations include: * `InMemoryProcessor`: Stores spans in memory for testing * `PrintProcessor`: Prints spans to stdout * Custom processors can export to external systems ### Propagation Formats **W3C Trace Context (default)**: * Header: `Traceparent: 00-{traceId}-{spanId}-{flags}` * Optional: `Tracestate` for vendor-specific data * Format: `version-traceId-spanId-flags` **B3 Propagation**: * Single header: `B3: {traceId}-{spanId}-{sampled}` * Multi-header: `X-B3-Traceid`, `X-B3-Spanid`, `X-B3-Sampled` * Compatible with Zipkin and other B3-based systems ### ID Generation * TraceID: 128-bit (16 bytes) cryptographically random hex string * SpanID: 64-bit (8 bytes) cryptographically random hex string * Generated using `crypto/rand` for uniqueness ### Request Flow 1. Extract parent context from incoming request headers 2. Create new span with inherited TraceID or generate new one 3. Generate new SpanID for current span 4. Add default HTTP attributes (method, URL, status, etc.) 5. Call OnStart hook if configured 6. Inject trace context into response headers 7. Store span in request context 8. Execute handler 9. Set final status and attributes based on response 10. Call OnEnd hook and SpanProcessor ### Thread Safety * Span methods use mutex locks for concurrent access * InMemoryProcessor is thread-safe with mutex protection * Safe for use with concurrent requests ## Best Practices * Use consistent service names * Add meaningful span attributes * Propagate context to all calls * Sample appropriately in production ## Testing The otel middleware includes comprehensive test coverage for all features: | Test Case | Description | Expected Behavior | | ------------------------------ | ----------------------------- | -------------------------------------------------------------------------------------------------------- | | `TestNew` | Basic middleware creation | Creates middleware with default options and processes requests successfully | | `TestSpanCreation` | Span generation | Creates span with correct name (HTTP method + path), generates TraceID and SpanID | | `TestSpanAttributes` | Attribute collection | Captures service.name, service.version, http.method, http.status\_code, http.user\_agent | | `TestW3CTracePropagation` | W3C Trace Context propagation | Inherits TraceID from parent, generates new SpanID, sets parent relationship, injects traceparent header | | `TestB3Propagation` | B3 single header format | Extracts trace context from B3 header, injects B3 headers in response | | `TestB3MultiHeaderPropagation` | B3 multi-header format | Extracts trace context from X-B3-\* headers, maintains compatibility | | `TestSkipPaths` | Path filtering | Skips configured paths (e.g., /health), traces non-skipped paths | | `TestSampler` | Custom sampling | Respects sampler function, only creates spans for sampled requests | | `TestErrorStatus` | Error handling | Sets StatusError for HTTP 4xx/5xx responses, captures error attributes | | `TestOnStartOnEnd` | Lifecycle hooks | Calls OnStart when span begins, OnEnd when span completes | | `TestGetSpan` | Context access | Retrieves span from context, allows custom attributes and events | | `TestSpanDuration` | Timing measurement | Calculates span duration, sets end time correctly | | `TestSpanContextIsValid` | Context validation | Validates SpanContext has TraceID and SpanID | | `TestSpanContextIsSampled` | Sampling flag check | Correctly interprets trace flags for sampling decision | | `TestInMemoryProcessor` | Span processing | Stores spans, retrieves all spans, clears span storage | ## Related Middlewares * [trace](/middlewares/trace) - Simple tracing * [prometheus](/middlewares/prometheus) - Prometheus metrics * [sentry](/middlewares/sentry) - Error tracking # Overview Source: https://docs.go-mizu.dev/middlewares/overview A comprehensive collection of common middlewares for Mizu. Mizu provides a rich set of middlewares to handle common web application concerns. All middlewares follow these principles: * **Zero external dependencies** - Only uses Go standard library * **Sensible defaults** - Works out of the box * **Options pattern** - Fully configurable when needed * **Concurrent safe** - Safe for use across goroutines ## Installation Middlewares are included with Mizu. Import the specific middleware you need: ```go theme={null} import ( "github.com/go-mizu/mizu" "github.com/go-mizu/mizu/middlewares/recover" "github.com/go-mizu/mizu/middlewares/requestid" "github.com/go-mizu/mizu/middlewares/timeout" ) ``` ## Quick Start A typical production setup: ```go theme={null} package main import ( "time" "github.com/go-mizu/mizu" "github.com/go-mizu/mizu/middlewares/helmet" "github.com/go-mizu/mizu/middlewares/ratelimit" "github.com/go-mizu/mizu/middlewares/recover" "github.com/go-mizu/mizu/middlewares/requestid" "github.com/go-mizu/mizu/middlewares/timeout" ) func main() { app := mizu.New() // Essential middlewares app.Use(recover.New()) // Panic recovery app.Use(requestid.New()) // Request tracing app.Use(timeout.New(30*time.Second)) // Request timeout app.Use(helmet.Default()) // Security headers app.Use(ratelimit.PerMinute(100)) // Rate limiting app.Get("/", func(c *mizu.Ctx) error { return c.Text(200, "Hello, World!") }) app.Listen(":3000") } ``` ## Categories ### Authentication Secure your endpoints with various authentication methods: | Middleware | Description | | ------------------------------------- | ------------------------------------- | | [basicauth](/middlewares/basicauth) | HTTP Basic Authentication | | [bearerauth](/middlewares/bearerauth) | Bearer token authentication | | [keyauth](/middlewares/keyauth) | API key authentication | | [csrf](/middlewares/csrf) | Cross-Site Request Forgery protection | | [csrf2](/middlewares/csrf2) | Enhanced CSRF with double submit | | [jwt](/middlewares/jwt) | JWT token authentication | | [oauth2](/middlewares/oauth2) | OAuth 2.0 authentication flows | | [oidc](/middlewares/oidc) | OpenID Connect authentication | ### Security Protect your application from common web vulnerabilities: | Middleware | Description | | ----------------------------------- | ----------------------------------------------------- | | [helmet](/middlewares/helmet) | Security headers (CSP, HSTS, etc.) | | [secure](/middlewares/secure) | HTTPS enforcement and security features | | [ipfilter](/middlewares/ipfilter) | IP whitelist/blacklist | | [honeypot](/middlewares/honeypot) | Detect and block malicious requests | | [captcha](/middlewares/captcha) | CAPTCHA verification (reCAPTCHA, hCaptcha, Turnstile) | | [cors](/middlewares/cors) | Cross-Origin Resource Sharing | | [cors2](/middlewares/cors2) | Enhanced CORS with preflight caching | | [rbac](/middlewares/rbac) | Role-based access control | | [signature](/middlewares/signature) | Request signature verification | ### Rate Limiting & Resilience Control request rates and protect against overload: | Middleware | Description | | --------------------------------------------- | -------------------------- | | [ratelimit](/middlewares/ratelimit) | Token bucket rate limiting | | [circuitbreaker](/middlewares/circuitbreaker) | Circuit breaker pattern | | [bulkhead](/middlewares/bulkhead) | Bulkhead isolation pattern | | [throttle](/middlewares/throttle) | Request throttling | | [concurrency](/middlewares/concurrency) | Concurrency limiting | | [adaptive](/middlewares/adaptive) | Adaptive rate limiting | ### Request Processing Validate and transform incoming requests: | Middleware | Description | | --------------------------------------------- | ------------------------------- | | [bodylimit](/middlewares/bodylimit) | Limit request body size | | [contenttype](/middlewares/contenttype) | Validate Content-Type | | [validator](/middlewares/validator) | Request validation rules | | [header](/middlewares/header) | Header manipulation | | [methodoverride](/middlewares/methodoverride) | HTTP method override | | [bodyclose](/middlewares/bodyclose) | Auto-close request body | | [bodydump](/middlewares/bodydump) | Dump request/response bodies | | [requestsize](/middlewares/requestsize) | Request size tracking | | [sanitizer](/middlewares/sanitizer) | Input sanitization | | [transformer](/middlewares/transformer) | Request/response transformation | | [filter](/middlewares/filter) | Request filtering | ### Response Processing Transform and enhance outgoing responses: | Middleware | Description | | ----------------------------------------- | --------------------------- | | [compress](/middlewares/compress) | Gzip/deflate compression | | [envelope](/middlewares/envelope) | Response envelope wrapper | | [responsesize](/middlewares/responsesize) | Response size tracking | | [vary](/middlewares/vary) | Vary header management | | [errorpage](/middlewares/errorpage) | Custom error pages | | [hypermedia](/middlewares/hypermedia) | Hypermedia response helpers | ### Caching Control HTTP caching behavior: | Middleware | Description | | ----------------------------------------- | ------------------------- | | [cache](/middlewares/cache) | Set Cache-Control headers | | [nocache](/middlewares/nocache) | Prevent caching | | [etag](/middlewares/etag) | ETag generation | | [lastmodified](/middlewares/lastmodified) | Last-Modified headers | ### URL Handling Redirect and rewrite URLs: | Middleware | Description | | --------------------------------- | ---------------------------- | | [redirect](/middlewares/redirect) | URL redirection (HTTPS, WWW) | | [slash](/middlewares/slash) | Trailing slash handling | | [rewrite](/middlewares/rewrite) | URL rewriting | ### Networking & Proxy Handle proxy headers and reverse proxying: | Middleware | Description | | ----------------------------------- | ------------------------ | | [proxy](/middlewares/proxy) | Reverse proxy | | [forwarded](/middlewares/forwarded) | X-Forwarded-\* headers | | [realip](/middlewares/realip) | Extract real client IP | | [h2c](/middlewares/h2c) | HTTP/2 cleartext support | | [surrogate](/middlewares/surrogate) | CDN surrogate headers | ### Request Context Enhance request handling: | Middleware | Description | | --------------------------------------- | -------------------------------- | | [requestid](/middlewares/requestid) | Generate/propagate request IDs | | [timeout](/middlewares/timeout) | Request timeout | | [recover](/middlewares/recover) | Panic recovery | | [timing](/middlewares/timing) | Server-Timing header | | [trace](/middlewares/trace) | Distributed tracing context | | [conditional](/middlewares/conditional) | Conditional middleware execution | ### Real-time Support real-time communication: | Middleware | Description | | ----------------------------------- | --------------------- | | [websocket](/middlewares/websocket) | WebSocket connections | | [sse](/middlewares/sse) | Server-Sent Events | ### Static Files Serve static content: | Middleware | Description | | ------------------------------- | ------------------------------- | | [static](/middlewares/static) | Static file serving | | [spa](/middlewares/spa) | Single Page Application support | | [favicon](/middlewares/favicon) | Favicon serving | | [embed](/middlewares/embed) | Embedded filesystem serving | ### Operations & Monitoring Tools for production operations: | Middleware | Description | | --------------------------------------- | --------------------------- | | [version](/middlewares/version) | API versioning | | [maintenance](/middlewares/maintenance) | Maintenance mode | | [pprof](/middlewares/pprof) | Profiling endpoints | | [healthcheck](/middlewares/healthcheck) | Health check endpoints | | [metrics](/middlewares/metrics) | Custom metrics collection | | [prometheus](/middlewares/prometheus) | Prometheus metrics endpoint | | [expvar](/middlewares/expvar) | Expvar metrics endpoint | | [logger](/middlewares/logger) | Request logging | | [requestlog](/middlewares/requestlog) | Detailed request logging | | [responselog](/middlewares/responselog) | Response logging | ### Advanced Specialized functionality: | Middleware | Description | | ----------------------------------------- | ------------------------- | | [feature](/middlewares/feature) | Feature flags | | [multitenancy](/middlewares/multitenancy) | Multi-tenant support | | [chaos](/middlewares/chaos) | Chaos engineering | | [mirror](/middlewares/mirror) | Request mirroring | | [fingerprint](/middlewares/fingerprint) | Request fingerprinting | | [canary](/middlewares/canary) | Canary deployment routing | | [audit](/middlewares/audit) | Audit logging | | [idempotency](/middlewares/idempotency) | Idempotency key handling | | [retry](/middlewares/retry) | Automatic request retries | | [hedge](/middlewares/hedge) | Hedged requests | | [fallback](/middlewares/fallback) | Fallback handlers | | [mock](/middlewares/mock) | Request mocking | ### Connection & Protocol Connection and protocol handling: | Middleware | Description | | ----------------------------------- | ----------------------------- | | [keepalive](/middlewares/keepalive) | Connection keep-alive control | | [maxconns](/middlewares/maxconns) | Maximum connections limit | | [msgpack](/middlewares/msgpack) | MessagePack handling | | [jsonrpc](/middlewares/jsonrpc) | JSON-RPC protocol support | | [graphql](/middlewares/graphql) | GraphQL request handling | | [xml](/middlewares/xml) | XML request/response handling | ### Internationalization Locale and timezone handling: | Middleware | Description | | --------------------------------- | ------------------------------ | | [language](/middlewares/language) | Language detection | | [timezone](/middlewares/timezone) | Timezone detection | | [nonce](/middlewares/nonce) | Cryptographic nonce generation | ### External Integrations Third-party service integrations: | Middleware | Description | | ------------------------------- | ------------------------- | | [otel](/middlewares/otel) | OpenTelemetry integration | | [sentry](/middlewares/sentry) | Sentry error tracking | | [session](/middlewares/session) | Session management | ### Bot & Client Detection Client identification and validation: | Middleware | Description | | --------------------------------------------- | --------------------------- | | [bot](/middlewares/bot) | Bot detection | | [xrequestedwith](/middlewares/xrequestedwith) | X-Requested-With validation | ## Usage Patterns ### Global middleware Apply to all routes: ```go theme={null} app.Use(recover.New()) ``` ### Route-specific middleware Apply to specific routes: ```go theme={null} app.Get("/admin", adminHandler, basicauth.New(credentials)) ``` ### Group middleware Apply to a group of routes: ```go theme={null} api := app.Group("/api") api.Use(keyauth.New(validateKey)) api.Get("/users", listUsers) api.Post("/users", createUser) ``` ### Conditional middleware Skip middleware based on conditions: ```go theme={null} app.Use(ratelimit.WithOptions(ratelimit.Options{ Rate: 100, Interval: time.Minute, Skip: func(c *mizu.Ctx) bool { return c.Request().URL.Path == "/health" }, })) ``` ## Best Practices 1. **Order matters** - Place `recover` first to catch all panics 2. **Use timeouts** - Always set request timeouts in production 3. **Rate limit** - Protect public endpoints from abuse 4. **Security headers** - Use `helmet` for recommended security headers 5. **Request IDs** - Add `requestid` for tracing and debugging ## Next Steps * Learn about [writing custom middleware](/concepts/middleware) * Explore individual middleware documentation in the sidebar # Pprof Source: https://docs.go-mizu.dev/middlewares/pprof Profiling endpoints middleware for performance analysis. ## Overview The `pprof` middleware exposes Go's built-in profiling endpoints for performance analysis and debugging. It integrates the standard `net/http/pprof` package with Mizu. ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/pprof" ``` ## Quick Start ```go theme={null} app := mizu.New() // Expose pprof endpoints at /debug/pprof app.Use(pprof.New()) ``` ## Configuration | Option | Type | Default | Description | | -------- | -------- | ---------------- | ------------------------------ | | `Prefix` | `string` | `"/debug/pprof"` | URL prefix for pprof endpoints | ## Endpoints | Path | Description | | --------------------------- | ---------------------------- | | `/debug/pprof/` | Index page with all profiles | | `/debug/pprof/cmdline` | Command line arguments | | `/debug/pprof/profile` | CPU profile (30s default) | | `/debug/pprof/symbol` | Symbol lookup | | `/debug/pprof/trace` | Execution trace | | `/debug/pprof/heap` | Heap memory profile | | `/debug/pprof/goroutine` | Goroutine stack traces | | `/debug/pprof/block` | Blocking profile | | `/debug/pprof/mutex` | Mutex contention profile | | `/debug/pprof/allocs` | Memory allocations | | `/debug/pprof/threadcreate` | Thread creation profile | ## Examples ### Basic Usage ```go theme={null} app := mizu.New() // Add pprof middleware app.Use(pprof.New()) // Access at http://localhost:8080/debug/pprof/ ``` ### Custom Prefix ```go theme={null} app.Use(pprof.WithOptions(pprof.Options{ Prefix: "/internal/profiling", })) // Access at http://localhost:8080/internal/profiling/ ``` ### Protected Pprof ```go theme={null} // Create a sub-app for admin routes admin := app.Group("/admin") // Add authentication admin.Use(basicauth.New(basicauth.Options{ Users: map[string]string{ "admin": "secret", }, })) // Add pprof to admin group admin.Use(pprof.WithOptions(pprof.Options{ Prefix: "/admin/pprof", })) ``` ### Conditional Pprof ```go theme={null} // Only enable in development if os.Getenv("ENV") == "development" { app.Use(pprof.New()) } ``` ### IP Restricted ```go theme={null} // Only allow from localhost app.Use(ipfilter.WithOptions(ipfilter.Options{ Whitelist: []string{"127.0.0.1", "::1"}, PathPrefix: "/debug/pprof", })) app.Use(pprof.New()) ``` ## Using the Profiles ### CPU Profile Capture a 30-second CPU profile: ```bash theme={null} go tool pprof http://localhost:8080/debug/pprof/profile ``` Or with a custom duration: ```bash theme={null} curl "http://localhost:8080/debug/pprof/profile?seconds=60" > cpu.prof go tool pprof cpu.prof ``` ### Heap Profile ```bash theme={null} go tool pprof http://localhost:8080/debug/pprof/heap ``` ### Goroutine Dump ```bash theme={null} curl http://localhost:8080/debug/pprof/goroutine?debug=2 ``` ### Execution Trace ```bash theme={null} curl "http://localhost:8080/debug/pprof/trace?seconds=5" > trace.out go tool trace trace.out ``` ### Memory Allocations ```bash theme={null} go tool pprof http://localhost:8080/debug/pprof/allocs ``` ## Common Pprof Commands Inside `go tool pprof`: ```bash theme={null} # Top functions by CPU/memory top 10 # View call graph web # List function source list # Show flamegraph (web view) web ``` ## API Reference ```go theme={null} func New() mizu.Middleware func WithOptions(opts Options) mizu.Middleware ``` ## Technical Details The pprof middleware integrates Go's standard `net/http/pprof` package with Mizu's middleware system. Here's how it works: ### Implementation Architecture The middleware uses a path-based routing strategy to handle different profiling endpoints: 1. **Path Matching**: The middleware checks if the incoming request path matches the configured prefix (default: `/debug/pprof`) 2. **Pass-through**: If the path doesn't match, the request is passed to the next middleware in the chain 3. **Subpath Routing**: For matching paths, the middleware extracts the subpath and routes to the appropriate pprof handler ### Handler Routing Logic * **Index Page** (`/` or empty subpath): Displays the pprof index using `pprof.Index()` * **Special Handlers**: Four endpoints have dedicated handlers: * `/cmdline`: Command line invocation using `pprof.Cmdline()` * `/profile`: CPU profiling using `pprof.Profile()` * `/symbol`: Symbol lookup using `pprof.Symbol()` * `/trace`: Execution tracing using `pprof.Trace()` * **Named Profiles**: All other subpaths are treated as named profiles (heap, goroutine, block, mutex, allocs, threadcreate) and handled by `pprof.Handler(name)` ### Prefix Normalization The middleware automatically normalizes the prefix by removing trailing slashes to ensure consistent path matching regardless of how the prefix is configured. ### Request Flow ``` Request -> Path Check -> Match Prefix? -> No -> Next Middleware | Yes | Extract Subpath -> Route to Handler -> Serve Profile ``` ## Security Warning Pprof endpoints can expose sensitive information about your application. In production: 1. Use authentication 2. Restrict by IP address 3. Use a separate admin port 4. Or disable entirely ```go theme={null} // Production-safe setup if os.Getenv("ENABLE_PPROF") == "true" { app.Use(ipfilter.WithOptions(ipfilter.Options{ Whitelist: []string{"10.0.0.0/8"}, PathPrefix: "/debug/pprof", })) app.Use(pprof.New()) } ``` ## Best Practices * Never expose pprof publicly in production * Use authentication for protected access * Enable only when debugging is needed * Use execution traces sparingly (high overhead) * CPU profiles should be short (30-60 seconds) ## Testing The pprof middleware includes comprehensive test coverage to ensure all endpoints and configurations work correctly: | Test Case | Description | Expected Behavior | | ----------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------ | | `TestNew/index page` | Tests the pprof index page at `/debug/pprof/` | Returns HTTP 200 and contains "pprof" in response body | | `TestNew/cmdline` | Tests the command line endpoint | Returns HTTP 200 with command line arguments | | `TestNew/symbol` | Tests the symbol lookup endpoint | Returns HTTP 200 for symbol queries | | `TestNew/heap profile` | Tests the heap memory profile endpoint | Returns HTTP 200 with heap profile data | | `TestNew/goroutine profile` | Tests the goroutine stack traces endpoint | Returns HTTP 200 with goroutine information | | `TestNew/non-pprof path` | Tests that non-pprof paths pass through to next handler | Returns "home" text from the application route | | `TestWithOptions_CustomPrefix` | Tests custom prefix configuration (`/profiling`) | Returns HTTP 200 when accessing index at custom prefix | | `TestWithOptions_PrefixWithTrailingSlash` | Tests prefix normalization with trailing slash | Returns HTTP 200, correctly handles trailing slash in prefix | ### Test Coverage The test suite validates: * All major pprof endpoints (index, cmdline, symbol, heap, goroutine) * Middleware pass-through for non-matching paths * Custom prefix configuration * Prefix normalization (trailing slash handling) ## Related Middlewares * [basicauth](/middlewares/basicauth) - Basic authentication * [ipfilter](/middlewares/ipfilter) - IP filtering * [timing](/middlewares/timing) - Performance timing # Prometheus Source: https://docs.go-mizu.dev/middlewares/prometheus Prometheus metrics endpoint middleware for monitoring. ## Overview The `prometheus` middleware exposes metrics in Prometheus format, enabling integration with Prometheus monitoring systems. Use it when you need: * Prometheus monitoring * Grafana dashboards * AlertManager integration ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/prometheus" ``` ## Quick Start ```go theme={null} app := mizu.New() // Collect metrics app.Use(prometheus.New()) // Expose endpoint app.Get("/metrics", prometheus.Handler()) ``` ## Configuration ### Options | Option | Type | Default | Description | | ----------- | ---------------------- | -------- | ---------------- | | `Namespace` | `string` | `""` | Metric namespace | | `Subsystem` | `string` | `""` | Metric subsystem | | `Buckets` | `[]float64` | Standard | Latency buckets | | `Skip` | `func(*mizu.Ctx) bool` | `nil` | Skip paths | ## Examples ### Basic Setup ```go theme={null} app.Use(prometheus.New()) app.Get("/metrics", prometheus.Handler()) ``` ### With Namespace ```go theme={null} app.Use(prometheus.New(prometheus.Options{ Namespace: "myapp", Subsystem: "api", })) ``` ### Skip Metrics Endpoint ```go theme={null} app.Use(prometheus.New(prometheus.Options{ Skip: func(c *mizu.Ctx) bool { return c.Request().URL.Path == "/metrics" }, })) ``` ### Custom Labels ```go theme={null} app.Use(prometheus.New(prometheus.Options{ Labels: func(c *mizu.Ctx) map[string]string { return map[string]string{ "service": "api", "version": "1.0", } }, })) ``` ## Default Metrics ``` # HELP http_requests_total Total HTTP requests # TYPE http_requests_total counter http_requests_total{method="GET",path="/api",status="200"} 1234 # HELP http_request_duration_seconds Request latency # TYPE http_request_duration_seconds histogram http_request_duration_seconds_bucket{...} ``` ## API Reference ### Functions ```go theme={null} // New creates Prometheus middleware func New(opts ...Options) mizu.Middleware // Handler returns metrics handler func Handler() mizu.Handler ``` ## Prometheus Config ```yaml theme={null} scrape_configs: - job_name: 'myapp' static_configs: - targets: ['localhost:8080'] metrics_path: '/metrics' ``` ## Technical Details ### Implementation Architecture The Prometheus middleware is implemented with the following key components: * **Metrics Collector**: A thread-safe `Metrics` struct that maintains multiple metric types * **Custom Histogram**: Lightweight histogram implementation for request duration and size tracking * **Response Writer Wrapper**: Custom `responseWriter` to capture HTTP status codes and response sizes * **Atomic Operations**: Uses `sync/atomic` for concurrent request counting and active request tracking ### Core Metrics The middleware collects four primary metric types: 1. **Request Counter** (`http_requests_total`): Total number of HTTP requests with labels for method, path, and status 2. **Request Duration Histogram** (`http_request_duration_seconds`): Request latency distribution in seconds 3. **Request Size Histogram** (`http_request_size_bytes`): HTTP request size distribution with fixed buckets \[100, 1000, 10000, 100000, 1000000] 4. **Response Size Histogram** (`http_response_size_bytes`): HTTP response size distribution with fixed buckets \[100, 1000, 10000, 100000, 1000000] 5. **Active Requests Gauge** (`http_requests_active`): Current number of in-flight requests ### Label Structure Metrics include the following labels: * `method`: HTTP method (GET, POST, etc.) * `path`: Request path * `status`: HTTP status code (for counter and duration metrics) ### Default Histogram Buckets Request duration uses the following default buckets (in seconds): ``` [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10] ``` ### Thread Safety * All metric recording operations are thread-safe using `sync.RWMutex` and atomic operations * Histogram observations are protected by individual mutex locks * Request counters use atomic increment operations for optimal performance ### Metric Naming Full metric names follow the pattern: * With namespace and subsystem: `__` * With namespace only: `_` * With subsystem only: `_` * Default: `` ## Best Practices * Use consistent naming * Avoid high cardinality labels * Skip health/metrics endpoints * Set appropriate scrape interval ## Testing | Test Case | Description | Expected Behavior | | ------------------------ | -------------------------------- | ---------------------------------------------------------------------------------------------------- | | TestNew | Basic middleware initialization | Middleware creation succeeds and processes requests | | TestMetricsEndpoint | Metrics endpoint functionality | Returns metrics in Prometheus format with http\_requests\_total and http\_request\_duration\_seconds | | TestRequestCounter | Request counting accuracy | TotalRequests() returns correct count after multiple requests | | TestDifferentStatusCodes | Status code labeling | Metrics include separate labels for different status codes (200, 500) | | TestNamespace | Namespace/subsystem prefixing | Metric names include configured namespace and subsystem prefixes | | TestSkipPaths | Path skipping functionality | Requests to skipped paths are not recorded in metrics | | TestHistogramBuckets | Custom histogram buckets | Metrics include custom bucket values in histogram output | | TestActiveRequests | Active requests gauge | ActiveRequests() returns 0 when no requests are in flight | | TestRegisterEndpoint | Custom metrics path registration | RegisterEndpoint() registers handler at custom path | | TestExportFormat | Prometheus format compliance | Output includes HELP, TYPE comments, buckets, sum, and count | | TestResponseSizeTracking | Response size metrics | Metrics include http\_response\_size\_bytes for responses | | TestMethodLabels | HTTP method labeling | Metrics include separate labels for different HTTP methods (GET, POST) | ## Related Middlewares * [metrics](/middlewares/metrics) - Custom metrics * [healthcheck](/middlewares/healthcheck) - Health checks * [otel](/middlewares/otel) - OpenTelemetry # Proxy Source: https://docs.go-mizu.dev/middlewares/proxy Reverse proxy middleware for forwarding requests to upstream servers. ## Overview The `proxy` middleware provides reverse proxy functionality, forwarding requests to upstream servers. It supports load balancing, request modification, and response handling. ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/proxy" ``` ## Quick Start ```go theme={null} app := mizu.New() // Forward all requests to upstream app.Use(proxy.New("http://backend:8080")) ``` ## Configuration | Option | Type | Default | Description | | ---------------- | ------------------------------ | -------- | ------------------------- | | `Target` | `*url.URL` | required | Upstream server URL | | `Rewrite` | `func(string) string` | - | Path rewriter | | `ModifyRequest` | `func(*http.Request)` | - | Request modifier | | `ModifyResponse` | `func(*http.Response) error` | - | Response modifier | | `Transport` | `http.RoundTripper` | Default | HTTP transport | | `Timeout` | `time.Duration` | `30s` | Request timeout | | `PreserveHost` | `bool` | `false` | Keep original Host header | | `ErrorHandler` | `func(*mizu.Ctx, error) error` | - | Error handler | ## Examples ### Basic Proxy ```go theme={null} app.Get("/api/*", nil, proxy.New("http://api-server:3000")) ``` ### Path Rewriting ```go theme={null} app.Use(proxy.WithOptions(proxy.Options{ Target: mustParseURL("http://backend:8080"), Rewrite: func(path string) string { return strings.TrimPrefix(path, "/api/v1") }, })) ``` ### Load Balancing ```go theme={null} app.Use(proxy.Balancer([]string{ "http://server1:8080", "http://server2:8080", "http://server3:8080", })) ``` ### Request Modification ```go theme={null} app.Use(proxy.WithOptions(proxy.Options{ Target: mustParseURL("http://backend:8080"), ModifyRequest: func(req *http.Request) { req.Header.Set("X-Source", "gateway") req.Header.Del("Authorization") // Strip auth }, })) ``` ### Response Modification ```go theme={null} app.Use(proxy.WithOptions(proxy.Options{ Target: mustParseURL("http://backend:8080"), ModifyResponse: func(resp *http.Response) error { resp.Header.Set("X-Proxy", "mizu") return nil }, })) ``` ### Preserve Host Header ```go theme={null} app.Use(proxy.WithOptions(proxy.Options{ Target: mustParseURL("http://backend:8080"), PreserveHost: true, })) ``` ### Custom Timeout ```go theme={null} app.Use(proxy.WithOptions(proxy.Options{ Target: mustParseURL("http://backend:8080"), Timeout: 60 * time.Second, })) ``` ## API Reference ```go theme={null} func New(target string) mizu.Middleware func WithOptions(opts Options) mizu.Middleware func Balancer(targets []string) mizu.Middleware ``` ## Headers Added | Header | Value | | ------------------- | --------------- | | `X-Forwarded-For` | Client IP chain | | `X-Forwarded-Host` | Original host | | `X-Forwarded-Proto` | http or https | ## Technical Details ### Request Flow 1. **URL Construction**: The target URL is built by combining the upstream target with the request path. If a `Rewrite` function is provided, it transforms the path before joining. 2. **Path Joining**: The `singleJoiningSlash` helper ensures proper path construction by handling slash prefixes/suffixes correctly, preventing issues like double slashes or missing slashes. 3. **Request Creation**: A new HTTP request is created with the same method, context, and body as the original request. 4. **Header Propagation**: All headers from the original request are copied to the proxy request using the `copyHeaders` function, which preserves multi-value headers. 5. **Forwarded Headers**: The middleware automatically sets standard forwarding headers: * `X-Forwarded-For`: Appends client IP to any existing value (creates a chain) * `X-Forwarded-Host`: Sets the original host * `X-Forwarded-Proto`: Sets `https` or `http` based on TLS presence 6. **Host Header Handling**: By default, the Host header is set to the upstream target's host. When `PreserveHost` is true, the original Host header is preserved. 7. **Request Modification**: If `ModifyRequest` is provided, it's called before sending the request, allowing custom header manipulation or other modifications. 8. **HTTP Client Configuration**: * Uses configurable transport (defaults to `http.DefaultTransport`) * Applies the specified timeout (defaults to 30 seconds) * Disables automatic redirect following (`CheckRedirect` returns `ErrUseLastResponse`) 9. **Response Handling**: After receiving the upstream response: * `ModifyResponse` callback is invoked if configured * Response headers are copied to the client * Status code is written * Response body is streamed to the client using `io.Copy` 10. **Error Handling**: Errors are passed to the custom `ErrorHandler` if configured, otherwise returns a 502 Bad Gateway response. ### Load Balancer Implementation The `Balancer` function implements simple round-robin load balancing: * Maintains a counter for selecting the next upstream * Uses modulo arithmetic to cycle through targets * Counter is not thread-safe but provides good-enough distribution for most use cases * Each request creates a new proxy middleware instance with the selected target ### Performance Considerations * **Streaming**: Response body is streamed using `io.Copy`, avoiding buffering large responses in memory * **Connection Reuse**: Uses `http.DefaultTransport` by default, which includes connection pooling * **Context Propagation**: Request context is preserved, enabling timeout and cancellation propagation * **Header Copying**: Headers are copied efficiently without additional allocations for single-value headers ## Best Practices 1. **Set Appropriate Timeouts**: Configure timeouts based on your upstream service characteristics to prevent hanging requests. 2. **Error Handling**: Implement custom error handlers for better error reporting and monitoring. 3. **Security**: Be cautious with `ModifyRequest` to avoid exposing sensitive headers to untrusted upstreams. 4. **Path Rewriting**: Use the `Rewrite` function for API versioning or routing transformations. 5. **Load Balancing**: For production load balancing, consider implementing health checks and weighted distributions. ## Testing The proxy middleware includes comprehensive test coverage for all functionality: | Test Case | Description | Expected Behavior | | -------------------------------- | ----------------------------------- | ---------------------------------------------------------------------------- | | `TestNew` | Basic proxy functionality | Forwards request to upstream, returns upstream response with headers | | `TestWithOptions_Rewrite` | Path rewriting with custom function | Transforms request path before forwarding (e.g., `/users` → `/api/v2/users`) | | `TestWithOptions_ModifyRequest` | Request modification callback | Custom headers are added to upstream request | | `TestWithOptions_ModifyResponse` | Response modification callback | Custom headers are added to client response | | `TestWithOptions_PreserveHost` | Host header preservation | Original Host header is preserved when `PreserveHost` is true | | `TestWithOptions_ErrorHandler` | Custom error handling | Custom error handler is invoked on proxy failure | | `TestWithOptions_QueryString` | Query parameter forwarding | Query strings are preserved and forwarded to upstream | | `TestWithOptions_POST` | POST request handling | POST requests with body are proxied correctly | | `TestNew_InvalidURL` | Invalid target URL validation | Panics with descriptive error on invalid URL | | `TestWithOptions_NoTarget` | Missing target validation | Panics when target is not provided | | `TestBalancer` | Round-robin load balancing | Distributes requests across multiple upstreams in rotation | | `TestBalancer_Empty` | Empty balancer targets validation | Panics when no targets are provided | ## Related Middlewares * [forwarded](/middlewares/forwarded) - Parse forwarded headers * [realip](/middlewares/realip) - Extract real client IP # Rate Limit Source: https://docs.go-mizu.dev/middlewares/ratelimit Token bucket rate limiting middleware to protect against abuse. ## Overview The `ratelimit` middleware implements token bucket rate limiting to control request rates. It protects your application from abuse, ensures fair usage, and prevents resource exhaustion. Use it when you need: * API rate limiting * Protection against brute force attacks * Fair usage enforcement * Resource protection ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/ratelimit" ``` ## Quick Start ```go theme={null} app := mizu.New() // 100 requests per minute app.Use(ratelimit.PerMinute(100)) ``` ## Configuration ### Options | Option | Type | Default | Description | | -------------- | ------------------------ | ------------ | ----------------------------- | | `Rate` | `int` | `100` | Requests allowed per interval | | `Interval` | `time.Duration` | `1m` | Time window | | `Burst` | `int` | Same as Rate | Maximum burst capacity | | `KeyFunc` | `func(*mizu.Ctx) string` | Client IP | Rate limit key extractor | | `Headers` | `bool` | `true` | Include rate limit headers | | `ErrorHandler` | `func(*mizu.Ctx) error` | - | Custom error handler | | `Skip` | `func(*mizu.Ctx) bool` | - | Skip rate limiting | ## Examples ### Basic Rate Limiting ```go theme={null} // 100 requests per minute per IP app.Use(ratelimit.PerMinute(100)) // 10 requests per second app.Use(ratelimit.PerSecond(10)) // 1000 requests per hour app.Use(ratelimit.PerHour(1000)) ``` ### Custom Rate and Interval ```go theme={null} // 50 requests per 30 seconds app.Use(ratelimit.New(50, 30*time.Second)) ``` ### With Burst ```go theme={null} // Allow burst of 200, refill at 100/minute app.Use(ratelimit.WithOptions(ratelimit.Options{ Rate: 100, Interval: time.Minute, Burst: 200, })) ``` ### Rate Limit by API Key ```go theme={null} app.Use(ratelimit.WithOptions(ratelimit.Options{ Rate: 1000, Interval: time.Hour, KeyFunc: func(c *mizu.Ctx) string { // Use API key instead of IP return c.Request().Header.Get("X-API-Key") }, })) ``` ### Rate Limit by User ```go theme={null} app.Use(ratelimit.WithOptions(ratelimit.Options{ Rate: 100, Interval: time.Minute, KeyFunc: func(c *mizu.Ctx) string { if user := GetUser(c); user != nil { return user.ID } return c.ClientIP() // Fallback to IP }, })) ``` ### Skip Certain Requests ```go theme={null} app.Use(ratelimit.WithOptions(ratelimit.Options{ Rate: 100, Interval: time.Minute, Skip: func(c *mizu.Ctx) bool { // Skip health checks if c.Request().URL.Path == "/health" { return true } // Skip authenticated admins if user := GetUser(c); user != nil && user.IsAdmin { return true } return false }, })) ``` ### Custom Error Handler ```go theme={null} app.Use(ratelimit.WithOptions(ratelimit.Options{ Rate: 100, Interval: time.Minute, ErrorHandler: func(c *mizu.Ctx) error { return c.JSON(429, map[string]any{ "error": "Rate limit exceeded", "retry_after": c.Header().Get("Retry-After"), }) }, })) ``` ### Disable Headers ```go theme={null} app.Use(ratelimit.WithOptions(ratelimit.Options{ Rate: 100, Interval: time.Minute, Headers: false, // Don't include rate limit headers })) ``` ### Different Limits for Different Routes ```go theme={null} // Global: 100 requests/minute app.Use(ratelimit.PerMinute(100)) // API: More restrictive api := app.Group("/api") api.Use(ratelimit.PerMinute(60)) // Expensive operations: Very restrictive app.Post("/api/export", exportHandler, ratelimit.PerMinute(5)) ``` ### Tiered Rate Limits ```go theme={null} func tierRateLimit() mizu.Middleware { free := ratelimit.WithOptions(ratelimit.Options{ Rate: 100, Interval: time.Hour, KeyFunc: func(c *mizu.Ctx) string { return "free:" + c.ClientIP() }, }) pro := ratelimit.WithOptions(ratelimit.Options{ Rate: 10000, Interval: time.Hour, KeyFunc: func(c *mizu.Ctx) string { return "pro:" + GetUser(c).ID }, }) return func(next mizu.Handler) mizu.Handler { return func(c *mizu.Ctx) error { user := GetUser(c) if user != nil && user.Plan == "pro" { return pro(next)(c) } return free(next)(c) } } } ``` ### Custom Store ```go theme={null} // Implement Store interface for Redis, etc. type redisStore struct { client *redis.Client } func (s *redisStore) Allow(key string, rate int, interval time.Duration, burst int) (bool, ratelimit.RateLimitInfo) { // Redis implementation } app.Use(ratelimit.WithStore(&redisStore{client}, ratelimit.Options{ Rate: 100, Interval: time.Minute, })) ``` ## Response Headers When `Headers` is enabled (default), these headers are included: | Header | Description | | ----------------------- | ---------------------------------- | | `X-RateLimit-Limit` | Maximum requests allowed | | `X-RateLimit-Remaining` | Remaining requests in window | | `X-RateLimit-Reset` | Unix timestamp when limit resets | | `Retry-After` | Seconds until retry (when limited) | ## API Reference ### Functions ```go theme={null} // Convenience constructors func PerSecond(n int) mizu.Middleware func PerMinute(n int) mizu.Middleware func PerHour(n int) mizu.Middleware // Custom rate and interval func New(rate int, interval time.Duration) mizu.Middleware // Full configuration func WithOptions(opts Options) mizu.Middleware // Custom store func WithStore(store Store, opts Options) mizu.Middleware ``` ### Store Interface ```go theme={null} type Store interface { Allow(key string, rate int, interval time.Duration, burst int) (bool, RateLimitInfo) } ``` ### RateLimitInfo ```go theme={null} type RateLimitInfo struct { Limit int Remaining int Reset time.Time } ``` ## Token Bucket Algorithm The middleware uses the token bucket algorithm: 1. **Bucket** starts full with `Burst` tokens 2. **Requests** consume one token 3. **Tokens** refill at `Rate` per `Interval` 4. **Burst** allows temporary spikes ``` Bucket: [████████████] 100 tokens Request → [███████████ ] 99 tokens Request → [██████████ ] 98 tokens ... Refill → [████████████] 100 tokens (after interval) ``` ## Technical Details The rate limit middleware implements the **token bucket algorithm**, a sophisticated approach for managing request rates with support for burst traffic. The implementation details are as follows: ### Token Bucket Implementation The algorithm maintains a bucket of tokens for each rate limit key: 1. **Initialization**: Each bucket starts with `Burst` tokens (default equals `Rate`) 2. **Token Consumption**: Each request consumes exactly one token from the bucket 3. **Token Refill**: Tokens are added continuously based on elapsed time: * Refill rate: `Rate / Interval` tokens per second * Formula: `tokensToAdd = (Rate * elapsedSeconds) / intervalSeconds` 4. **Capacity Limit**: Bucket capacity is capped at `Burst` tokens to prevent unlimited accumulation 5. **Request Decision**: Request is allowed if bucket has at least 1 token available ### In-Memory Store The default `MemoryStore` provides: * **Thread Safety**: Uses `sync.Mutex` for concurrent access protection * **Per-Key Tracking**: Maintains separate buckets for each key (IP, user ID, API key, etc.) * **Automatic Cleanup**: Background goroutine removes stale buckets after 10 minutes of inactivity * **Fractional Tokens**: Uses `float64` for precise token calculations during refills ### Rate Limit Information For each request, the middleware calculates and returns: * **Limit**: Maximum requests allowed per interval (from `Rate` option) * **Remaining**: Current token count (floored to nearest integer) * **Reset**: Timestamp when bucket will be full again (current time + `Interval`) ## Best Practices * Set reasonable limits based on expected usage * Use different limits for different endpoints * Include rate limit headers for client awareness * Monitor rate limit hits for tuning * Consider user tiers for fair access ## Testing The rate limit middleware includes comprehensive test coverage for all functionality: | Test Case | Description | Expected Behavior | | ------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------ | | `TestNew` | Basic rate limiting with IP-based keys | First N requests succeed, subsequent requests return 429; different IPs tracked separately | | `TestWithOptions_Headers` | Rate limit headers in response | Headers include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset | | `TestWithOptions_ErrorHandler` | Custom error handler for rate limit exceeded | Custom error handler called instead of default when limit exceeded | | `TestWithOptions_KeyFunc` | Custom key extraction function | Rate limits applied per custom key (e.g., API key) instead of IP | | `TestWithOptions_Skip` | Skip rate limiting for certain requests | Requests matching skip condition bypass rate limiting | | `TestWithOptions_RetryAfter` | Retry-After header on rate limit | Retry-After header included when request is rate limited | | `TestPerSecond` | PerSecond convenience function | Middleware created with per-second interval | | `TestPerMinute` | PerMinute convenience function | Middleware created with per-minute interval | | `TestPerHour` | PerHour convenience function | Middleware created with per-hour interval | | `TestMemoryStore_Allow` | Token bucket allows/denies requests | Allows up to limit, denies when bucket empty | | `TestMemoryStore_TokenRefill` | Token bucket refills over time | Tokens refill after interval allowing new requests | | `TestMemoryStore_DifferentKeys` | Different keys have independent buckets | Exhausting one key doesn't affect other keys | ## Related Middlewares * [circuitbreaker](/middlewares/circuitbreaker) - Circuit breaker pattern * [timeout](/middlewares/timeout) - Request timeout # RBAC Source: https://docs.go-mizu.dev/middlewares/rbac Role-based access control middleware for authorization. ## Overview The `rbac` middleware provides role-based access control, allowing you to restrict routes based on user roles and permissions. Use it when you need: * Role-based route protection * Permission-based access control * Multi-level authorization ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/rbac" ``` ## Quick Start ```go theme={null} app := mizu.New() // Get user role from context getRoles := func(c *mizu.Ctx) []string { user := c.Get("user").(User) return user.Roles } // Require admin role adminOnly := rbac.RequireRole(getRoles, "admin") app.Get("/admin", adminHandler, adminOnly) ``` ## Configuration ### Options | Option | Type | Description | | ---------------- | -------------------------- | --------------------------- | | `GetRoles` | `func(*mizu.Ctx) []string` | Function to get user roles | | `GetPermissions` | `func(*mizu.Ctx) []string` | Function to get permissions | | `ErrorHandler` | `func(*mizu.Ctx) error` | Custom 403 handler | ## Examples ### Simple Role Check ```go theme={null} getRoles := func(c *mizu.Ctx) []string { claims := jwt.GetClaims(c) return claims["roles"].([]string) } // Single role app.Get("/admin", handler, rbac.RequireRole(getRoles, "admin")) // Any of multiple roles app.Get("/dashboard", handler, rbac.RequireAnyRole(getRoles, "admin", "manager")) // All roles required app.Get("/super", handler, rbac.RequireAllRoles(getRoles, "admin", "superuser")) ``` ### Permission-Based ```go theme={null} getPerms := func(c *mizu.Ctx) []string { return getUserPermissions(c) } // Single permission app.Post("/users", createUser, rbac.RequirePermission(getPerms, "users:create")) // Multiple permissions app.Delete("/users/:id", deleteUser, rbac.RequireAllPermissions(getPerms, "users:delete", "admin:access")) ``` ### Hierarchical Roles ```go theme={null} hierarchy := rbac.NewHierarchy() hierarchy.Add("superadmin", "admin", "user") hierarchy.Add("admin", "moderator", "user") hierarchy.Add("moderator", "user") // superadmin and admin can access app.Get("/manage", handler, rbac.RequireRoleWithHierarchy(getRoles, hierarchy, "admin")) ``` ### Custom Error Handler ```go theme={null} app.Use(rbac.WithOptions(rbac.Options{ GetRoles: getRoles, ErrorHandler: func(c *mizu.Ctx) error { return c.JSON(403, map[string]string{ "error": "You don't have permission to access this resource", }) }, })) ``` ### Route Groups ```go theme={null} // Admin routes admin := app.Group("/admin") admin.Use(rbac.RequireRole(getRoles, "admin")) admin.Get("/users", listUsers) admin.Post("/users", createUser) // Manager routes manager := app.Group("/manager") manager.Use(rbac.RequireAnyRole(getRoles, "admin", "manager")) manager.Get("/reports", viewReports) ``` ## API Reference ### Functions ```go theme={null} // RequireRole requires a specific role func RequireRole(getRoles func(*mizu.Ctx) []string, role string) mizu.Middleware // RequireAnyRole requires any of the specified roles func RequireAnyRole(getRoles func(*mizu.Ctx) []string, roles ...string) mizu.Middleware // RequireAllRoles requires all specified roles func RequireAllRoles(getRoles func(*mizu.Ctx) []string, roles ...string) mizu.Middleware // RequirePermission requires a specific permission func RequirePermission(getPerms func(*mizu.Ctx) []string, perm string) mizu.Middleware // RequireAllPermissions requires all permissions func RequireAllPermissions(getPerms func(*mizu.Ctx) []string, perms ...string) mizu.Middleware // WithOptions creates middleware with configuration func WithOptions(opts Options) mizu.Middleware ``` ### Hierarchy ```go theme={null} // NewHierarchy creates role hierarchy func NewHierarchy() *Hierarchy // Add defines parent-child relationships func (h *Hierarchy) Add(parent string, children ...string) // RequireRoleWithHierarchy checks against hierarchy func RequireRoleWithHierarchy(getRoles func(*mizu.Ctx) []string, h *Hierarchy, role string) mizu.Middleware ``` ## Technical Details ### Implementation Overview The RBAC middleware is built around a User struct that contains roles and permissions: ```go theme={null} type User struct { ID string Roles []string Permissions []string } ``` ### Context Management User information is stored in the request context using a private context key: * `Set(c *mizu.Ctx, user *User)` - Stores user in context * `Get(c *mizu.Ctx) *User` - Retrieves user from context * Returns nil if no user is found in context ### Role and Permission Checking The middleware provides several helper functions for checking access: * `HasRole(c *mizu.Ctx, role string) bool` - Checks if user has a specific role * `HasAnyRole(c *mizu.Ctx, roles ...string) bool` - Checks if user has any of the specified roles * `HasAllRoles(c *mizu.Ctx, roles ...string) bool` - Checks if user has all specified roles * `HasPermission(c *mizu.Ctx, permission string) bool` - Checks if user has a specific permission ### Middleware Functions Role-based middlewares: * `RequireRole(role string)` - Requires a specific role * `RequireAnyRole(roles ...string)` - Requires any of the specified roles (OR logic) * `RequireAllRoles(roles ...string)` - Requires all specified roles (AND logic) Permission-based middlewares: * `RequirePermission(permission string)` - Requires a specific permission * `RequireAnyPermission(permissions ...string)` - Requires any of the permissions * `RequireAllPermissions(permissions ...string)` - Requires all permissions Convenience middlewares: * `Admin()` - Shorthand for RequireRole("admin") * `Authenticated()` - Checks if any user is present in context ### Error Handling * Default behavior returns HTTP 403 Forbidden with "Access denied" text * Returns HTTP 401 Unauthorized with "Authentication required" for unauthenticated users * Custom error handlers can be wrapped using `WithErrorHandler` ## Best Practices * Use JWT claims or session for role storage * Implement role hierarchy for complex permissions * Cache permission checks for performance * Log authorization failures for security monitoring ## Testing | Test Case | Description | Expected Behavior | | ----------------------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------ | | TestRequireRole | User with required role accessing protected route | Returns 200 OK, access granted | | TestRequireRole\_Forbidden | User without required role accessing protected route | Returns 403 Forbidden, access denied | | TestRequireAnyRole | User with one of multiple required roles | Returns 200 OK, access granted when user has editor role | | TestRequireAllRoles (has all) | User with all required roles | Returns 200 OK, access granted when user has both admin and editor roles | | TestRequireAllRoles (missing one) | User missing one of required roles | Returns 403 Forbidden, access denied when user has admin but not editor | | TestRequirePermission | User with required permission accessing resource | Returns 200 OK, access granted with write:posts permission | | TestAdmin | User with admin role accessing admin route | Returns 200 OK, access granted | | TestAuthenticated (authenticated) | Authenticated user accessing protected route | Returns 200 OK, access granted | | TestAuthenticated (unauthenticated) | Unauthenticated user accessing protected route | Returns 401 Unauthorized, authentication required | | TestHasRole | Checking if user has specific roles | Returns true for user role, false for admin role | | TestGet | Retrieving user from context | Returns user object with correct ID | | TestNoUser | Checking role without user in context | Returns false, no access without user | ## Related Middlewares * [jwt](/middlewares/jwt) - JWT authentication * [basicauth](/middlewares/basicauth) - Basic authentication * [keyauth](/middlewares/keyauth) - API key authentication # Real IP Source: https://docs.go-mizu.dev/middlewares/realip Real client IP extraction middleware for proxy environments. ## Overview The `realip` middleware extracts the real client IP address from proxy headers. It's essential when running behind load balancers or reverse proxies. ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/realip" ``` ## Quick Start ```go theme={null} app := mizu.New() app.Use(realip.New()) ``` ## Configuration | Option | Type | Default | Description | | ---------------- | ---------- | --------- | ----------------------- | | `TrustedProxies` | `[]string` | - | Trusted proxy IPs/CIDRs | | `TrustedHeaders` | `[]string` | See below | Headers to check | ### Default Headers (checked in order) 1. `X-Forwarded-For` 2. `X-Real-IP` 3. `CF-Connecting-IP` (Cloudflare) 4. `True-Client-IP` ## Examples ### Basic Usage ```go theme={null} app.Use(realip.New()) app.Get("/", func(c *mizu.Ctx) error { ip := realip.Get(c) return c.Text(200, "Your IP: "+ip) }) ``` ### Trusted Proxies ```go theme={null} app.Use(realip.WithTrustedProxies( "10.0.0.0/8", "192.168.0.0/16", )) ``` ### Custom Configuration ```go theme={null} app.Use(realip.WithOptions(realip.Options{ TrustedProxies: []string{"10.0.0.0/8"}, TrustedHeaders: []string{ "X-Real-IP", "X-Forwarded-For", }, })) ``` ## API Reference ```go theme={null} func New() mizu.Middleware func WithTrustedProxies(proxies ...string) mizu.Middleware func WithOptions(opts Options) mizu.Middleware func FromContext(c *mizu.Ctx) string func Get(c *mizu.Ctx) string // Alias for FromContext ``` ## Technical Details ### IP Extraction Process The middleware follows a systematic approach to extract the real client IP: 1. **Remote IP Extraction**: Extracts the IP from `Request.RemoteAddr` using `net.SplitHostPort` 2. **Proxy Trust Verification**: Checks if the remote IP is in the trusted proxy list (CIDR notation supported) 3. **Header Processing**: If trusted, iterates through configured headers in priority order 4. **IP Parsing**: Extracts the first valid IP from comma-separated header values (for `X-Forwarded-For`) 5. **Fallback**: Uses `RemoteAddr` if no valid IP found in headers ### CIDR Network Parsing The middleware automatically converts single IP addresses to CIDR notation: * IPv4 addresses: `/32` suffix (e.g., `10.0.0.1` becomes `10.0.0.1/32`) * IPv6 addresses: `/128` suffix (e.g., `::1` becomes `::1/128`) ### Context Storage The real IP is stored in the request context using a private `contextKey{}` type, preventing key collisions with other middleware or application code. ### Header Priority When multiple headers are set, the middleware checks them in the configured order and uses the first valid IP found. Default priority: 1. X-Forwarded-For (supports comma-separated lists) 2. X-Real-IP 3. CF-Connecting-IP (Cloudflare) 4. True-Client-IP ## Security Only trust proxy headers when behind a trusted proxy. Without `TrustedProxies`, all requests are trusted. ## Testing The middleware includes comprehensive test coverage for various scenarios: | Test Case | Description | Expected Behavior | | --------------------------------------- | -------------------------------------------------- | ----------------------------------------------------------- | | **Default Header Tests** | | | | `uses X-Forwarded-For` | Multiple IPs in X-Forwarded-For header | Extracts first IP (203.0.113.195) from comma-separated list | | `uses X-Real-IP` | Single IP in X-Real-IP header | Returns the IP from X-Real-IP (198.51.100.178) | | `uses CF-Connecting-IP` | Cloudflare connecting IP header | Returns the IP from CF-Connecting-IP (192.0.2.1) | | `falls back to RemoteAddr` | No proxy headers present | Uses RemoteAddr (192.168.1.100) | | **Trusted Proxy Tests** | | | | `trusts headers from trusted proxy` | Request from 10.0.0.1 with X-Forwarded-For | Returns IP from header (203.0.113.195) | | `ignores headers from untrusted source` | Request from 192.168.1.1 (not in trusted range) | Ignores header, returns RemoteAddr (192.168.1.1) | | **Single IP Trust** | | | | `WithTrustedProxies single IP` | Trust specific IP (10.0.0.1) | Accepts headers from exact IP match | | **Custom Headers** | | | | `WithOptions custom headers` | Configure X-Custom-IP as trusted header | Extracts IP from custom header (5.6.7.8) | | **API Compatibility** | | | | `Get() alias test` | FromContext vs Get methods | Both return identical IP values | | **Helper Function Tests** | | | | `extractFirstIP - single IP` | Parse "203.0.113.195" | Returns 203.0.113.195 | | `extractFirstIP - comma-separated` | Parse "203.0.113.195, 70.41.3.18, 150.172.238.178" | Returns first IP (203.0.113.195) | | `extractFirstIP - whitespace` | Parse " 203.0.113.195 " | Trims and returns 203.0.113.195 | | `extractFirstIP - invalid` | Parse "invalid" | Returns empty string | | `extractFirstIP - empty` | Parse "" | Returns empty string | | `extractFirstIP - IPv6` | Parse "2001:db8::1" | Returns IPv6 address | | `extractIP - with port` | Parse "192.168.1.1:8080" | Returns IP without port (192.168.1.1) | | `extractIP - without port` | Parse "192.168.1.1" | Returns IP as-is (192.168.1.1) | | `extractIP - IPv6 with port` | Parse "\[::1]:8080" | Returns IPv6 without port (::1) | | `isTrusted - within CIDR` | Check 10.0.0.1 in 10.0.0.0/8 | Returns true | | `isTrusted - edge of CIDR` | Check 10.255.255.255 in 10.0.0.0/8 | Returns true | | `isTrusted - outside CIDR` | Check 11.0.0.1 in 10.0.0.0/8 | Returns false | | `isTrusted - multiple networks` | Check 192.168.1.1 in \[10.0.0.0/8, 192.168.0.0/16] | Returns true (matches second network) | ## Related Middlewares * [forwarded](/middlewares/forwarded) - Full forwarded header parsing * [ipfilter](/middlewares/ipfilter) - IP filtering # Recover Source: https://docs.go-mizu.dev/middlewares/recover Panic recovery middleware for graceful error handling. ## Overview The `recover` middleware catches panics in handlers and converts them to proper error responses. It prevents your server from crashing and logs stack traces for debugging. ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/recover" ``` ## Quick Start ```go theme={null} app := mizu.New() // Add as first middleware app.Use(recover.New()) ``` ## Configuration | Option | Type | Default | Description | | ------------------- | ------------------------------------ | ------------ | -------------------------- | | `StackSize` | `int` | `4096` | Stack trace buffer size | | `DisableStackAll` | `bool` | `false` | Limit to current goroutine | | `DisablePrintStack` | `bool` | `false` | Disable stack logging | | `ErrorHandler` | `func(*mizu.Ctx, any, []byte) error` | - | Custom panic handler | | `Logger` | `*slog.Logger` | `c.Logger()` | Custom logger | ## Examples ### Basic Recovery ```go theme={null} app.Use(recover.New()) app.Get("/", func(c *mizu.Ctx) error { panic("something went wrong") // Caught by recover }) // Returns 500 Internal Server Error ``` ### Custom Error Handler ```go theme={null} app.Use(recover.WithOptions(recover.Options{ ErrorHandler: func(c *mizu.Ctx, err any, stack []byte) error { // Log to error tracking service errorTracker.Report(err, string(stack)) return c.JSON(500, map[string]string{ "error": "Internal server error", }) }, })) ``` ### Disable Stack Trace ```go theme={null} // Production: don't log stack traces app.Use(recover.WithOptions(recover.Options{ DisablePrintStack: true, })) ``` ### Custom Logger ```go theme={null} logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) app.Use(recover.WithOptions(recover.Options{ Logger: logger, })) ``` ### With Request ID ```go theme={null} app.Use(requestid.New()) app.Use(recover.WithOptions(recover.Options{ ErrorHandler: func(c *mizu.Ctx, err any, stack []byte) error { c.Logger().Error("panic recovered", "error", err, "request_id", requestid.Get(c), "stack", string(stack), ) return c.Text(500, "Internal Server Error") }, })) ``` ## API Reference ```go theme={null} func New() mizu.Middleware func WithOptions(opts Options) mizu.Middleware ``` ## Log Output ```json theme={null} { "level": "ERROR", "msg": "panic recovered", "error": "runtime error: index out of range", "stack": "goroutine 1 [running]:\nmain.handler..." } ``` ## Technical Details The recover middleware uses Go's built-in `defer` and `recover()` mechanisms to catch panics in handlers. Here's how it works internally: ### Implementation Architecture 1. **Panic Recovery**: A deferred function wraps the next handler, catching any panic that occurs during request processing 2. **Stack Trace Capture**: Uses `runtime/debug.Stack()` to capture the full stack trace when a panic occurs 3. **Stack Size Management**: Truncates stack traces to the configured `StackSize` (default 4096 bytes) to prevent memory issues 4. **Logging Integration**: Integrates with Go's `log/slog` package for structured logging of panic information 5. **Error Propagation**: Converts recovered panics into proper HTTP error responses ### Key Implementation Details * The middleware returns a closure that wraps the next handler with a deferred recovery function * When `DisablePrintStack` is false, `debug.Stack()` captures the current goroutine's stack trace * The stack trace is truncated if it exceeds `StackSize` to prevent excessive memory usage * If a custom `ErrorHandler` is provided, it receives the panic value and stack trace for custom handling * Default behavior returns a 500 Internal Server Error with the standard HTTP status text * The logger defaults to `c.Logger()` if no custom logger is specified ### Performance Considerations * Minimal overhead when no panic occurs (just a defer statement) * Stack trace capture only happens during panics * Configurable stack size prevents unbounded memory allocation * Option to disable stack printing for production environments ## Best Practices * Always add as the first middleware * Log panics for debugging * Don't expose stack traces to users in production * Use with request ID for correlation ## Testing The recover middleware includes comprehensive test coverage for all features and edge cases: | Test Case | Description | Expected Behavior | | ---------------------------------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------- | | `TestNew/recovers from panic` | Handler that panics with "test panic" | Returns 500 Internal Server Error status | | `TestNew/passes through normal requests` | Handler that returns normally without panic | Returns 200 OK with response body "ok" | | `TestWithOptions_ErrorHandler` | Custom error handler captures panic and stack | Returns 503 Service Unavailable with "custom error", captures panic value and stack trace | | `TestWithOptions_DisablePrintStack` | DisablePrintStack option enabled | Returns 500 status, no stack trace in log output | | `TestWithOptions_CustomLogger` | Custom slog.Logger provided | Panic is logged with "panic recovered" message and panic details | | `TestWithOptions_StackSize` | StackSize set to 100 bytes | Captured stack trace is truncated to maximum 100 bytes | ## Related Middlewares * [requestid](/middlewares/requestid) - Request tracing * [timeout](/middlewares/timeout) - Request timeout # Redirect Source: https://docs.go-mizu.dev/middlewares/redirect URL redirection middleware for HTTPS, WWW, and custom redirects. ## Overview The `redirect` middleware provides URL redirection capabilities including HTTPS enforcement, WWW handling, and custom redirect rules. ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/redirect" ``` ## Quick Start ```go theme={null} app := mizu.New() // Redirect HTTP to HTTPS app.Use(redirect.HTTPSRedirect()) ``` ## Functions | Function | Description | | ------------------------- | ---------------------------- | | `HTTPSRedirect()` | Redirect HTTP to HTTPS (301) | | `HTTPSRedirectCode(code)` | Redirect with custom code | | `WWWRedirect()` | Redirect to www subdomain | | `NonWWWRedirect()` | Remove www subdomain | | `TrailingSlashRedirect()` | Add trailing slash | | `New(rules)` | Custom redirect rules | ## Examples ### HTTPS Redirect ```go theme={null} // Permanent redirect to HTTPS app.Use(redirect.HTTPSRedirect()) // Temporary redirect (307) app.Use(redirect.HTTPSRedirectCode(307)) ``` ### WWW Handling ```go theme={null} // Add www prefix app.Use(redirect.WWWRedirect()) // Remove www prefix app.Use(redirect.NonWWWRedirect()) ``` ### Trailing Slash ```go theme={null} // Add trailing slash: /about → /about/ app.Use(redirect.TrailingSlashRedirect()) ``` ### Custom Redirect Rules ```go theme={null} app.Use(redirect.New([]redirect.Rule{ {From: "/old-page", To: "/new-page", Code: 301}, {From: "/blog", To: "/articles", Code: 302}, })) ``` ### Regex Redirects ```go theme={null} app.Use(redirect.New([]redirect.Rule{ { From: `^/posts/(\d+)$`, To: "/articles/$1", Regex: true, Code: 301, }, })) ``` ### Combined Redirects ```go theme={null} app.Use(redirect.HTTPSRedirect()) app.Use(redirect.NonWWWRedirect()) ``` ## API Reference ```go theme={null} func HTTPSRedirect() mizu.Middleware func HTTPSRedirectCode(code int) mizu.Middleware func WWWRedirect() mizu.Middleware func WWWRedirectCode(code int) mizu.Middleware func NonWWWRedirect() mizu.Middleware func NonWWWRedirectCode(code int) mizu.Middleware func TrailingSlashRedirect() mizu.Middleware func TrailingSlashRedirectCode(code int) mizu.Middleware func New(rules []Rule) mizu.Middleware ``` ### Rule Type ```go theme={null} type Rule struct { From string // Source path or regex To string // Target URL ($1, $2 for captures) Code int // HTTP status code (default: 301) Regex bool // Is From a regex pattern? } ``` ## Status Codes | Code | Type | Use Case | | ---- | --------- | -------------------- | | 301 | Permanent | SEO, permanent moves | | 302 | Found | Temporary redirect | | 307 | Temporary | Preserve method | | 308 | Permanent | Preserve method | ## Technical Details ### Implementation Overview The redirect middleware provides several specialized redirect handlers, each implemented as a middleware function that returns a handler chain: #### HTTPS Redirect * Checks if the request is non-HTTPS by examining `c.Request().TLS` and the `X-Forwarded-Proto` header * Constructs the HTTPS URL using the request's host and request URI * Returns redirect response with the specified status code (default: 301) #### WWW Domain Redirect * **WWWRedirect**: Checks if the host lacks the "[www](http://www)." prefix and adds it * **NonWWWRedirect**: Checks if the host has the "[www](http://www)." prefix and removes it * Preserves the original protocol (HTTP/HTTPS) by checking TLS and `X-Forwarded-Proto` * Constructs the redirect URL with the modified host #### Trailing Slash Redirect * Examines the request path to see if it lacks a trailing slash * Skips the root path ("/") to avoid unnecessary redirects * Preserves query strings when redirecting * Adds "/" suffix to the path before redirecting #### Custom Rule-Based Redirect The `New()` function processes redirect rules with the following behavior: * Compiles regex patterns at initialization time for performance * Sets default status code (301) if not specified * For each request, iterates through rules in order: * **Regex rules**: Uses `FindStringSubmatch` to capture groups, replaces `$0`, `$1`, `$2`, etc. in the target URL * **Exact match rules**: Performs simple string comparison on the path * Preserves query strings by appending `RawQuery` to the target URL * Returns the first matching rule's redirect ### Performance Considerations * Regex patterns are compiled once during middleware initialization, not on each request * Rules are evaluated in order; place more frequently matched rules first * HTTPS and WWW redirects perform minimal string operations * Query string preservation adds negligible overhead ## Best Practices 1. **Order Matters**: Apply HTTPS redirect before WWW/non-WWW redirects 2. **Status Codes**: Use 301 for permanent SEO-friendly redirects, 307/308 to preserve HTTP methods 3. **Regex Efficiency**: Keep regex patterns simple; complex patterns impact performance 4. **Rule Organization**: Place frequently matched rules at the beginning of the rules array 5. **Testing**: Always test redirects with query strings and various HTTP methods ## Testing The redirect middleware includes comprehensive test coverage for all redirect scenarios: | Test Case | Description | Expected Behavior | | --------------------------------------------------------- | -------------------------------------------------------------- | --------------------------------------------------------------------------------- | | `TestHTTPSRedirect - redirects HTTP to HTTPS` | HTTP request to example.com/test | Redirects to [https://example.com/test](https://example.com/test) with 301 status | | `TestHTTPSRedirect - allows HTTPS` | HTTPS request with X-Forwarded-Proto header | Passes through with 200 status (no redirect) | | `TestHTTPSRedirectCode` | HTTP request with custom redirect code (307) | Redirects with 307 Temporary Redirect status | | `TestWWWRedirect - redirects to www` | Request to example.com/test | Redirects to [www.example.com/test](http://www.example.com/test) with 301 status | | `TestWWWRedirect - allows www` | Request to [www.example.com/test](http://www.example.com/test) | Passes through with 200 status (no redirect) | | `TestNonWWWRedirect - redirects from www` | Request to [www.example.com/test](http://www.example.com/test) | Redirects to example.com/test with 301 status | | `TestNonWWWRedirect - allows non-www` | Request to example.com/test | Passes through with 200 status (no redirect) | | `TestNew - redirects matching rule` | Request to /old with rule `{From: "/old", To: "/new"}` | Redirects to /new with 301 status | | `TestNew - preserves query string` | Request to /old?foo=bar | Redirects to /new?foo=bar with query preserved | | `TestNew_Regex - regex redirect with capture` | Request to /users/123 with pattern /users/(\d+) | Redirects to /profile/123 with captured group | | `TestTrailingSlashRedirect - adds trailing slash` | Request to /test without trailing slash | Redirects to /test/ with 301 status | | `TestTrailingSlashRedirect - preserves query on redirect` | Request to /test?a=1 | Redirects to /test/?a=1 with query preserved | | `TestTrailingSlashRedirect - skips root path` | Request to / (root path) | Passes through with 200 status (no redirect) | ## Related Middlewares * [slash](/middlewares/slash) - Trailing slash handling * [rewrite](/middlewares/rewrite) - URL rewriting (no redirect) * [secure](/middlewares/secure) - HTTPS with security headers # Request ID Source: https://docs.go-mizu.dev/middlewares/requestid Request ID generation and propagation middleware for tracing. ## Overview The `requestid` middleware generates or propagates unique request IDs for distributed tracing and debugging. Each request gets a unique identifier that can be logged and passed to downstream services. ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/requestid" ``` ## Quick Start ```go theme={null} app := mizu.New() app.Use(requestid.New()) ``` ## Configuration | Option | Type | Default | Description | | ----------- | --------------- | ---------------- | --------------------- | | `Header` | `string` | `"X-Request-ID"` | Header name | | `Generator` | `func() string` | UUID v4 | ID generator function | ## Examples ### Basic Usage ```go theme={null} app.Use(requestid.New()) app.Get("/", func(c *mizu.Ctx) error { id := requestid.Get(c) c.Logger().Info("processing", "request_id", id) return c.JSON(200, map[string]string{"id": id}) }) ``` ### Custom Header ```go theme={null} app.Use(requestid.WithOptions(requestid.Options{ Header: "X-Trace-ID", })) ``` ### Custom Generator ```go theme={null} var counter uint64 app.Use(requestid.WithOptions(requestid.Options{ Generator: func() string { return fmt.Sprintf("req-%d-%d", time.Now().Unix(), atomic.AddUint64(&counter, 1)) }, })) ``` ### ID Propagation If the incoming request has a request ID header, it's preserved: ``` Client → X-Request-ID: abc123 → Server → Response X-Request-ID: abc123 ``` If not, a new one is generated: ``` Client → (no header) → Server generates: xyz789 → Response X-Request-ID: xyz789 ``` ### Logging with Request ID ```go theme={null} app.Use(requestid.New()) app.Get("/", func(c *mizu.Ctx) error { logger := c.Logger().With("request_id", requestid.Get(c)) logger.Info("handling request") // ... return c.JSON(200, data) }) ``` ### Pass to Downstream Services ```go theme={null} func callService(c *mizu.Ctx) error { req, _ := http.NewRequest("GET", "http://service/api", nil) req.Header.Set("X-Request-ID", requestid.Get(c)) // ... } ``` ## API Reference ```go theme={null} func New() mizu.Middleware func WithOptions(opts Options) mizu.Middleware func FromContext(c *mizu.Ctx) string func Get(c *mizu.Ctx) string // Alias for FromContext ``` ## Technical Details ### Implementation The requestid middleware is implemented with the following key components: * **Context Storage**: Request IDs are stored in the request context using a private `contextKey{}` struct type to prevent collisions * **ID Generation**: Default generator creates UUID v4-style identifiers using crypto/rand for cryptographic randomness * **Header Propagation**: The middleware checks for existing request IDs in incoming headers and preserves them, or generates new ones if absent * **Response Headers**: Request IDs are automatically added to response headers for client visibility ### UUID v4 Format The default generator produces IDs in the format: `xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx` * Uses 16 random bytes from `crypto/rand` * Version bits (byte 6): Set to `0x40` to indicate version 4 * Variant bits (byte 8): Set to `0x80` to indicate RFC 4122 variant 2 * Final format: 32 hexadecimal characters in 5 groups (8-4-4-4-12) ### Context Key Design The middleware uses a private struct type as the context key: ```go theme={null} type contextKey struct{} ``` This prevents accidental key collisions with other middleware or application code that might use string or integer keys. ### Flow 1. Extract request ID from incoming header (if present) 2. If no header, generate new ID using configured generator 3. Store ID in request context with private key 4. Set ID in response header 5. Continue to next handler ## Best Practices * Add early in middleware chain * Include in all logs * Pass to downstream services * Use for error correlation ## Testing The middleware includes comprehensive test coverage for all functionality: | Test Case | Description | Expected Behavior | | ---------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | `TestNew/generates request ID` | Tests default middleware behavior when no request ID is provided | Generates a new UUID v4-style request ID, stores it in context, and sets it in response header | | `TestNew/uses existing request ID` | Tests ID propagation when client provides an existing request ID | Preserves the existing request ID from request header, stores it in context, and returns it in response header | | `TestWithOptions_CustomHeader` | Tests custom header name configuration | Uses custom header name (e.g., "X-Correlation-ID") instead of default "X-Request-ID" | | `TestWithOptions_CustomGenerator` | Tests custom ID generator function | Uses provided generator function to create sequential custom IDs instead of UUID v4 format | | `TestGenerateID` | Tests default UUID v4 generator format and uniqueness | Generates IDs in correct UUID v4 format (8-4-4-4-12), sets version 4 bit, and ensures uniqueness across 1000 generations | | `TestGet` | Tests that Get() and FromContext() are equivalent | Both functions return the same request ID value from context | ## Related Middlewares * [timing](/middlewares/timing) - Performance tracing * [recover](/middlewares/recover) - Panic recovery with context # Request Log Source: https://docs.go-mizu.dev/middlewares/requestlog Detailed request logging middleware with structured output. ## Overview The `requestlog` middleware provides detailed structured logging of HTTP requests with configurable fields and formats. Use it when you need: * Structured request logs * Detailed debugging info * Log aggregation integration ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/requestlog" ``` ## Quick Start ```go theme={null} app := mizu.New() app.Use(requestlog.New()) ``` ## Configuration ### Options | Option | Type | Default | Description | | -------- | ---------------------- | -------- | ----------------- | | `Logger` | `*slog.Logger` | Default | Structured logger | | `Level` | `slog.Level` | Info | Log level | | `Fields` | `[]string` | Standard | Fields to log | | `Skip` | `func(*mizu.Ctx) bool` | `nil` | Skip logging | ## Examples ### Default Logging ```go theme={null} app.Use(requestlog.New()) ``` ### With slog ```go theme={null} logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) app.Use(requestlog.WithLogger(logger)) ``` ### Custom Fields ```go theme={null} app.Use(requestlog.WithOptions(requestlog.Options{ Fields: []string{ "method", "path", "status", "latency", "ip", "user_agent", }, })) ``` ### Skip Health Checks ```go theme={null} app.Use(requestlog.WithOptions(requestlog.Options{ Skip: func(c *mizu.Ctx) bool { return c.Request().URL.Path == "/health" }, })) ``` ### Error Level for Errors ```go theme={null} app.Use(requestlog.WithOptions(requestlog.Options{ LevelFunc: func(status int) slog.Level { if status >= 500 { return slog.LevelError } if status >= 400 { return slog.LevelWarn } return slog.LevelInfo }, })) ``` ## Log Output ```json theme={null} { "time": "2024-01-15T10:30:00Z", "level": "INFO", "msg": "request", "method": "GET", "path": "/api/users", "status": 200, "latency": "1.234ms", "ip": "192.168.1.1", "user_agent": "Mozilla/5.0..." } ``` ## API Reference ### Functions ```go theme={null} // New creates request log middleware func New() mizu.Middleware // WithLogger creates with custom logger func WithLogger(logger *slog.Logger) mizu.Middleware // WithOptions creates with configuration func WithOptions(opts Options) mizu.Middleware ``` ## Technical Details ### Implementation The requestlog middleware is built on Go's `log/slog` package and provides structured logging with the following key features: * **Timing**: Uses `time.Now()` and `time.Since()` to measure request duration with nanosecond precision * **Skip Logic**: Pre-computes skip paths and methods into maps for O(1) lookup performance * **Body Preservation**: Reads request body using `io.LimitReader` and restores it with `io.NopCloser(bytes.NewReader(body))` to allow downstream handlers to access it * **Header Redaction**: Maintains a map of sensitive header names for constant-time redaction checks * **Default Values**: Automatically sets default logger (text handler to stdout), max body size (4KB), and sensitive headers (Authorization, Cookie, X-API-Key) ### Logged Fields The middleware logs the following fields for each request: * `method`: HTTP method (GET, POST, etc.) * `path`: Request URL path * `remote_addr`: Client remote address * `query`: Raw query string (only if present) * `headers`: Request headers (only if `LogHeaders: true`) * `body`: Request body up to `MaxBodySize` (only if `LogBody: true`) * `duration`: Request processing duration * `error`: Error message (only if request returns an error) ### Error Handling When a request handler returns an error: * The error is logged at ERROR level with all request attributes * The error is propagated to the next middleware (not swallowed) * Request duration is still calculated and logged ## Security Considerations ### Sensitive Data Protection The middleware automatically redacts sensitive headers to prevent credential leakage: * Default sensitive headers: `Authorization`, `Cookie`, `X-API-Key` * Custom sensitive headers can be configured via `SensitiveHeaders` option * Redacted headers show `[REDACTED]` in logs ### Body Logging Risks When `LogBody: true`: * Request bodies may contain passwords, tokens, or PII * Body size is limited by `MaxBodySize` (default 4KB) to prevent memory exhaustion * Consider using skip logic for authentication endpoints * Review compliance requirements (GDPR, HIPAA, etc.) before enabling ## Best Practices * Use structured logging for aggregation * Skip noisy endpoints * Include request IDs for correlation * Set appropriate log levels * Be cautious with `LogBody` on production systems * Configure `SensitiveHeaders` for your application's needs * Use `SkipPaths` for health checks and metrics endpoints * Set reasonable `MaxBodySize` limits to prevent memory issues ## Testing The requestlog middleware includes comprehensive test coverage: | Test Case | Description | Expected Behavior | | ---------------------------------- | ----------------------------------------------- | ----------------------------------------------------------------------- | | `TestNew` | Tests basic middleware creation and logging | Logs method and path for standard requests | | `TestWithOptions_LogHeaders` | Tests header logging functionality | Includes headers in log output when `LogHeaders: true` | | `TestWithOptions_LogBody` | Tests body logging functionality | Includes request body in log output when `LogBody: true` | | `TestWithOptions_SkipPaths` | Tests path-based skip logic | Skips logging for configured paths (e.g., `/health`) | | `TestWithOptions_SkipMethods` | Tests method-based skip logic | Skips logging for configured HTTP methods (e.g., `OPTIONS`) | | `TestWithOptions_SensitiveHeaders` | Tests header redaction | Redacts sensitive headers showing `[REDACTED]` instead of actual values | | `TestFull` | Tests full logging mode | Logs both headers and body when both options are enabled | | `TestQueryParams` | Tests query parameter logging | Includes query parameters in log output when present | | `TestBodyPreserved` | Tests body preservation for downstream handlers | Ensures request body remains readable by handlers after logging | ## Related Middlewares * [logger](/middlewares/logger) - Simple logging * [audit](/middlewares/audit) - Audit logging * [requestid](/middlewares/requestid) - Request IDs # Request Size Source: https://docs.go-mizu.dev/middlewares/requestsize Request size tracking middleware for monitoring and metrics. ## Overview The `requestsize` middleware tracks request sizes for monitoring, logging, and metrics collection. Use it when you need: * Monitor request sizes * Collect bandwidth metrics * Analyze traffic patterns ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/requestsize" ``` ## Quick Start ```go theme={null} app := mizu.New() app.Use(requestsize.New()) app.Post("/upload", func(c *mizu.Ctx) error { size := requestsize.Get(c) log.Printf("Request size: %d bytes", size) return c.Text(200, "OK") }) ``` ## Configuration ### Options | Option | Type | Default | Description | | --------- | ------------------------ | ------- | ------------- | | `Handler` | `func(*mizu.Ctx, int64)` | `nil` | Size callback | ## Examples ### Track in Context ```go theme={null} app.Use(requestsize.New()) app.Post("/api", func(c *mizu.Ctx) error { size := requestsize.Get(c) // Use size for logging, metrics, etc. return c.JSON(200, map[string]int64{"received": size}) }) ``` ### With Callback ```go theme={null} app.Use(requestsize.WithHandler(func(c *mizu.Ctx, size int64) { metrics.RecordRequestSize(c.Request().URL.Path, size) })) ``` ### Combined with Response Size ```go theme={null} app.Use(requestsize.New()) app.Use(responsesize.New()) app.Post("/api", func(c *mizu.Ctx) error { reqSize := requestsize.Get(c) // Response size available after response return c.JSON(200, data) }) ``` ## API Reference ### Functions ```go theme={null} // New creates request size middleware func New() mizu.Middleware // WithHandler creates with callback func WithHandler(handler func(*mizu.Ctx, int64)) mizu.Middleware // Get returns request size from context func Get(c *mizu.Ctx) int64 ``` ## Technical Details The requestsize middleware tracks request sizes by wrapping the request body with a custom `io.ReadCloser` implementation: ### Architecture * **Context Storage**: Size information is stored in the request context using a private context key * **Body Wrapping**: The middleware wraps `http.Request.Body` with a `trackingBody` struct that counts bytes as they're read * **Deferred Callback**: Size callbacks are executed via `defer` to ensure they run after the request body is processed ### Implementation Details 1. **Info Structure**: Contains two fields: * `ContentLength`: The `Content-Length` header value (may be -1 if not set) * `BytesRead`: Actual bytes read from the request body during processing 2. **Tracking Mechanism**: The `trackingBody` wrapper intercepts all `Read()` calls and accumulates the byte count 3. **Context Integration**: Size info is attached to the request context and can be retrieved using the `Get()`, `ContentLength()`, or `BytesRead()` helper functions 4. **Callback Execution**: If configured, the `OnSize` callback is invoked after request processing completes, providing access to final byte counts ## Best Practices * Use for bandwidth monitoring * Track alongside response sizes * Set alerts for unusually large requests * Combine with body limit for enforcement ## Testing The middleware includes comprehensive test coverage for various scenarios: | Test Case | Description | Expected Behavior | | -------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------- | | `TestNew` | Basic middleware initialization and usage | Correctly captures Content-Length header (9 bytes) | | `TestWithOptions_Callback` | Callback function invocation with options | Callback is called with accurate BytesRead count (5 bytes) | | `TestContentLength` | ContentLength helper function | Returns correct Content-Length value from context (12 bytes for "test content") | | `TestBytesRead` | BytesRead tracking for partial reads | Accurately tracks partial body reads (3 bytes out of "hello world") | | `TestWithCallback` | WithCallback convenience function | Callback is invoked during request processing | | `TestNoBody` | Requests without body (GET requests) | Handles nil body gracefully with 0 content length | ## Related Middlewares * [responsesize](/middlewares/responsesize) - Response size tracking * [bodylimit](/middlewares/bodylimit) - Size enforcement * [metrics](/middlewares/metrics) - Metrics collection # Response Log Source: https://docs.go-mizu.dev/middlewares/responselog Response logging middleware for debugging and monitoring. ## Overview The `responselog` middleware logs HTTP response details including status codes, response times, and response sizes. Use it when you need: * Response monitoring * Debugging response issues * Performance tracking ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/responselog" ``` ## Quick Start ```go theme={null} app := mizu.New() app.Use(responselog.New()) ``` ## Configuration ### Options | Option | Type | Default | Description | | ------------- | -------------- | ------- | ----------------- | | `Logger` | `*slog.Logger` | Default | Logger instance | | `IncludeBody` | `bool` | `false` | Log response body | | `MaxBodySize` | `int` | `1024` | Max body to log | ## Examples ### Basic Logging ```go theme={null} app.Use(responselog.New()) ``` ### Include Response Body ```go theme={null} app.Use(responselog.WithOptions(responselog.Options{ IncludeBody: true, MaxBodySize: 4096, })) ``` ### With Custom Logger ```go theme={null} logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) app.Use(responselog.WithLogger(logger)) ``` ## API Reference ### Functions ```go theme={null} // New creates response log middleware func New() mizu.Middleware // WithLogger creates with custom logger func WithLogger(logger *slog.Logger) mizu.Middleware // WithOptions creates with configuration func WithOptions(opts Options) mizu.Middleware ``` ## Technical Details The `responselog` middleware captures response data through a custom `ResponseWriter` wrapper: ### Response Capture Mechanism The middleware uses a `responseCapture` struct that wraps the standard `http.ResponseWriter`: * Intercepts `WriteHeader()` to capture the status code * Intercepts `Write()` to capture response body up to `MaxBodySize` * Automatically restores the original writer after processing ### Logging Behavior Response logs include: * **Method**: HTTP request method * **Path**: Request URL path * **Status**: HTTP status code * **Duration**: Time taken to process the request * **Size**: Response body size in bytes * **Headers**: Response headers (if `LogHeaders` is enabled) * **Body**: Response body content (if `LogBody` is enabled, truncated to `MaxBodySize`) ### Log Levels The middleware automatically sets log levels based on status codes: * Status >= 400: Logged as ERROR * Status \< 400: Logged as INFO ### Filtering Two filtering mechanisms are available: * **SkipPaths**: Paths that should not be logged at all * **SkipStatuses**: Status codes that should not be logged Paths are checked before request processing, while statuses are checked after. ### Default Values * `MaxBodySize`: 4KB (4096 bytes) * `LogBody`: false * `LogHeaders`: false * `Logger`: Default slog text handler to stdout ## Best Practices * Be careful with body logging (sensitive data) * Use for debugging only * Set reasonable body size limits * Combine with request logging ## Testing The middleware includes comprehensive test coverage for all features: | Test Case | Description | Expected Behavior | | ------------------------------ | --------------------------------------------- | --------------------------------------------------------- | | `TestNew` | Basic middleware creation with default logger | Logs response with status code 200 | | `TestWithOptions_LogBody` | Enable response body logging | Response body content appears in logs | | `TestWithOptions_LogHeaders` | Enable response header logging | Response headers appear in logs | | `TestWithOptions_SkipPaths` | Skip logging for specific paths | Configured paths (e.g., /health) are not logged | | `TestWithOptions_SkipStatuses` | Skip logging for specific status codes | Configured status codes (e.g., 200) are not logged | | `TestWithOptions_MaxBodySize` | Limit logged body size | Response body is truncated to MaxBodySize bytes | | `TestErrorsOnly` | Log only error responses | Success responses are skipped, error responses are logged | | `TestDurationLogged` | Verify duration tracking | Request duration appears in logs | | `TestSizeLogged` | Verify size tracking | Response body size appears in logs | ## Related Middlewares * [requestlog](/middlewares/requestlog) - Request logging * [bodydump](/middlewares/bodydump) - Body dumping * [logger](/middlewares/logger) - Simple logging # Response Size Source: https://docs.go-mizu.dev/middlewares/responsesize Response size tracking middleware for monitoring and metrics. ## Overview The `responsesize` middleware tracks response sizes for monitoring, logging, and metrics collection. Use it when you need: * Monitor response sizes * Collect bandwidth metrics * Detect response size anomalies ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/responsesize" ``` ## Quick Start ```go theme={null} app := mizu.New() app.Use(responsesize.New()) ``` ## Configuration ### Options | Option | Type | Default | Description | | --------- | ------------------------ | ------- | ------------- | | `Handler` | `func(*mizu.Ctx, int64)` | `nil` | Size callback | ## Examples ### Track in Context ```go theme={null} app.Use(responsesize.New()) ``` ### With Callback ```go theme={null} app.Use(responsesize.WithHandler(func(c *mizu.Ctx, size int64) { metrics.RecordResponseSize(c.Request().URL.Path, size) })) ``` ### Log Large Responses ```go theme={null} app.Use(responsesize.WithHandler(func(c *mizu.Ctx, size int64) { if size > 1024*1024 { // > 1MB log.Printf("Large response: %s %d bytes", c.Request().URL.Path, size) } })) ``` ## API Reference ### Functions ```go theme={null} // New creates response size middleware func New() mizu.Middleware // WithHandler creates with callback func WithHandler(handler func(*mizu.Ctx, int64)) mizu.Middleware // Get returns response size from context func Get(c *mizu.Ctx) int64 ``` ## Technical Details ### Implementation Architecture The middleware uses a wrapping pattern to intercept and track response writes: 1. **Context Storage**: Response size information is stored in the request context using a custom `contextKey` type for type-safe retrieval. 2. **Writer Wrapping**: The middleware wraps the original `http.ResponseWriter` with a `trackingWriter` that intercepts all `Write()` calls. 3. **Atomic Tracking**: Uses `sync/atomic` operations to safely track bytes written, ensuring thread-safe counting even with concurrent writes. 4. **Callback Mechanism**: After the response is fully written, an optional callback (`OnSize`) is invoked with the total byte count. ### Key Components * **Info struct**: Holds the `bytesWritten` counter with an atomic accessor method `BytesWritten()`. * **trackingWriter**: A wrapper around `http.ResponseWriter` that increments the byte counter on each write. * **contextKey**: An empty struct used as a unique key for storing size info in the context. ### Flow 1. Create `Info` instance and store in context 2. Wrap response writer with `trackingWriter` 3. Execute next handler in chain 4. Invoke `OnSize` callback with final byte count 5. Restore original writer ## Best Practices * Track alongside request sizes * Set alerts for unusually large responses * Use for bandwidth billing * Monitor compression effectiveness ## Testing The middleware includes comprehensive test coverage: | Test Case | Description | Expected Behavior | | -------------------------- | --------------------------------- | ------------------------------------------------------------------------ | | `TestNew` | Basic middleware creation | Middleware executes without errors and returns 200 OK | | `TestWithOptions_Callback` | OnSize callback functionality | Callback receives correct byte count (5 bytes for "12345") | | `TestBytesWritten` | BytesWritten helper function | Returns accurate byte count matching response body length | | `TestGet` | Info retrieval from context | Get() returns valid Info instance from context | | `TestWithCallback` | WithCallback convenience function | Callback is invoked with correct size (3 bytes for "abc") | | `TestEmptyResponse` | Empty response handling | Reports 0 bytes for 204 No Content responses | | `TestMultipleWrites` | Multiple Write() calls | Accumulates bytes across multiple writes ("firstsecondthird" = 16 bytes) | ## Related Middlewares * [requestsize](/middlewares/requestsize) - Request size tracking * [compress](/middlewares/compress) - Response compression * [metrics](/middlewares/metrics) - Metrics collection # Retry Source: https://docs.go-mizu.dev/middlewares/retry Automatic retry middleware for handling transient failures. ## Overview The `retry` middleware automatically retries failed requests with configurable backoff strategies, handling transient failures gracefully. Use it when you need: * Handle temporary network issues * Retry on specific error codes * Implement exponential backoff ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/retry" ``` ## Quick Start ```go theme={null} app := mizu.New() // Retry up to 3 times on 5xx errors app.Use(retry.New(3)) ``` ## Configuration ### Options | Option | Type | Default | Description | | ------------ | ----------------------------- | ----------- | ---------------------- | | `MaxRetries` | `int` | `3` | Maximum retry attempts | | `Delay` | `time.Duration` | `100ms` | Initial delay | | `MaxDelay` | `time.Duration` | `10s` | Maximum delay | | `Multiplier` | `float64` | `2.0` | Backoff multiplier | | `RetryOn` | `[]int` | `[500-599]` | Status codes to retry | | `RetryIf` | `func(*mizu.Ctx, error) bool` | - | Custom retry condition | ## Examples ### Basic Retry ```go theme={null} app.Use(retry.New(3)) ``` ### With Backoff ```go theme={null} app.Use(retry.WithOptions(retry.Options{ MaxRetries: 5, Delay: 100 * time.Millisecond, MaxDelay: 5 * time.Second, Multiplier: 2.0, // Exponential backoff })) ``` ### Specific Status Codes ```go theme={null} app.Use(retry.WithOptions(retry.Options{ MaxRetries: 3, RetryOn: []int{502, 503, 504}, // Gateway errors only })) ``` ### Custom Condition ```go theme={null} app.Use(retry.WithOptions(retry.Options{ MaxRetries: 3, RetryIf: func(c *mizu.Ctx, err error) bool { // Retry on timeout errors return errors.Is(err, context.DeadlineExceeded) }, })) ``` ### Constant Delay ```go theme={null} app.Use(retry.WithOptions(retry.Options{ MaxRetries: 3, Delay: 1 * time.Second, Multiplier: 1.0, // No increase })) ``` ## API Reference ### Functions ```go theme={null} // New creates retry middleware func New(maxRetries int) mizu.Middleware // WithOptions creates with configuration func WithOptions(opts Options) mizu.Middleware ``` ## Backoff Calculation ``` delay = min(initialDelay * (multiplier ^ attempt), maxDelay) ``` Example with defaults: * Attempt 1: 100ms * Attempt 2: 200ms * Attempt 3: 400ms ## Technical Details ### Implementation Architecture The retry middleware uses a custom `retryResponseWriter` wrapper that intercepts response status codes to determine if a retry should occur. Key implementation details: * **Response Writer Wrapping**: Each retry attempt wraps the response writer to capture the status code without committing the response until success or max retries reached * **Exponential Backoff Algorithm**: Delay calculation follows the formula: `delay = min(initialDelay * (multiplier ^ attempt), maxDelay)` * **Retry Decision Logic**: The `RetryIf` function receives the context, error, and current attempt number to make intelligent retry decisions * **State Management**: The middleware tracks attempt count, last error, and current delay across retry iterations ### Default Behavior When using `New()` without options, the middleware: * Retries up to 3 times (4 total attempts including initial) * Starts with 100ms delay * Uses 2.0x multiplier for exponential backoff * Caps maximum delay at 1 second * Retries on any error or 5xx HTTP status codes ### Helper Functions The package provides several helper functions for common retry patterns: * **`RetryOn(codes ...int)`**: Creates a RetryIf function that retries only on specific HTTP status codes * **`RetryOnError()`**: Creates a RetryIf function that retries only when an error is returned * **`NoRetry()`**: Creates a RetryIf function that disables retries (useful for testing) ### Performance Considerations * The middleware sleeps between retries using `time.Sleep`, blocking the goroutine * Response writer wrapping adds minimal overhead * Status code capture happens in memory without additional allocations * The `OnRetry` callback allows for logging and metrics without impacting retry logic ## Best Practices * Use exponential backoff to prevent thundering herd * Set reasonable max retries (3-5) * Only retry idempotent operations * Add jitter for distributed systems * Log retry attempts for debugging ## Testing ### Test Coverage The retry middleware includes comprehensive test cases covering various scenarios: | Test Case | Description | Expected Behavior | | ------------------------------------ | ---------------------------------------- | --------------------------------------------------------------------------- | | `TestNew` | Basic retry with default options | Retries up to 3 times on errors, succeeds on 3rd attempt | | `TestWithOptions_MaxRetries` | Custom max retries configuration | Respects MaxRetries setting (1 initial + 2 retries = 3 attempts) | | `TestWithOptions_OnRetry` | OnRetry callback invocation | OnRetry called before each retry attempt (not on initial attempt) | | `TestWithOptions_NoRetryNeeded` | Successful request on first attempt | No retries when request succeeds immediately | | `TestWithOptions_CustomRetryIf` | Custom retry condition logic | Custom RetryIf function controls retry behavior independently of MaxRetries | | `TestRetryOn` | RetryOn helper for specific status codes | Retries only on specified status codes (500, 502, 503) | | `TestRetryOnError` | RetryOnError helper function | Retries only when error is present, not on status codes | | `TestNoRetry` | NoRetry helper function | Never retries regardless of error or status code | | `TestWithOptions_ExponentialBackoff` | Exponential backoff timing | Delays increase exponentially between retry attempts | ## Related Middlewares * [circuitbreaker](/middlewares/circuitbreaker) - Circuit breaker * [timeout](/middlewares/timeout) - Request timeout * [idempotency](/middlewares/idempotency) - Idempotency keys # Rewrite Source: https://docs.go-mizu.dev/middlewares/rewrite URL rewriting middleware for path transformation without redirects. ## Overview The `rewrite` middleware transforms URL paths internally without sending redirects to the client. The browser URL remains unchanged while the server processes a different path. ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/rewrite" ``` ## Quick Start ```go theme={null} app := mizu.New() // Rewrite /api/v1/* to /api/* app.Use(rewrite.New(rewrite.Prefix("/api/v1", "/api"))) ``` ## Examples ### Prefix Rewrite ```go theme={null} // /old/* → /new/* app.Use(rewrite.New(rewrite.Prefix("/old", "/new"))) ``` ### Regex Rewrite ```go theme={null} // /posts/123 → /articles/123 app.Use(rewrite.New(rewrite.Regex(`^/posts/(\d+)$`, "/articles/$1"))) ``` ### Multiple Rules ```go theme={null} app.Use(rewrite.New( rewrite.Prefix("/api/v1", "/api"), rewrite.Prefix("/legacy", "/current"), rewrite.Regex(`^/user/(\d+)$`, "/users/$1"), )) ``` ### Version Stripping ```go theme={null} // Remove API version from path app.Use(rewrite.New( rewrite.Regex(`^/v\d+/(.*)$`, "/$1"), )) ``` ## API Reference ```go theme={null} func New(rules ...Rule) mizu.Middleware func WithOptions(opts Options) mizu.Middleware func Prefix(from, to string) Rule func Regex(pattern, replacement string) Rule ``` ### Rule Type ```go theme={null} type Rule struct { Match string // Pattern to match Rewrite string // Replacement pattern Regex bool // Is Match a regex? } ``` ## Rewrite vs Redirect | Feature | Rewrite | Redirect | | --------------- | ---------------- | ----------- | | Client sees | Original URL | New URL | | HTTP round trip | No | Yes | | SEO | Hidden | Visible | | Use case | Internal routing | URL changes | ## Technical Details ### Implementation Overview The rewrite middleware operates by modifying the `Request.URL.Path` before the request reaches downstream handlers: 1. **Rule Compilation**: Regex patterns are compiled once during middleware initialization to optimize performance 2. **Path Matching**: Each incoming request's path is evaluated against rules in order 3. **First Match Wins**: Processing stops at the first matching rule 4. **Path Replacement**: The request URL path is modified in-place ### Rule Processing **Prefix Rules**: * Uses `strings.HasPrefix()` for fast prefix matching * Strips the matched prefix and prepends the replacement * Example: `/old/path` with rule `Prefix("/old", "/new")` becomes `/new/path` **Regex Rules**: * Compiled using Go's `regexp` package during initialization * Supports capture groups (`$1`, `$2`, etc.) for dynamic replacements * Uses `ReplaceAllString()` for substitution * Example: `/user/123/profile` with pattern `^/user/(\d+)/profile$` and replacement `/profiles/$1` becomes `/profiles/123` ### Performance Characteristics * **Initialization**: O(n) where n is the number of regex rules * **Per-Request**: O(r) where r is the number of rules (worst case: no match) * **Memory**: Compiled regex patterns are cached * **Early Exit**: Processing stops after first match for efficiency ### Internal State The middleware maintains: * Compiled regex patterns in the `Rule.re` field * Original rule configuration in the `Options` struct * No request-specific state (thread-safe) ## Best Practices ### Rule Ordering Place more specific rules before general ones: ```go theme={null} app.Use(rewrite.New( rewrite.Regex(`^/api/v2/(.*)$`, "/v2/$1"), // Specific rewrite.Prefix("/api", "/internal"), // General )) ``` ### Performance Optimization * Minimize the number of regex rules when possible * Use prefix rules for simple path transformations * Test regex patterns for efficiency with large inputs ### Security Considerations * Validate rewrite patterns to prevent unintended path access * Be cautious with user-controlled input in dynamic rules * Consider path traversal implications when rewriting paths ## Testing The rewrite middleware includes comprehensive test coverage for various scenarios: | Test Case | Description | Expected Behavior | | ------------------------------- | -------------------------- | --------------------------------------------------------------------- | | `TestNew` | Basic prefix rewrite | Rewrites `/old/path` to `/new/path` using prefix rule | | `TestWithOptions_Regex` | Regex pattern matching | Matches regex pattern with version number and resource path | | `TestWithOptions_MultipleRules` | Multiple rule processing | Applies first matching rule and stops processing | | `TestWithOptions_NoMatch` | Path without matching rule | Leaves path unchanged when no rules match | | `TestPrefix` | Prefix rule creation | Creates rule with correct match, rewrite, and regex=false | | `TestRegex` | Regex rule creation | Creates rule with pattern, replacement, and regex=true | | `TestWithOptions_RegexCapture` | Regex capture groups | Replaces `/user/123/profile` with `/profiles/123` using capture group | ## Related Middlewares * [redirect](/middlewares/redirect) - URL redirects * [slash](/middlewares/slash) - Trailing slash # Sanitizer Source: https://docs.go-mizu.dev/middlewares/sanitizer Input sanitization middleware for security and data cleaning. ## Overview The `sanitizer` middleware sanitizes request data to prevent XSS, SQL injection, and other injection attacks. Use it when you need: * XSS prevention * Input cleaning * Data normalization ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/sanitizer" ``` ## Quick Start ```go theme={null} app := mizu.New() // Sanitize all string inputs app.Use(sanitizer.New()) ``` ## Configuration ### Options | Option | Type | Default | Description | | -------------- | ---------- | ------- | ------------------ | | `StripHTML` | `bool` | `true` | Remove HTML tags | | `TrimSpace` | `bool` | `true` | Trim whitespace | | `StripScripts` | `bool` | `true` | Remove script tags | | `EscapeHTML` | `bool` | `false` | HTML escape | | `Fields` | `[]string` | All | Specific fields | ## Examples ### Default Sanitization ```go theme={null} app.Use(sanitizer.New()) ``` ### Strip HTML ```go theme={null} app.Use(sanitizer.WithOptions(sanitizer.Options{ StripHTML: true, })) ``` ### Escape Instead of Strip ```go theme={null} app.Use(sanitizer.WithOptions(sanitizer.Options{ StripHTML: false, EscapeHTML: true, })) ``` ### Specific Fields ```go theme={null} app.Use(sanitizer.WithOptions(sanitizer.Options{ Fields: []string{"name", "email", "comment"}, })) ``` ### Custom Sanitizer ```go theme={null} app.Use(sanitizer.WithOptions(sanitizer.Options{ Custom: func(field string, value string) string { // Custom sanitization logic return strings.TrimSpace(value) }, })) ``` ## API Reference ### Functions ```go theme={null} // New creates sanitizer middleware func New() mizu.Middleware // WithOptions creates with configuration func WithOptions(opts Options) mizu.Middleware // SanitizeString sanitizes a single string func SanitizeString(s string, opts Options) string ``` ## What Gets Sanitized * Query parameters * Form data * JSON body fields * Path parameters ## Technical Details The sanitizer middleware operates by intercepting HTTP requests and applying configurable sanitization rules to input data before it reaches your handlers. ### Implementation Overview The middleware processes request data in the following order: 1. **Query Parameters**: Sanitizes all URL query parameters 2. **Form Data**: For POST/PUT/PATCH requests, sanitizes both `r.Form` and `r.PostForm` fields 3. **Field Filtering**: Applies whitelist (`Fields`) or blacklist (`Exclude`) filtering ### Sanitization Pipeline Each value passes through a configurable pipeline of operations: 1. **Trim Spaces** (`TrimSpaces`): Removes leading and trailing whitespace using `strings.TrimSpace` 2. **Strip Non-Printable** (`StripNonPrintable`): Removes non-printable characters while preserving newlines, carriage returns, and tabs 3. **Strip Tags** (`StripTags`): Removes HTML tags using regex-based matching: * First removes `World"` becomes `"HelloWorld"`) | | `TestWithOptions_MaxLength` | MaxLength option with long input | Value truncated to specified length (`"VeryLongName"` becomes `"VeryL"` with MaxLength=5) | | `TestWithOptions_Fields` | Fields whitelist with multiple parameters | Only specified fields sanitized, others passed through unchanged | | `TestWithOptions_Exclude` | Exclude blacklist with multiple parameters | Excluded fields bypass sanitization, others are sanitized | | `TestXSS` | XSS prevention preset | Script tags escaped to prevent XSS attacks | | `TestStripHTML` | HTML stripping preset | All HTML tags removed from input | | `TestTrim` | Trim whitespace preset | Leading and trailing whitespace removed | | `TestSanitize` | Direct sanitization function with various options | Correct sanitization applied: HTML escape, trim, strip tags, max length | | `TestSanitizeHTML` | HTML sanitization helper | HTML escaped and trimmed | | `TestStripTagsString` | Tag stripping helper | HTML tags removed from string | | `TestTrimString` | Trim helper function | Whitespace trimmed from string | | `TestClean` | All-in-one cleaning function | All sanitization operations applied (HTML escape, trim, strip tags, strip non-printable) | | `TestWithOptions_StripNonPrintable` | Non-printable character removal | Control characters removed (`"hello\x00world\x1f"` becomes `"helloworld"`) | ## Related Middlewares * [validator](/middlewares/validator) - Input validation * [bodylimit](/middlewares/bodylimit) - Size limits * [helmet](/middlewares/helmet) - Security headers # Secure Source: https://docs.go-mizu.dev/middlewares/secure HTTPS enforcement and comprehensive security middleware. ## Overview The `secure` middleware enforces HTTPS connections and adds security headers. It's a comprehensive security solution that combines SSL redirect with security header management. Use it when you need: * Automatic HTTP to HTTPS redirect * Security headers in one middleware * Flexible security configuration * Development/production toggle ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/secure" ``` ## Quick Start ```go theme={null} app := mizu.New() // Default security settings app.Use(secure.New()) ``` ## Configuration ### Options | Option | Type | Default | Description | | ----------------------- | ---------- | ----------------------- | --------------------------- | | `SSLRedirect` | `bool` | `true` | Redirect HTTP to HTTPS | | `SSLHost` | `string` | `""` | Host for SSL redirect | | `SSLTemporaryRedirect` | `bool` | `false` | Use 307 instead of 301 | | `STSSeconds` | `int64` | `0` | HSTS max-age (0 = disabled) | | `STSIncludeSubdomains` | `bool` | `false` | HSTS includeSubDomains | | `STSPreload` | `bool` | `false` | HSTS preload | | `ForceSTSHeader` | `bool` | `false` | Send HSTS on HTTP | | `ContentTypeNosniff` | `bool` | `true` | X-Content-Type-Options | | `FrameDeny` | `bool` | `true` | X-Frame-Options: DENY | | `CustomFrameOptions` | `string` | `""` | Custom X-Frame-Options | | `XSSProtection` | `string` | `"1; mode=block"` | X-XSS-Protection | | `ContentSecurityPolicy` | `string` | `""` | CSP header | | `ReferrerPolicy` | `string` | `""` | Referrer-Policy | | `IsDevelopment` | `bool` | `false` | Disable all features | | `ProxyHeaders` | `[]string` | `["X-Forwarded-Proto"]` | Headers for HTTPS detection | ## Examples ### Basic HTTPS Redirect ```go theme={null} // Redirect all HTTP to HTTPS app.Use(secure.New()) ``` ### Production Configuration ```go theme={null} app.Use(secure.WithOptions(secure.Options{ SSLRedirect: true, STSSeconds: 31536000, // 1 year STSIncludeSubdomains: true, STSPreload: true, ContentTypeNosniff: true, FrameDeny: true, ContentSecurityPolicy: "default-src 'self'", ReferrerPolicy: "strict-origin-when-cross-origin", })) ``` ### Development Mode ```go theme={null} app.Use(secure.WithOptions(secure.Options{ IsDevelopment: true, // Disables all security features })) ``` ### Behind a Proxy ```go theme={null} app.Use(secure.WithOptions(secure.Options{ SSLRedirect: true, ProxyHeaders: []string{ "X-Forwarded-Proto", "X-Forwarded-SSL", }, })) ``` ### Custom SSL Host ```go theme={null} // Redirect to different host app.Use(secure.WithOptions(secure.Options{ SSLRedirect: true, SSLHost: "secure.example.com", })) ``` ### Temporary Redirect ```go theme={null} // Use 307 for testing, preserves method app.Use(secure.WithOptions(secure.Options{ SSLRedirect: true, SSLTemporaryRedirect: true, })) ``` ### Frame Options ```go theme={null} // Allow framing from same origin app.Use(secure.WithOptions(secure.Options{ FrameDeny: false, CustomFrameOptions: "SAMEORIGIN", })) ``` ### Environment-Based Configuration ```go theme={null} func setupSecurity(app *mizu.App) { isDev := os.Getenv("ENV") == "development" app.Use(secure.WithOptions(secure.Options{ IsDevelopment: isDev, SSLRedirect: !isDev, STSSeconds: 31536000, STSIncludeSubdomains: true, ContentTypeNosniff: true, FrameDeny: true, })) } ``` ### Full Security Setup ```go theme={null} app.Use(secure.WithOptions(secure.Options{ // SSL SSLRedirect: true, SSLTemporaryRedirect: false, // HSTS STSSeconds: 31536000, STSIncludeSubdomains: true, STSPreload: true, // Security Headers ContentTypeNosniff: true, FrameDeny: true, XSSProtection: "1; mode=block", ContentSecurityPolicy: "default-src 'self'; script-src 'self'", ReferrerPolicy: "strict-origin-when-cross-origin", // Proxy support ProxyHeaders: []string{"X-Forwarded-Proto"}, })) ``` ## API Reference ### Functions ```go theme={null} // New creates middleware with default options func New() mizu.Middleware // WithOptions creates middleware with custom options func WithOptions(opts Options) mizu.Middleware ``` ## Headers Set The middleware sets these headers based on configuration: | Header | Condition | Value | | --------------------------- | ------------------- | --------------------------------------- | | `Strict-Transport-Security` | STSSeconds > 0 | `max-age=X; includeSubDomains; preload` | | `X-Content-Type-Options` | ContentTypeNosniff | `nosniff` | | `X-Frame-Options` | FrameDeny or Custom | `DENY` or custom | | `X-XSS-Protection` | XSSProtection set | Configured value | | `Content-Security-Policy` | CSP set | Configured policy | | `Referrer-Policy` | ReferrerPolicy set | Configured policy | ## Technical Details ### Implementation Overview The secure middleware implements a comprehensive security layer that: 1. **HTTPS Detection**: Checks for secure connections via: * Direct TLS connection (r.TLS != nil) * Proxy headers (X-Forwarded-Proto, X-Forwarded-SSL, etc.) * Case-insensitive header matching using strings.EqualFold 2. **SSL Redirection**: When HTTP is detected and SSLRedirect is enabled: * Constructs HTTPS URL: `https://` + host + requestURI * Uses SSLHost if specified, otherwise uses request host * Returns 301 (permanent) or 307 (temporary) redirect based on SSLTemporaryRedirect 3. **Security Header Application**: Sets headers conditionally based on: * Connection security (HTTPS vs HTTP) * Configuration options * Development mode (bypasses all security when IsDevelopment is true) ### Header Construction **Strict-Transport-Security (HSTS)**: ``` max-age=[; includeSubDomains][; preload] ``` * Only set on HTTPS connections unless ForceSTSHeader is true * Requires STSSeconds > 0 to be enabled **X-Frame-Options**: * Priority: CustomFrameOptions > FrameDeny > none * FrameDeny sets "DENY", CustomFrameOptions allows "SAMEORIGIN" or other values **Default Headers** (when using New()): * X-Content-Type-Options: nosniff * X-Frame-Options: DENY * X-XSS-Protection: 1; mode=block * SSLRedirect: enabled ### Proxy Support The middleware checks proxy headers in order: ```go theme={null} for _, header := range opts.ProxyHeaders { if strings.EqualFold(r.Header.Get(header), "https") { isSSL = true break } } ``` Common proxy headers: * X-Forwarded-Proto (default) * X-Forwarded-SSL * Front-End-Https ### Performance Optimization The middleware uses a custom `itoa` function instead of strconv.Itoa for integer-to-string conversion: * Avoids heap allocations * Uses fixed-size array \[20]byte for conversion * Handles negative numbers and zero efficiently ## Security Considerations 1. **HSTS Commitment** - Once set, browsers remember for max-age duration 2. **SSL Certificate** - Ensure valid certificate before enabling HSTS 3. **Subdomains** - Only enable STSIncludeSubdomains if all subdomains support HTTPS 4. **Preload** - Apply at hstspreload.org for browser built-in list ### HSTS Timeline ```go theme={null} // Start with short duration app.Use(secure.WithOptions(secure.Options{ STSSeconds: 3600, // 1 hour for testing })) // Increase gradually // STSSeconds: 86400 // 1 day // STSSeconds: 604800 // 1 week // STSSeconds: 31536000 // 1 year (production) ``` ## Best Practices * Start with short HSTS duration, increase gradually * Test thoroughly before enabling preload * Use `IsDevelopment` flag for local development * Configure `ProxyHeaders` when behind load balancers ## Testing The secure middleware includes comprehensive test coverage for all features: | Test Case | Description | Expected Behavior | | ------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ | | `TestNew` | Default middleware with SSL redirect | Redirects HTTP to HTTPS with 301 status, Location header set to [https://example.com/](https://example.com/) | | `TestWithOptions_NoSSLRedirect` | Middleware with SSL redirect disabled but security headers enabled | Returns 200 OK, sets X-Content-Type-Options: nosniff, X-Frame-Options: DENY, X-XSS-Protection: 1; mode=block | | `TestWithOptions_HSTS` | HSTS configuration with all options enabled | Sets Strict-Transport-Security header with max-age=31536000; includeSubDomains; preload | | `TestWithOptions_CustomFrameOptions` | Custom frame options instead of default DENY | Sets X-Frame-Options: SAMEORIGIN | | `TestWithOptions_CSP` | Content Security Policy configuration | Sets Content-Security-Policy: default-src 'self' | | `TestWithOptions_ReferrerPolicy` | Referrer policy configuration | Sets Referrer-Policy: strict-origin-when-cross-origin | | `TestWithOptions_SSLHost` | SSL redirect to custom host with temporary redirect | Redirects with 307 status to [https://secure.example.com/path?q=1](https://secure.example.com/path?q=1) | | `TestWithOptions_ProxyHeader` | Request behind proxy with X-Forwarded-Proto: https | Does not redirect (200 OK) since connection is detected as HTTPS via proxy header | | `TestWithOptions_IsDevelopment` | Development mode enabled | Bypasses all security features, returns 200 OK without redirect or headers | ## Related Middlewares * [helmet](/middlewares/helmet) - Detailed security headers * [redirect](/middlewares/redirect) - URL redirection # Sentry Source: https://docs.go-mizu.dev/middlewares/sentry Sentry error tracking middleware for error monitoring. ## Overview The `sentry` middleware integrates with Sentry for error tracking, performance monitoring, and debugging. Use it when you need: * Error tracking * Performance monitoring * Issue management ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/sentry" ``` ## Quick Start ```go theme={null} app := mizu.New() // Initialize Sentry sentry.Init(sentry.Options{ DSN: "https://key@sentry.io/project", }) app.Use(sentry.Middleware()) ``` ## Configuration ### Options | Option | Type | Default | Description | | ------------------ | --------- | -------- | ----------------------- | | `DSN` | `string` | Required | Sentry DSN | | `Environment` | `string` | `""` | Environment name | | `Release` | `string` | `""` | Release version | | `SampleRate` | `float64` | `1.0` | Error sample rate | | `TracesSampleRate` | `float64` | `0` | Performance sample rate | ## Examples ### Basic Setup ```go theme={null} sentry.Init(sentry.Options{ DSN: os.Getenv("SENTRY_DSN"), }) app.Use(sentry.Middleware()) ``` ### With Environment ```go theme={null} sentry.Init(sentry.Options{ DSN: os.Getenv("SENTRY_DSN"), Environment: os.Getenv("ENV"), Release: "v1.0.0", }) ``` ### Performance Monitoring ```go theme={null} sentry.Init(sentry.Options{ DSN: os.Getenv("SENTRY_DSN"), TracesSampleRate: 0.1, // 10% of transactions }) ``` ### Manual Error Capture ```go theme={null} app.Get("/", func(c *mizu.Ctx) error { if err := doSomething(); err != nil { sentry.CaptureError(c, err) return c.JSON(500, map[string]string{"error": "Internal error"}) } return c.JSON(200, data) }) ``` ### Add Context ```go theme={null} app.Use(func(next mizu.Handler) mizu.Handler { return func(c *mizu.Ctx) error { sentry.SetUser(c, sentry.User{ ID: getUserID(c), Email: getEmail(c), }) sentry.SetTag(c, "feature", "checkout") return next(c) } }) ``` ## API Reference ### Functions ```go theme={null} // Init initializes Sentry func Init(opts Options) error // Middleware creates Sentry middleware func Middleware() mizu.Middleware // CaptureError sends error to Sentry func CaptureError(c *mizu.Ctx, err error) // SetUser sets user context func SetUser(c *mizu.Ctx, user User) // SetTag sets a tag func SetTag(c *mizu.Ctx, key, value string) ``` ## Technical Details ### Architecture The Sentry middleware is built around a lightweight error tracking system with the following components: **Hub**: Central management structure that handles event capture, storage, and transport. Each middleware instance creates a Hub that is stored in the request context. **Event**: Represents an error event with metadata including: * Event ID, timestamp, and severity level * Exception details with stack traces * HTTP request information (URL, method, headers, query strings) * User context, tags, and extra data * Runtime context (Go version, platform) **Transport**: Interface for sending events to Sentry. Supports custom implementations via the `Transport` interface: * `MockTransport`: For testing, stores events in memory * `HTTPTransport`: Sends events to Sentry via HTTP * Custom transports can be implemented by satisfying the `Transport` interface ### Event Capture Flow 1. Middleware wraps the handler with panic recovery 2. Hub is stored in request context for the lifetime of the request 3. Errors returned from handlers are automatically captured 4. Panics are caught, converted to events, and re-thrown 5. Events are processed through hooks (BeforeSend) before sending 6. Events are sent asynchronously via the configured transport ### Stack Trace Capture Stack traces are captured using Go's `runtime.Callers` and `runtime.CallersFrames`: * Frames include filename, function name, line number, and absolute path * Configurable skip depth to exclude middleware internals * Frames are reversed to show most recent call first ### Header Filtering Sensitive headers are automatically filtered from request data: * `Authorization` * `Cookie` * `X-Api-Key` ### Sampling Events can be sampled based on `SampleRate` (0.0 to 1.0): * 1.0 captures all events (default) * 0.5 captures 50% of events * Sampling uses nanosecond precision for distribution ### Context Management The middleware uses Go's context to store: * Hub instance (via `hubKey`) * User information (via `userContextKey`) * Tags (via `tagsContextKey`) * Extra data (via `extraContextKey`) ## Best Practices * Use environment for staging/production * Set release for version tracking * Add user context for debugging * Sample transactions in production ## Testing The Sentry middleware includes comprehensive test coverage for all major features: | Test Case | Description | Expected Behavior | | ------------------------------ | ------------------------------------- | ----------------------------------------------------------- | | `TestNew` | Basic middleware initialization | Middleware processes requests successfully | | `TestCaptureError` | Error capture from handler | Error event captured with correct message and level "error" | | `TestCaptureMessage` | Manual message capture | Message event captured with level "info" | | `TestCapturePanic` | Panic recovery | Panic captured as "fatal" level event and re-thrown | | `TestEnvironmentAndRelease` | Environment and release configuration | Events include configured environment and release values | | `TestTags` | Default tags configuration | Default tags applied to all events | | `TestBeforeSend` | BeforeSend hook modification | Hook can modify events before sending | | `TestBeforeSendDrop` | BeforeSend hook event dropping | Returning nil from hook prevents event capture | | `TestRequestInfo` | HTTP request metadata capture | Event includes method, URL, query string, and headers | | `TestSensitiveHeadersFiltered` | Sensitive header filtering | Authorization, Cookie, and X-Api-Key headers excluded | | `TestStacktrace` | Stack trace generation | Events include stack trace with frames | | `TestMockTransport` | Mock transport for testing | Events sent to custom transport | | `TestHubEvents` | Hub event storage and clearing | Events can be retrieved and cleared from hub | | `TestGetHub` | Hub retrieval from context | Hub accessible within request handlers | | `TestSetUser` | User context setting | User information can be set on context | | `TestSetTagAndExtra` | Dynamic tags and extra data | Tags and extra data can be set per request | ## Related Middlewares * [recover](/middlewares/recover) - Panic recovery * [otel](/middlewares/otel) - OpenTelemetry * [logger](/middlewares/logger) - Request logging # Session Source: https://docs.go-mizu.dev/middlewares/session Server-side session management middleware with cookie-based tracking. ## Overview The `session` middleware provides server-side session management with secure cookie-based tracking. Sessions can store user data across requests with configurable storage backends. Use it when you need: * User login sessions * Shopping cart persistence * Multi-step form wizards ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/session" ``` ## Quick Start ```go theme={null} app := mizu.New() // Enable sessions app.Use(session.New(session.Options{})) app.Get("/login", func(c *mizu.Ctx) error { sess := session.Get(c) sess.Set("user_id", "123") return c.Text(200, "Logged in") }) app.Get("/profile", func(c *mizu.Ctx) error { sess := session.Get(c) userID, _ := sess.Get("user_id").(string) return c.Text(200, "User: "+userID) }) ``` ## Configuration ### Options | Option | Type | Default | Description | | ---------------- | --------------- | -------------- | ------------------------ | | `CookieName` | `string` | `"session_id"` | Session cookie name | | `CookiePath` | `string` | `"/"` | Cookie path | | `CookieDomain` | `string` | `""` | Cookie domain | | `CookieSecure` | `bool` | `false` | HTTPS only | | `CookieHTTPOnly` | `bool` | `true` | HTTP only (no JS access) | | `SameSite` | `http.SameSite` | `Lax` | SameSite policy | | `MaxAge` | `time.Duration` | `24h` | Session lifetime | ## Examples ### Basic Sessions ```go theme={null} app.Use(session.New(session.Options{})) app.Post("/login", func(c *mizu.Ctx) error { sess := session.Get(c) sess.Set("user", "john") sess.Set("role", "admin") return c.JSON(200, map[string]string{"status": "logged in"}) }) ``` ### Secure Cookie Settings ```go theme={null} app.Use(session.New(session.Options{ CookieName: "my_app_session", CookieSecure: true, CookiePath: "/", SameSite: http.SameSiteStrictMode, MaxAge: 7 * 24 * time.Hour, // 1 week })) ``` ### Custom Store ```go theme={null} store := session.NewMemoryStore() app.Use(session.WithStore(store, session.Options{})) ``` ### Session Operations ```go theme={null} app.Get("/session", func(c *mizu.Ctx) error { sess := session.Get(c) // Set value sess.Set("key", "value") // Get value val := sess.Get("key") // Delete value sess.Delete("key") // Clear all values sess.Clear() // Get session ID id := sess.ID return c.Text(200, id) }) ``` ### Logout ```go theme={null} app.Post("/logout", func(c *mizu.Ctx) error { sess := session.Get(c) sess.Clear() return c.JSON(200, map[string]string{"status": "logged out"}) }) ``` ## API Reference ### Functions ```go theme={null} // New creates session middleware func New(opts Options) mizu.Middleware // WithStore creates middleware with custom store func WithStore(store Store, opts Options) mizu.Middleware // Get returns session from context func Get(c *mizu.Ctx) *Session // FromContext returns session from context (alias) func FromContext(c *mizu.Ctx) *Session // NewMemoryStore creates in-memory session store func NewMemoryStore() *MemoryStore ``` ### Session Methods ```go theme={null} type Session struct { ID string } func (s *Session) Set(key string, value any) func (s *Session) Get(key string) any func (s *Session) Delete(key string) func (s *Session) Clear() ``` ### Store Interface ```go theme={null} type Store interface { Get(id string) (*Session, error) Save(sess *Session) error Delete(id string) error } ``` ## Technical Details ### Session Lifecycle The session middleware manages a complete lifecycle for each session: 1. **Session Creation**: When a new request arrives without a session cookie, a new session ID is generated using `crypto/rand` (32 bytes, 64 hex characters) and stored in the context 2. **Cookie Management**: Session cookies are set before the handler executes to ensure headers are properly sent 3. **Data Persistence**: Session data is saved to the store only when changes are detected via the `changed` flag 4. **Cleanup**: The memory store includes automatic cleanup that runs every 10 minutes to remove sessions inactive for more than 24 hours ### Thread Safety The Session struct uses `sync.RWMutex` for concurrent access: * **Read operations** (`Get`): Use read locks to allow multiple concurrent reads * **Write operations** (`Set`, `Delete`, `Clear`, `Destroy`): Use write locks for exclusive access * The `changed` flag tracks modifications to optimize store operations ### Store Implementation The package includes a `MemoryStore` implementation with: * **Deep copying**: `Get` returns a deep copy of session data to prevent external mutations * **Background cleanup**: Automatic goroutine removes expired sessions every 10 minutes * **Concurrent safety**: All operations are protected by `sync.RWMutex` ### Session ID Generation Session IDs are generated using: ```go theme={null} crypto/rand.Read(32 bytes) -> hex.EncodeToString() = 64 character ID ``` This provides cryptographically secure random IDs suitable for production use. ### Context Integration Sessions are stored in the request context using a private `contextKey{}` struct, preventing collisions with other middleware or application code. ## Security Considerations 1. **Use HTTPS** - Always set `CookieSecure: true` in production 2. **HTTPOnly cookies** - Keep default to prevent XSS access 3. **Strict SameSite** - Use `SameSiteStrictMode` for sensitive apps 4. **Regenerate ID** - After login to prevent session fixation 5. **Set expiration** - Don't use infinite sessions ## Best Practices * Use secure cookie settings in production * Store minimal data in sessions * Implement session store cleanup for expired sessions * Consider Redis or database store for distributed systems ## Testing The session middleware includes comprehensive test coverage for all operations: | Test Case | Description | Expected Behavior | | -------------------------- | ----------------------------- | -------------------------------------------------------------------- | | `TestNew` | Set and get session value | Session values persist across requests with the same session cookie | | `TestSession_Clear` | Clear all session values | All session values are removed after calling `Clear()` | | `TestSession_Delete` | Delete specific session value | Individual session value is removed after calling `Delete(key)` | | `TestWithStore` | Custom store integration | Session middleware works with custom Store implementations | | `TestSession_CustomCookie` | Custom cookie configuration | Cookie settings (name, path, secure, SameSite) are applied correctly | | `TestFromContext` | Context retrieval alias | `Get()` and `FromContext()` return the same session instance | | `TestGenerateSessionID` | Session ID generation | Generates unique 64-character hex IDs without duplicates | ## Related Middlewares * [jwt](/middlewares/jwt) - Stateless token authentication * [csrf](/middlewares/csrf) - CSRF protection * [secure](/middlewares/secure) - Security settings # Signature Source: https://docs.go-mizu.dev/middlewares/signature Request signature verification middleware for secure API access. ## Overview The `signature` middleware verifies request signatures using HMAC or other algorithms, ensuring requests haven't been tampered with and come from authorized sources. Use it when you need: * Webhook signature verification * API request authentication * Tamper detection ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/signature" ``` ## Quick Start ```go theme={null} app := mizu.New() // Verify HMAC-SHA256 signatures app.Use(signature.New(signature.Options{ Secret: []byte("your-secret-key"), Header: "X-Signature", })) ``` ## Configuration ### Options | Option | Type | Default | Description | | -------------------- | ------------------------ | --------------- | --------------------- | | `Secret` | `[]byte` | Required | Signing secret | | `Header` | `string` | `"X-Signature"` | Signature header name | | `Algorithm` | `string` | `"sha256"` | Hash algorithm | | `Encoding` | `string` | `"hex"` | Signature encoding | | `TimestampHeader` | `string` | `""` | Timestamp header | | `TimestampTolerance` | `time.Duration` | `5m` | Max timestamp age | | `GetSecret` | `func(*mizu.Ctx) []byte` | - | Dynamic secret lookup | ## Examples ### Basic HMAC Verification ```go theme={null} app.Use(signature.New(signature.Options{ Secret: []byte("webhook-secret"), Header: "X-Signature", })) ``` ### GitHub Webhook ```go theme={null} app.Post("/webhook/github", handler, signature.New(signature.Options{ Secret: []byte(os.Getenv("GITHUB_WEBHOOK_SECRET")), Header: "X-Hub-Signature-256", Prefix: "sha256=", Algorithm: "sha256", })) ``` ### Stripe Webhook ```go theme={null} app.Post("/webhook/stripe", handler, signature.New(signature.Options{ Secret: []byte(os.Getenv("STRIPE_WEBHOOK_SECRET")), Header: "Stripe-Signature", TimestampHeader: "Stripe-Signature", ParseStripeStyle: true, })) ``` ### With Timestamp Validation ```go theme={null} app.Use(signature.New(signature.Options{ Secret: []byte("api-secret"), Header: "X-Signature", TimestampHeader: "X-Timestamp", TimestampTolerance: 5 * time.Minute, })) ``` ### Per-Client Secrets ```go theme={null} app.Use(signature.New(signature.Options{ Header: "X-Signature", GetSecret: func(c *mizu.Ctx) []byte { clientID := c.Request().Header.Get("X-Client-ID") return getClientSecret(clientID) }, })) ``` ### Custom Signature Format ```go theme={null} app.Use(signature.New(signature.Options{ Secret: secret, Header: "Authorization", Prefix: "HMAC-SHA256 ", Algorithm: "sha256", Encoding: "base64", })) ``` ## API Reference ### Functions ```go theme={null} // New creates signature verification middleware func New(opts Options) mizu.Middleware // Sign generates signature for data func Sign(data, secret []byte, algorithm string) string // Verify checks if signature is valid func Verify(data, secret []byte, signature, algorithm string) bool ``` ## Signature Algorithms | Algorithm | Description | | --------- | ------------------------- | | `sha256` | HMAC-SHA256 (recommended) | | `sha512` | HMAC-SHA512 | | `sha1` | HMAC-SHA1 (legacy) | ## Creating Signatures (Client Side) ```go theme={null} // Go client func signRequest(body []byte, secret string) string { mac := hmac.New(sha256.New, []byte(secret)) mac.Write(body) return hex.EncodeToString(mac.Sum(nil)) } ``` ```javascript theme={null} // JavaScript client async function signRequest(body, secret) { const encoder = new TextEncoder(); const key = await crypto.subtle.importKey( 'raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'] ); const sig = await crypto.subtle.sign('HMAC', key, encoder.encode(body)); return Array.from(new Uint8Array(sig)) .map(b => b.toString(16).padStart(2, '0')) .join(''); } ``` ## Technical Details ### Implementation Architecture The signature middleware implements HMAC-based request verification using Go's crypto packages: * **Algorithm Support**: SHA-1 (legacy), SHA-256 (default), SHA-512 * **Encoding Formats**: Hexadecimal (default) and Base64 * **Signature Verification**: Constant-time comparison using `hmac.Equal()` to prevent timing attacks ### Core Components #### Middleware Flow 1. **Path & Method Filtering**: Checks if request should skip validation based on configured paths and methods 2. **Signature Extraction**: Retrieves signature from configured header and validates prefix if specified 3. **Payload Reading**: Uses default body reader or custom `PayloadGetter` function 4. **HMAC Verification**: Computes expected signature and performs constant-time comparison 5. **Context Storage**: Stores verification result in request context for downstream handlers #### Key Functions * `New(secret)`: Creates middleware with default options (SHA-256, hex encoding, X-Signature header) * `WithOptions(opts)`: Creates middleware with custom configuration * `Sign(secret, algo, encoding, payload)`: Generates HMAC signature for given payload * `GetInfo(c)`: Retrieves signature verification information from context * `IsValid(c)`: Quick check if signature was valid #### Provider-Specific Helpers The middleware includes pre-configured functions for popular webhook providers: * **GitHub**: `X-Hub-Signature-256` header with `sha256=` prefix * **Stripe**: `Stripe-Signature` header (simplified implementation) * **Slack**: Custom payload format combining version, timestamp, and body * **Twilio**: SHA-1 with Base64 encoding, URL + sorted form parameters * **AWS**: Signature Version 4 (simplified implementation) ### Security Features * **Constant-Time Comparison**: Uses `hmac.Equal()` to prevent timing attacks * **Body Preservation**: Re-wraps request body after reading for downstream handlers * **Configurable Skip Rules**: Allows excluding specific paths and HTTP methods * **Custom Error Handling**: Supports custom error responses via `ErrorHandler` ## Best Practices * Use SHA-256 or stronger algorithms * Include timestamp to prevent replay attacks * Use secure random secrets (32+ bytes) * Rotate secrets periodically * Log signature failures for security monitoring ## Testing The signature middleware includes comprehensive test coverage for all features and edge cases: | Test Case | Description | Expected Behavior | | ------------------------------- | ---------------------------------------------- | ------------------------------------------------ | | `TestNew` | Basic middleware creation with default options | GET requests skip validation (default behavior) | | `TestValidSignature` | Valid HMAC-SHA256 signature verification | Request accepted with 200 OK | | `TestInvalidSignature` | Invalid signature provided | Request rejected with 401 Unauthorized | | `TestMissingSignature` | No signature header present | Request rejected with 401 Unauthorized | | `TestSignaturePrefix` | Signature with prefix (e.g., "sha256=") | Prefix validated and stripped correctly | | `TestBase64Encoding` | Base64-encoded signature verification | Signature decoded and verified | | `TestSHA1Algorithm` | SHA-1 algorithm (legacy support) | Signature verified with SHA-1 HMAC | | `TestSHA512Algorithm` | SHA-512 algorithm verification | Signature verified with SHA-512 HMAC | | `TestSkipPaths` | Configured paths excluded from validation | Skipped paths accept requests without signatures | | `TestCustomHeaderName` | Custom signature header name | Signature read from custom header | | `TestCustomErrorHandler` | Custom error response handler | Custom error format returned on failure | | `TestOnValid` | Callback function on successful validation | OnValid callback executed | | `TestGetInfo` | Retrieve signature info from context | Info object contains validation details | | `TestIsValid` | Check if signature is valid | Returns true for valid signatures | | `TestGitHub` | GitHub webhook signature format | X-Hub-Signature-256 with sha256= prefix verified | | `TestSign` | Signature generation for various algorithms | Consistent signatures generated for same input | | `TestSigner` | Client-side request signing | Signature header added to outgoing requests | | `TestSignerWithPrefix` | Signer with prefix configuration | Signature includes configured prefix | | `TestSignerNilBody` | Signing request with nil body | Handles nil body gracefully | | `TestStripe` | Stripe webhook signature verification | Stripe-Signature header verified | | `TestSlack` | Slack signature with timestamp format | v0=HMAC(v0:timestamp:body) format verified | | `TestTwilio` | Twilio signature with form parameters | SHA-1 Base64 signature of URL + sorted params | | `TestAWS` | AWS Signature Version 4 (simplified) | Authorization header signature verified | | `TestGetInfo_NoContext` | GetInfo without signature middleware | Returns nil when middleware not used | | `TestIsValid_NoContext` | IsValid without signature middleware | Returns false when middleware not used | | `TestSignaturePrefix_Missing` | Required prefix not present | Request rejected with 401 Unauthorized | | `TestCustomPayloadGetter` | Custom payload extraction function | Uses custom payload for verification | | `TestCustomPayloadGetter_Error` | PayloadGetter returns error | Request rejected with 401 Unauthorized | | `TestSignUnknownAlgorithm` | Unknown algorithm specified | Defaults to SHA-256 | | `TestSkipMethods_Custom` | Custom skip methods configuration | Configured methods skip validation | | `TestSignerSign_Direct` | Direct Sign method on Signer | Signature added with all custom options | ## Related Middlewares * [keyauth](/middlewares/keyauth) - API key authentication * [jwt](/middlewares/jwt) - JWT authentication * [hmac](/middlewares/hmac) - HMAC utilities # Slash Source: https://docs.go-mizu.dev/middlewares/slash Trailing slash handling middleware for consistent URLs. ## Overview The `slash` middleware handles trailing slashes in URLs by adding or removing them with redirects. Use it for URL consistency and SEO. ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/slash" ``` ## Quick Start ```go theme={null} app := mizu.New() // Add trailing slash: /about → /about/ app.Use(slash.Add()) // Remove trailing slash: /about/ → /about app.Use(slash.Remove()) ``` ## Functions | Function | Description | | ------------------ | ------------------------------------ | | `Add()` | Add trailing slash (301 redirect) | | `AddCode(code)` | Add with custom status code | | `Remove()` | Remove trailing slash (301 redirect) | | `RemoveCode(code)` | Remove with custom status code | ## Examples ### Add Trailing Slash ```go theme={null} app.Use(slash.Add()) // /about → 301 → /about/ // /contact → 301 → /contact/ // /about/ → (no redirect) // / → (no redirect) ``` ### Remove Trailing Slash ```go theme={null} app.Use(slash.Remove()) // /about/ → 301 → /about // /contact/ → 301 → /contact // /about → (no redirect) // / → (no redirect) ``` ### Temporary Redirect ```go theme={null} app.Use(slash.AddCode(302)) // Temporary redirect app.Use(slash.RemoveCode(307)) // Preserve HTTP method ``` ## Behavior * Root path `/` is never modified * Query strings are preserved * Uses 301 (Permanent) by default ## Technical Details The slash middleware implements trailing slash normalization through HTTP redirects. It provides four main functions that return middleware handlers: ### Implementation * **Add()**: Wraps `AddCode()` with HTTP 301 (Moved Permanently) status * **AddCode(code int)**: Returns middleware that checks if path lacks trailing slash and redirects to add it * **Remove()**: Wraps `RemoveCode()` with HTTP 301 (Moved Permanently) status * **RemoveCode(code int)**: Returns middleware that checks if path has trailing slash and redirects to remove it ### Internal Logic 1. **Path Extraction**: Reads the request URL path from `c.Request().URL.Path` 2. **Root Protection**: Always skips the root path `/` to prevent infinite redirects 3. **Slash Detection**: Uses `strings.HasSuffix(path, "/")` to check for trailing slash 4. **Target Construction**: Builds redirect URL by adding/removing trailing slash 5. **Query Preservation**: Appends `?` + `c.Request().URL.RawQuery` if query string exists 6. **Redirect Execution**: Calls `c.Redirect(code, target)` with specified status code 7. **Pass-through**: Calls `next(c)` if no redirect needed ### Performance Characteristics * **Zero allocations** for paths that don't need redirection * **Single string concatenation** for building redirect target * **Early exit** for root path and paths already in correct format * **No regular expressions** - uses simple string operations ## Best Practices * Choose one style and be consistent * Use 301 for permanent SEO benefit * Apply early in middleware chain ## Testing The slash middleware includes comprehensive test coverage for all functions and edge cases: | Test Case | Description | Expected Behavior | | ------------------------------- | ------------------------------------------------------ | ----------------------------------------------------- | | Add - adds trailing slash | Request to `/test` with `Add()` middleware | Redirects to `/test/` with 301 status | | Add - preserves query | Request to `/test?foo=bar` with `Add()` middleware | Redirects to `/test/?foo=bar` preserving query string | | Add - skips root | Request to `/` with `Add()` middleware | Returns 200 OK, no redirect performed | | AddCode - custom status | Request to `/test` with `AddCode(307)` middleware | Redirects to `/test/` with 307 status | | Remove - removes trailing slash | Request to `/test/` with `Remove()` middleware | Redirects to `/test` with 301 status | | Remove - preserves query | Request to `/test/?foo=bar` with `Remove()` middleware | Redirects to `/test?foo=bar` preserving query string | | Remove - skips root | Request to `/` with `Remove()` middleware | Returns 200 OK, no redirect performed | | RemoveCode - custom status | Request to `/test/` with `RemoveCode(302)` middleware | Redirects to `/test` with 302 status | ## Related Middlewares * [redirect](/middlewares/redirect) - General redirects * [rewrite](/middlewares/rewrite) - URL rewriting # SPA Source: https://docs.go-mizu.dev/middlewares/spa Single Page Application middleware for client-side routing support. ## Overview The `spa` middleware serves Single Page Applications by returning the index file for any unmatched routes, enabling client-side routing to work correctly. Use it when you need: * React/Vue/Angular app hosting * Client-side routing support * HTML5 pushState routing ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/spa" ``` ## Quick Start ```go theme={null} app := mizu.New() // Serve SPA from ./dist directory app.Use(spa.New("./dist")) // API routes still work app.Get("/api/users", listUsers) ``` ## Configuration ### Options | Option | Type | Default | Description | | ----------- | ---------- | -------------- | ---------------------------- | | `Root` | `string` | Required | Root directory | | `FS` | `fs.FS` | `nil` | Filesystem to serve from | | `Index` | `string` | `"index.html"` | Index file name | | `Prefix` | `string` | `""` | URL prefix | | `SkipPaths` | `[]string` | `[]` | Paths to skip (pass through) | ## Examples ### Basic Usage ```go theme={null} app.Use(spa.New("./dist")) ``` ### From Embedded FS ```go theme={null} //go:embed dist/* var distFS embed.FS app.Use(spa.WithFS(distFS, "dist")) ``` ### Skip API Routes ```go theme={null} app.Use(spa.WithOptions(spa.Options{ Root: "./dist", SkipPaths: []string{"/api", "/health"}, })) ``` ### Custom Index ```go theme={null} app.Use(spa.WithOptions(spa.Options{ Root: "./public", Index: "app.html", })) ``` ### With Prefix ```go theme={null} app.Use(spa.WithOptions(spa.Options{ Root: "./dist", Prefix: "/app", })) // SPA available at /app/* ``` ## How It Works 1. Request comes in for `/users/123` 2. Check if file exists at `./dist/users/123` 3. If file exists: serve it 4. If not: serve `./dist/index.html` 5. Client-side router handles `/users/123` ## API Reference ### Functions ```go theme={null} // New creates SPA middleware for directory func New(root string) mizu.Middleware // WithFS creates SPA middleware for fs.FS func WithFS(fsys fs.FS, root string) mizu.Middleware // WithOptions creates SPA middleware with configuration func WithOptions(opts Options) mizu.Middleware ``` ## Complete Example ```go theme={null} package main import ( "embed" "github.com/go-mizu/mizu" "github.com/go-mizu/mizu/middlewares/spa" ) //go:embed dist/* var distFS embed.FS func main() { app := mizu.New() // API routes api := app.Group("/api") api.Get("/users", listUsers) api.Post("/users", createUser) // Serve SPA for everything else app.Use(spa.WithOptions(spa.Options{ FS: distFS, Root: "dist", SkipPaths: []string{"/api"}, })) app.Listen(":3000") } ``` ## Technical Details ### Implementation Overview The SPA middleware provides a fallback mechanism for single-page applications by serving the index file when a requested file does not exist. This enables client-side routing to work correctly. #### Path Resolution Process 1. **Ignore Path Check**: Checks if the request path matches any configured `IgnorePaths` (default: `/api`) 2. **Prefix Handling**: If a `Prefix` is configured, strips it from the path before file resolution 3. **File Existence Check**: Determines if the requested path corresponds to an actual file: * For `fs.FS`: Uses `fs.Stat()` to check file existence * For directory path: Uses `os.Stat()` with `filepath.Join()` 4. **File Serving**: If file exists and is not a directory, serves it with appropriate cache headers 5. **Index Fallback**: If file doesn't exist, serves the index file (default: `index.html`) #### Cache Control Strategy The middleware implements a dual cache control strategy: * **Static Assets**: Configurable via `MaxAge` option (default: 0/no cache) * Sets `Cache-Control: public, max-age=` header * Recommended: Long cache duration (e.g., 31536000 for 1 year) for versioned assets * **Index File**: Configurable via `IndexMaxAge` option (default: 0) * When `IndexMaxAge = 0`: Sets `Cache-Control: no-cache, no-store, must-revalidate` * When `IndexMaxAge > 0`: Sets `Cache-Control: public, max-age=` * No-cache default ensures users always get latest SPA routing configuration #### File Serving Details The middleware uses `http.ServeContent()` for file delivery, which provides: * Automatic MIME type detection based on file extension * HTTP range request support for partial content * Proper `Last-Modified` header handling * Efficient byte streaming with `io.ReadSeeker` #### Security Considerations * All file paths are cleaned using `filepath.Clean()` to prevent directory traversal attacks * The middleware only serves files from the configured root directory or filesystem * Directory listings are disabled (directories trigger index fallback) ## Best Practices * Define API routes before SPA middleware * Use embedded FS for portable deployments * Skip API and health check paths * Set proper cache headers for static assets * Use long cache durations for static assets with content hashing * Keep index.html uncached to ensure SPA updates are immediately available ## Testing The SPA middleware includes comprehensive test coverage for all configuration scenarios: | Test Case | Description | Expected Behavior | | --------------------------- | -------------------------------------------------------- | ----------------------------------------------------------------------- | | Serve static file | Request for existing static file (`/app.js`) | Returns file content with status 200 | | Fallback to index for route | Request for non-existent route (`/users/123`) | Returns `index.html` content with status 200 | | Root path | Request for root path (`/`) | Returns `index.html` content with status 200 | | Serve from FS | Request for file in embedded FS (`/main.js`) | Returns file content from `fs.FS` | | Fallback from FS | Request for non-existent file in FS (`/dashboard`) | Returns `index.html` from `fs.FS` | | Ignore API path | Request to ignored path (`/api/users`) | Passes through to registered route handler | | Ignore health path | Request to ignored path (`/health`) | Passes through to registered route handler | | Non-ignored path gets SPA | Request to non-ignored path (`/profile`) | Returns SPA index file | | With prefix | Request with configured prefix (`/app/dashboard`) | Strips prefix and serves SPA | | Without prefix | Request without prefix when prefix configured (`/other`) | Passes through to other handlers | | Static asset cache | Request for static file with MaxAge set | Returns file with `Cache-Control: public, max-age=31536000` | | Index no cache | Request for route with IndexMaxAge=0 | Returns index with `Cache-Control: no-cache, no-store, must-revalidate` | | Custom index | Request with custom index file (`app.html`) | Returns custom index instead of `index.html` | | Panics without FS or Root | Create middleware with empty options | Panics with error message | ## Related Middlewares * [static](/middlewares/static) - Static file serving * [favicon](/middlewares/favicon) - Favicon serving * [embed](/middlewares/embed) - Embedded filesystem # SSE Source: https://docs.go-mizu.dev/middlewares/sse Server-Sent Events middleware for real-time server-to-client streaming. ## Overview The `sse` middleware enables Server-Sent Events (SSE) for real-time server-to-client communication. Unlike WebSocket, SSE is unidirectional and uses standard HTTP. ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/sse" ``` ## Quick Start ```go theme={null} app := mizu.New() app.Get("/events", sse.New(func(c *mizu.Ctx, client *sse.Client) { // Send events to client client.SendData("Hello!") // Keep connection open <-client.Done })) ``` ## Configuration | Option | Type | Default | Description | | ------------ | ----- | ------- | ------------------------- | | `BufferSize` | `int` | `10` | Event channel buffer size | | `Retry` | `int` | `3000` | Reconnection time in ms | ## Examples ### Basic Usage ```go theme={null} app.Get("/events", sse.New(func(c *mizu.Ctx, client *sse.Client) { // Send simple data events client.SendData("Event 1") client.SendData("Event 2") // Wait for client to disconnect <-client.Done })) ``` ### Periodic Updates ```go theme={null} app.Get("/time", sse.New(func(c *mizu.Ctx, client *sse.Client) { ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() for { select { case t := <-ticker.C: client.SendData(t.Format(time.RFC3339)) case <-client.Done: return } } })) ``` ### Named Events ```go theme={null} app.Get("/notifications", sse.New(func(c *mizu.Ctx, client *sse.Client) { // Send different event types client.SendEvent("message", "New message received") client.SendEvent("alert", "System alert!") client.SendEvent("update", `{"version": "2.0"}`) <-client.Done })) ``` ### Full Event Control ```go theme={null} app.Get("/events", sse.New(func(c *mizu.Ctx, client *sse.Client) { // Send event with all fields client.Send(&sse.Event{ ID: "1", Event: "notification", Data: "You have a new message", Retry: 5000, }) // Send with ID for resumption client.Send(&sse.Event{ ID: "2", Data: "Important update", }) <-client.Done })) ``` ### Using Broker for Broadcasting ```go theme={null} // Create a broker for managing multiple clients broker := sse.NewBroker() app.Get("/stream", sse.New(func(c *mizu.Ctx, client *sse.Client) { // Register client with broker broker.Register(client) // Wait for disconnect <-client.Done })) // Broadcast to all clients from anywhere app.Post("/broadcast", func(c *mizu.Ctx) error { message := c.FormValue("message") broker.BroadcastData(message) return c.Text(200, "Sent to "+strconv.Itoa(broker.ClientCount())+" clients") }) ``` ### Broadcast Named Events ```go theme={null} broker := sse.NewBroker() // Broadcast different event types broker.BroadcastEvent("chat", `{"user": "alice", "message": "Hello!"}`) broker.BroadcastEvent("typing", `{"user": "bob"}`) broker.BroadcastEvent("presence", `{"user": "carol", "status": "online"}`) ``` ### Custom Options ```go theme={null} app.Get("/events", sse.WithOptions( func(c *mizu.Ctx, client *sse.Client) { // Handler }, sse.Options{ BufferSize: 50, // Larger buffer Retry: 5000, // 5 second retry }, )) ``` ### Resume from Last Event ID ```go theme={null} app.Get("/events", sse.New(func(c *mizu.Ctx, client *sse.Client) { // Check if client is resuming lastID := client.ID if lastID != "" { // Send missed events since lastID missedEvents := getEventsSince(lastID) for _, event := range missedEvents { client.Send(event) } } // Continue with new events for event := range newEvents { client.Send(&sse.Event{ ID: event.ID, Data: event.Data, }) } })) ``` ### Real-time Notifications ```go theme={null} broker := sse.NewBroker() app.Get("/notifications", sse.New(func(c *mizu.Ctx, client *sse.Client) { userID := c.Query("user_id") // Register with user-specific logic broker.Register(client) // Send welcome event client.SendEvent("connected", `{"status": "ok"}`) <-client.Done })) // Send notification from other parts of your app func notifyUser(userID string, message string) { broker.BroadcastEvent("notification", message) } ``` ## Event Structure ```go theme={null} type Event struct { ID string // Event ID (for resumption) Event string // Event type name Data string // Event payload Retry int // Retry time in ms } ``` ## Client Methods ```go theme={null} func (c *Client) Send(event *Event) func (c *Client) SendData(data string) func (c *Client) SendEvent(eventType, data string) func (c *Client) Close() ``` ## Broker Methods ```go theme={null} func NewBroker() *Broker func (b *Broker) Register(client *Client) func (b *Broker) Broadcast(event *Event) func (b *Broker) BroadcastData(data string) func (b *Broker) BroadcastEvent(eventType, data string) func (b *Broker) ClientCount() int ``` ## API Reference ```go theme={null} func New(handler Handler) mizu.Middleware func WithOptions(handler Handler, opts Options) mizu.Middleware func NewBroker() *Broker type Handler func(c *mizu.Ctx, client *Client) ``` ## Client Example (JavaScript) ```javascript theme={null} const events = new EventSource('/events'); // Listen for all events events.onmessage = (e) => { console.log('Data:', e.data); }; // Listen for specific event types events.addEventListener('notification', (e) => { console.log('Notification:', e.data); }); events.addEventListener('update', (e) => { const data = JSON.parse(e.data); console.log('Update:', data); }); events.onerror = () => { console.log('Connection lost, reconnecting...'); }; ``` ## Event Format ``` id: 123 event: notification retry: 5000 data: Hello, World! ``` ## Technical Details ### Architecture The SSE middleware implements the W3C Server-Sent Events specification using Go's HTTP response writer and flusher interfaces. The implementation consists of three main components: 1. **Client**: Represents an individual SSE connection with buffered event channels and disconnection handling 2. **Event Loop**: Goroutine-based event processing that listens for both new events and client disconnection signals 3. **Broker**: Optional multi-client manager for broadcasting events to all connected clients ### Connection Lifecycle 1. **Accept Header Check**: Validates that the client accepts `text/event-stream` or `*/*` 2. **Flusher Verification**: Ensures the response writer supports HTTP flushing for streaming 3. **Header Setup**: Sets required SSE headers (Content-Type, Cache-Control, Connection, X-Accel-Buffering) 4. **Client Creation**: Initializes client with event channel (default buffer: 10) and done channel 5. **Event Loop Start**: Launches goroutine to process events from the channel 6. **Handler Execution**: Runs user-provided handler function 7. **Cleanup**: Waits for client disconnection before returning ### Event Processing Events are formatted according to SSE specification: * **ID field**: Optional event identifier for client resumption (`id: value\n`) * **Event field**: Optional event type name (`event: value\n`) * **Retry field**: Optional reconnection time in milliseconds (`retry: value\n`) * **Data field**: Event payload, split by newlines (`data: value\n` for each line) * **Terminator**: Empty line (`\n`) signals end of event Multiline data is automatically split and prefixed with `data:` for each line. ### Broker Implementation The broker uses a fan-out pattern with three internal channels: * **Register channel**: Adds new clients to the client map * **Unregister channel**: Removes disconnected clients from the map * **Broadcast channel**: Queues events for distribution (buffer: 100) The broker runs a dedicated goroutine that: 1. Manages client registration/unregistration 2. Broadcasts events to all clients without blocking 3. Skips clients with full buffers to prevent blocking ### Concurrency Safety * Client send operations use mutex locks to prevent concurrent writes * Broker uses RWMutex for safe concurrent access to the client map * Done channel is checked before send operations to prevent panics * Multiple Close() calls are safe and non-blocking ### Last-Event-ID Support The middleware reads the `Last-Event-ID` header from reconnecting clients, allowing handlers to resume event streams from a specific point. ## Best Practices * Use event IDs for client resumption * Set appropriate retry intervals * Close connections when done * Use Broker for multi-client scenarios * Keep payload sizes reasonable * Handle client disconnections gracefully ## Testing | Test Case | Description | Expected Behavior | | ------------------------------------ | ---------------------------------------- | --------------------------------------------------------------------------------- | | `TestEvent` | Event structure creation with all fields | Event object correctly stores ID, Event, Data, and Retry fields | | `TestClient_Close` | Client close operation | Close() can be called multiple times safely without panic, Done channel is closed | | `TestClient_Send` | Sending event to client | Event is successfully sent through the Events channel | | `TestClient_SendData` | Sending data-only event | Data is wrapped in Event struct and sent through channel | | `TestClient_SendEvent` | Sending named event with data | Event type and data are correctly packaged and sent | | `TestClient_SendOnClosedChannel` | Sending to closed client | Send operation does not block or panic after client is closed | | `TestClient_send` | Internal send formatting with all fields | Output contains properly formatted id, event, data, and retry fields | | `TestClient_sendMultilineData` | Multiline data formatting | Each line is prefixed with "data:" in the output | | `TestClient_sendEmptyFields` | Send with minimal event data | Empty fields (ID, Event, Retry=0) are omitted from output | | `TestBroker` | Broker client management | Clients register successfully, receive broadcasts, and unregister on disconnect | | `TestBroker_BroadcastEvent` | Broadcasting named events | All registered clients receive the event with correct type and data | | `TestBroker_Broadcast` | Broadcasting full Event object | Event with all fields (ID, Event, Data, Retry) is delivered to clients | | `TestBroker_BroadcastWithFullBuffer` | Broadcast to client with full buffer | Broadcast operation does not block, skips clients with full buffers | | `TestOptions` | Options structure configuration | BufferSize and Retry values are correctly stored | | `TestNew` | Middleware creation with New() | Middleware is successfully created with default options | | `TestWithOptions_DefaultValues` | Middleware with empty options | Default values (BufferSize: 10, Retry: 3000) are applied | | `TestWithOptions_CustomValues` | Middleware with custom options | Custom BufferSize and Retry values are used | | `TestSSE_NonSSERequest` | Non-SSE Accept header | Request passes through to next handler, normal response returned | | `TestSSE_EmptyAcceptAllowed` | Empty Accept header | SSE connection is allowed (treated as accepting all types) | | `TestSSE_WildcardAccept` | Accept: */* header | SSE connection is allowed (wildcard matches event-stream) | ## Related Middlewares * [websocket](/middlewares/websocket) - Bidirectional communication * [timeout](/middlewares/timeout) - Request timeout # Static Source: https://docs.go-mizu.dev/middlewares/static Static file serving middleware for serving files from the filesystem or embedded FS. ## Overview The `static` middleware serves static files from a directory or embedded filesystem. It supports index files, directory browsing, and cache control. Use it when you need: * Serve static assets (CSS, JS, images) * Serve a web application's static files * Serve files from embedded filesystem ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/static" ``` ## Quick Start ```go theme={null} app := mizu.New() // Serve files from ./public directory app.Use(static.New("./public")) // Or serve from embedded filesystem //go:embed static/* var staticFS embed.FS app.Use(static.WithFS(staticFS)) ``` ## Configuration ### Options | Option | Type | Default | Description | | ----------------- | ----------------------- | -------------- | -------------------------------- | | `Root` | `string` | Required | Root directory path | | `FS` | `fs.FS` | `nil` | Filesystem to serve from | | `Prefix` | `string` | `""` | URL prefix for static files | | `Index` | `string` | `"index.html"` | Default index file | | `Browse` | `bool` | `false` | Enable directory browsing | | `MaxAge` | `int` | `0` | Cache-Control max-age in seconds | | `NotFoundHandler` | `func(*mizu.Ctx) error` | `nil` | Custom 404 handler | ## Examples ### Basic File Serving ```go theme={null} app.Use(static.New("./public")) ``` ### With URL Prefix ```go theme={null} app.Use(static.WithOptions(static.Options{ Root: "./public", Prefix: "/static", })) // Files available at /static/css/style.css, etc. ``` ### From Embedded Filesystem ```go theme={null} //go:embed static/* var staticFS embed.FS app.Use(static.WithFS(staticFS)) ``` ### With Cache Control ```go theme={null} app.Use(static.WithOptions(static.Options{ Root: "./public", MaxAge: 86400, // 1 day })) ``` ### Directory Browsing ```go theme={null} app.Use(static.WithOptions(static.Options{ Root: "./files", Browse: true, })) ``` ### Custom Index File ```go theme={null} app.Use(static.WithOptions(static.Options{ Root: "./public", Index: "default.htm", })) ``` ### Custom 404 Handler ```go theme={null} app.Use(static.WithOptions(static.Options{ Root: "./public", NotFoundHandler: func(c *mizu.Ctx) error { return c.Text(404, "File not found") }, })) ``` ### Multiple Static Directories ```go theme={null} // Serve assets with prefix app.Use(static.WithOptions(static.Options{ Root: "./assets", Prefix: "/assets", })) // Serve uploads with different prefix app.Use(static.WithOptions(static.Options{ Root: "./uploads", Prefix: "/uploads", })) ``` ## API Reference ### Functions ```go theme={null} // New creates static middleware for a directory func New(root string) mizu.Middleware // WithFS creates static middleware for an fs.FS func WithFS(fsys fs.FS) mizu.Middleware // WithOptions creates static middleware with full configuration func WithOptions(opts Options) mizu.Middleware ``` ## Behavior * Falls through to next handler if file not found (unless NotFoundHandler set) * Automatically serves `index.html` for directory requests * Sets appropriate Content-Type based on file extension * Supports conditional requests with If-Modified-Since ## Technical Details ### Implementation Overview The static middleware serves files from either a local filesystem directory or an embedded `fs.FS` interface. It implements intelligent path resolution and content serving with the following key features: #### Path Resolution * Strips URL prefix if configured * Cleans and normalizes file paths using `filepath.Clean` * Converts URL paths to filesystem paths safely * Checks file existence before serving #### File Serving Strategy The middleware uses two different serving approaches: 1. **Direct File Serving** (default) * Uses `http.ServeContent` for individual files * Provides proper Content-Type detection based on file extension * Supports HTTP range requests for partial content * Handles conditional requests with If-Modified-Since headers * Sets Last-Modified based on file modification time 2. **FileServer Mode** (directory browsing enabled) * Uses `http.FileServer` when `Browse: true` * Generates HTML directory listings automatically * Ensures directory paths end with `/` to prevent redirects #### Index File Handling When a directory is requested: 1. Checks if an index file exists (default: `index.html`) 2. Serves the index file if found 3. If not found and browsing disabled: calls NotFoundHandler or falls through to next middleware 4. If not found and browsing enabled: shows directory listing #### Cache Control * Sets `Cache-Control: public, max-age=` when `MaxAge > 0` * No caching headers by default (`MaxAge: 0`) * Relies on HTTP standard caching mechanisms #### Filesystem Abstraction The middleware supports two filesystem sources: * **FS (fs.FS)**: Takes precedence if set, ideal for embedded filesystems * **Root (string)**: Uses OS filesystem with the specified root directory #### Error Handling * Returns 404 for non-existent files (via custom handler or next middleware) * Returns 500 for internal file stat errors * Safely handles file close operations with deferred cleanup #### Performance Optimizations * Custom `itoa` function to avoid allocations for Cache-Control header * Direct file serving without unnecessary FileServer overhead * Minimal path manipulation and string operations ## Best Practices * Use embedded filesystem for portable deployments * Set appropriate `MaxAge` for caching static assets * Use versioned filenames for cache busting * Disable directory browsing in production ## Testing The static middleware includes comprehensive test coverage for all major functionality: | Test Case | Description | Expected Behavior | | ----------------------------------------------------- | ------------------------------------------------- | -------------------------------------------------- | | `TestNew/serve file` | Serves a regular file from filesystem | Returns 200 OK with correct file content | | `TestNew/serve index` | Serves index.html for root directory request | Returns 200 OK with index.html content | | `TestNew/file not found falls through` | Handles missing files when no NotFoundHandler set | Falls through to next middleware handler | | `TestWithFS/serve file from FS` | Serves files from fs.FS interface | Returns 200 OK with file content from embedded FS | | `TestWithOptions_Prefix/with prefix` | Serves files with URL prefix stripped | Returns 200 OK for paths matching prefix | | `TestWithOptions_Prefix/without prefix falls through` | Handles requests not matching prefix | Falls through to next middleware | | `TestWithOptions_MaxAge` | Sets cache control headers | Returns Cache-Control header with max-age value | | `TestWithOptions_NotFoundHandler` | Uses custom 404 handler for missing files | Calls custom handler returning custom 404 response | | `TestWithOptions_CustomIndex` | Serves custom index file (e.g., default.htm) | Returns 200 OK with custom index file content | | `TestWithOptions_Browse` | Enables directory browsing | Returns 200 OK with directory listing HTML | | `TestWithOptions_Panics` | Validates configuration on creation | Panics when neither FS nor Root is provided | ## Related Middlewares * [spa](/middlewares/spa) - Single Page Application support * [favicon](/middlewares/favicon) - Favicon serving * [embed](/middlewares/embed) - Embedded filesystem helpers * [cache](/middlewares/cache) - Cache control headers # Surrogate Source: https://docs.go-mizu.dev/middlewares/surrogate CDN surrogate headers middleware for cache control. ## Overview The `surrogate` middleware manages Surrogate-Control and Surrogate-Key headers for CDN cache control (Fastly, Varnish, etc.). Use it when you need: * CDN cache purging * Edge cache control * Cache tagging ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/surrogate" ``` ## Quick Start ```go theme={null} app := mizu.New() app.Use(surrogate.New()) app.Get("/article/:id", func(c *mizu.Ctx) error { surrogate.Key(c, "article-"+c.Param("id")) return c.JSON(200, article) }) ``` ## Configuration ### Options | Option | Type | Default | Description | | ---------------------- | --------------- | ------- | ---------------------- | | `MaxAge` | `time.Duration` | `0` | Surrogate max-age | | `StaleWhileRevalidate` | `time.Duration` | `0` | Stale-while-revalidate | ## Examples ### Set Surrogate Keys ```go theme={null} app.Get("/article/:id", func(c *mizu.Ctx) error { surrogate.Key(c, "article-"+c.Param("id")) surrogate.Key(c, "articles") return c.JSON(200, article) }) // Surrogate-Key: article-123 articles ``` ### Surrogate Control ```go theme={null} app.Use(surrogate.WithOptions(surrogate.Options{ MaxAge: time.Hour, StaleWhileRevalidate: 10 * time.Minute, })) // Surrogate-Control: max-age=3600, stale-while-revalidate=600 ``` ### Per-Route Control ```go theme={null} app.Get("/dynamic", func(c *mizu.Ctx) error { surrogate.Control(c, "no-store") return c.JSON(200, data) }) app.Get("/static", func(c *mizu.Ctx) error { surrogate.Control(c, "max-age=86400") return c.JSON(200, data) }) ``` ## API Reference ### Functions ```go theme={null} // New creates surrogate middleware func New() mizu.Middleware // WithOptions creates with configuration func WithOptions(opts Options) mizu.Middleware // Key adds surrogate key func Key(c *mizu.Ctx, keys ...string) // Control sets surrogate control func Control(c *mizu.Ctx, control string) ``` ## CDN Integration ### Fastly ``` Surrogate-Key: article-123 homepage Surrogate-Control: max-age=3600 ``` ### Varnish Uses same headers with xkey support. ## Technical Details The surrogate middleware implements CDN cache control through a context-based key management system: ### Architecture * **Context Storage**: Uses Go's `context.Context` to store surrogate keys during request processing * **Keys Structure**: Maintains a `Keys` type that holds a slice of string keys * **Header Management**: Automatically sets headers after handler execution using middleware pattern ### Implementation Details 1. **Initialization**: The middleware creates a `Keys` instance and stores it in the request context 2. **Default Keys**: If configured, default keys are added to every response 3. **Handler Execution**: The next handler in the chain executes, allowing route handlers to add keys via `Add()` 4. **Header Setting**: After handler completion, the middleware: * Joins all keys with spaces and sets the configured header (default: `Surrogate-Key`) * Builds and sets the `Surrogate-Control` header based on options (MaxAge, StaleWhileRevalidate, StaleIfError) ### Control Header Format The `Surrogate-Control` header is built from options: * `max-age=N`: Set via `Options.MaxAge` * `stale-while-revalidate=N`: Set via `Options.StaleWhileRevalidate` * `stale-if-error=N`: Set via `Options.StaleIfError` All directives are combined with comma-space separation. ### CDN Presets The middleware includes three CDN-specific presets: * **Fastly()**: Uses `Surrogate-Key` header * **Varnish()**: Uses `xkey` header * **CloudFront()**: Uses `x-amz-meta-surrogate-key` header Each preset configures the appropriate headers for the CDN while maintaining the same API. ## Best Practices * Use meaningful, granular keys * Group related content with shared keys * Set appropriate TTLs * Plan purge strategy ## Testing The surrogate middleware includes comprehensive test coverage for all functionality: | Test Case | Description | Expected Behavior | | ------------------------------ | ------------------------------------------------- | ----------------------------------------------------------------------- | | `TestNew` | Basic middleware initialization with key addition | Adds multiple keys (`page`, `content`) and sets `Surrogate-Key` header | | `TestWithOptions_DefaultKeys` | Default keys configuration | Automatically includes default keys (`site`, `global`) in all responses | | `TestWithOptions_CustomHeader` | Custom header name | Uses custom header name (`xkey`) instead of default `Surrogate-Key` | | `TestGet` | Retrieve Keys from context | Returns the `Keys` instance from context and allows manipulation | | `TestAdd` | Add multiple keys in handler | Adds multiple keys (`key1`, `key2`) and includes both in header | | `TestClear` | Clear all keys including defaults | Removes all keys, resulting in no `Surrogate-Key` header | | `TestWithKeys` | Convenience function for default keys | Creates middleware with default keys using `WithKeys()` helper | | `TestFastly` | Fastly CDN preset | Uses `Surrogate-Key` header with Fastly configuration | | `TestVarnish` | Varnish CDN preset | Uses `xkey` header for Varnish compatibility | | `TestNoKeys` | No keys added scenario | Does not set `Surrogate-Key` header when no keys are added | | `TestKeysCleared` | Keys.Clear() method | Properly clears keys from the Keys struct | ## Related Middlewares * [cache](/middlewares/cache) - Cache-Control headers * [etag](/middlewares/etag) - ETag generation * [vary](/middlewares/vary) - Vary headers # Throttle Source: https://docs.go-mizu.dev/middlewares/throttle Request throttling middleware for controlling concurrent request processing. ## Overview The `throttle` middleware limits the number of concurrent requests being processed, ensuring your application doesn't get overwhelmed by too many simultaneous operations. Use it when you need: * Limit concurrent request processing * Prevent resource exhaustion * Control server load * Queue requests during high traffic ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/throttle" ``` ## Quick Start ```go theme={null} app := mizu.New() // Allow max 100 concurrent requests app.Use(throttle.New(100)) ``` ## Configuration ### Options | Option | Type | Default | Description | | ------------ | ----------------- | ------- | ------------------------------------------ | | `Limit` | `int` | `100` | Maximum concurrent requests | | `Backlog` | `int` | `1000` | Max queued requests waiting for a slot | | `BacklogSet` | `bool` | `false` | Internal flag for explicit backlog setting | | `Timeout` | `time.Duration` | `30s` | Max wait time for a slot | | `OnThrottle` | `func(*mizu.Ctx)` | `nil` | Callback when request is throttled | ## Examples ### Simple Throttle ```go theme={null} // Allow max 100 concurrent requests app.Use(throttle.New(100)) ``` ### With Backlog ```go theme={null} // 50 concurrent with 200 waiting in queue app.Use(throttle.WithOptions(throttle.Options{ Limit: 50, Backlog: 200, })) ``` ### With Timeout ```go theme={null} // Wait max 10 seconds for a slot app.Use(throttle.WithOptions(throttle.Options{ Limit: 50, Timeout: 10 * time.Second, })) ``` ### No Backlog (Immediate Rejection) ```go theme={null} // Reject immediately when all slots busy app.Use(throttle.WithOptions(throttle.Options{ Limit: 100, Backlog: 0, BacklogSet: true, // Required to use Backlog: 0 })) ``` ### With OnThrottle Callback ```go theme={null} app.Use(throttle.WithOptions(throttle.Options{ Limit: 100, OnThrottle: func(c *mizu.Ctx) { log.Printf("Request throttled: %s", c.Request().URL.Path) }, })) ``` ## API Reference ### Functions ```go theme={null} // New creates throttle with concurrent request limit func New(limit int) mizu.Middleware // WithOptions creates throttle with configuration func WithOptions(opts Options) mizu.Middleware // Concurrency is an alias for New func Concurrency(limit int) mizu.Middleware ``` ## Technical Details ### Implementation The throttle middleware uses a semaphore-based approach to control concurrency: 1. **Semaphore**: A buffered channel (`chan struct{}`) acts as a semaphore with capacity equal to the `Limit`. Each slot represents permission to process one request concurrently. 2. **Request Flow**: * When a request arrives, the middleware attempts to acquire a slot from the semaphore (non-blocking) * If a slot is available immediately, the request proceeds * If no slot is available, the request checks the backlog capacity 3. **Backlog Queue**: * A counter tracks the number of requests waiting for a slot * If backlog capacity is reached, new requests are rejected immediately with `503 Service Unavailable` * Requests in the backlog wait with a timeout for a slot to become available 4. **Timeout Handling**: * Waiting requests use `time.NewTimer` with the configured `Timeout` * Three exit conditions: slot acquired, timeout reached, or request context cancelled * On timeout or cancellation, the backlog counter is decremented 5. **Thread Safety**: A `sync.Mutex` protects the backlog counter to prevent race conditions. ### Default Values * `Limit`: 100 concurrent requests * `Backlog`: 1000 waiting requests * `Timeout`: 30 seconds ### Error Responses * `503 Service Unavailable` with "service busy" - backlog capacity exceeded * `503 Service Unavailable` with "request timeout" - timeout waiting for slot ## Throttle vs Rate Limit | Feature | Throttle | Rate Limit | | -------- | ------------------- | ------------------------ | | Purpose | Limit concurrency | Limit request rate | | Behavior | Queues requests | Rejects excess | | Metric | Concurrent requests | Requests per time window | | Use case | Resource protection | Abuse prevention | ## Best Practices * Use for upstream service protection * Set reasonable wait timeouts * Monitor queue depths * Combine with rate limiting for full control * Set `BacklogSet: true` when using `Backlog: 0` to disable queueing * Use `OnThrottle` callback for monitoring and metrics collection ## Testing | Test Case | Description | Expected Behavior | | ---------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | | `TestNew` | Tests basic concurrency limiting with limit of 2 | Maximum concurrent requests never exceeds the configured limit (2) | | `TestWithOptions_Backlog` | Tests backlog queue with limit=1, backlog=1 | With 3 concurrent requests, at least one should be rejected with 503 Service Unavailable | | `TestWithOptions_Timeout` | Tests timeout functionality with 50ms timeout and slow handler (200ms) | Second request should timeout waiting for slot and return 503 Service Unavailable | | `TestWithOptions_OnThrottle` | Tests OnThrottle callback with zero backlog | OnThrottle callback is invoked when a request is throttled/rejected | | `TestConcurrency` | Tests the Concurrency alias function | Concurrency() alias works identically to New() and processes requests successfully | ## Related Middlewares * [ratelimit](/middlewares/ratelimit) - Rate limiting * [bulkhead](/middlewares/bulkhead) - Concurrency isolation * [adaptive](/middlewares/adaptive) - Adaptive rate limiting # Timeout Source: https://docs.go-mizu.dev/middlewares/timeout Request timeout middleware for preventing long-running requests. ## Overview The `timeout` middleware enforces a maximum duration for request processing. If a handler takes too long, the request is cancelled and an error response is returned. ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/timeout" ``` ## Quick Start ```go theme={null} app := mizu.New() // 30 second timeout app.Use(timeout.New(30 * time.Second)) ``` ## Configuration | Option | Type | Default | Description | | -------------- | ----------------------- | ----------------------- | ------------------------ | | `Timeout` | `time.Duration` | `30s` | Maximum request duration | | `ErrorHandler` | `func(*mizu.Ctx) error` | - | Custom timeout handler | | `ErrorMessage` | `string` | `"Service Unavailable"` | Error message | ## Examples ### Basic Timeout ```go theme={null} app.Use(timeout.New(30 * time.Second)) ``` ### Custom Error Handler ```go theme={null} app.Use(timeout.WithOptions(timeout.Options{ Timeout: 10 * time.Second, ErrorHandler: func(c *mizu.Ctx) error { return c.JSON(504, map[string]string{ "error": "Request timed out", }) }, })) ``` ### Different Timeouts Per Route ```go theme={null} // Global: 30s app.Use(timeout.New(30 * time.Second)) // Slow endpoint: 5 minutes app.Post("/export", exportHandler, timeout.New(5*time.Minute)) // Fast endpoint: 5 seconds app.Get("/health", healthHandler, timeout.New(5*time.Second)) ``` ### Check Context in Handler ```go theme={null} app.Get("/data", func(c *mizu.Ctx) error { for i := 0; i < 100; i++ { select { case <-c.Context().Done(): return c.Context().Err() // Timeout reached default: processItem(i) } } return c.JSON(200, results) }) ``` ## API Reference ```go theme={null} func New(timeout time.Duration) mizu.Middleware func WithOptions(opts Options) mizu.Middleware ``` ## How It Works 1. Creates a context with deadline 2. Runs handler in goroutine 3. Returns whichever completes first: * Handler completion * Timeout expiration (returns 503) ## Technical Details ### Implementation The timeout middleware uses Go's `context.WithTimeout` to enforce request deadlines: 1. **Context Creation**: Creates a context with deadline using `context.WithTimeout(c.Context(), opts.Timeout)` 2. **Request Update**: Replaces the request's context with the timeout context 3. **Goroutine Execution**: Runs the next handler in a separate goroutine to enable timeout detection 4. **Channel Communication**: Uses a buffered channel to receive the handler's result 5. **Select Statement**: Races between handler completion and context cancellation * If handler completes first, returns the result * If timeout occurs first, calls error handler or returns 503 Service Unavailable ### Default Values * **Timeout Duration**: 30 seconds (when `Timeout <= 0`) * **Error Message**: "Service Unavailable" * **HTTP Status**: 503 Service Unavailable (when no custom error handler is provided) ### Concurrency Safety The middleware safely handles concurrent requests by: * Using a buffered channel (size 1) to prevent goroutine leaks * Properly deferring the context cancel function to release resources * Isolating each request's timeout context ## Best Practices * Set reasonable timeouts based on expected duration * Use longer timeouts for file uploads/downloads * Check context in long-running operations * Return 503 (Service Unavailable) on timeout ## Testing ### Test Cases | Test Case | Description | Expected Behavior | | --------------------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------- | | `TestNew/times out slow request` | Handler takes longer than timeout (200ms vs 50ms) | Returns 503 Service Unavailable status | | `TestNew/allows fast request` | Handler completes before timeout | Returns 200 OK with correct response body | | `TestWithOptions_ErrorHandler` | Custom error handler is provided | Calls custom error handler, returns 504 Gateway Timeout with JSON error | | `TestWithOptions_CustomMessage` | Custom error message is set | Returns custom message "Request took too long" in response body | | `TestWithOptions_DefaultTimeout` | No timeout specified in options | Creates middleware with 30 second default timeout | | `TestTimeout_ContextCancellation` | Handler checks context cancellation | Context is cancelled when timeout occurs, handler can detect via `c.Context().Done()` | ## Related Middlewares * [recover](/middlewares/recover) - Panic recovery * [circuitbreaker](/middlewares/circuitbreaker) - Failure handling # Timezone Source: https://docs.go-mizu.dev/middlewares/timezone Timezone detection middleware for time localization. ## Overview The `timezone` middleware detects user timezone from headers, cookies, or client hints for proper time display. Use it when you need: * Time localization * Timezone-aware scheduling * User time preferences ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/timezone" ``` ## Quick Start ```go theme={null} app := mizu.New() app.Use(timezone.New()) app.Get("/time", func(c *mizu.Ctx) error { tz := timezone.Get(c) loc, _ := time.LoadLocation(tz) now := time.Now().In(loc) return c.JSON(200, map[string]string{ "timezone": tz, "time": now.Format(time.RFC3339), }) }) ``` ## Configuration ### Options | Option | Type | Default | Description | | ------------ | -------- | -------------- | ---------------- | | `Default` | `string` | `"UTC"` | Default timezone | | `Header` | `string` | `"X-Timezone"` | Timezone header | | `CookieName` | `string` | `"tz"` | Cookie name | ## Examples ### Basic Detection ```go theme={null} app.Use(timezone.New()) ``` ### Custom Header ```go theme={null} app.Use(timezone.New(timezone.Options{ Header: "X-User-Timezone", })) ``` ### With Default ```go theme={null} app.Use(timezone.New(timezone.Options{ Default: "America/New_York", })) ``` ### Client-Side Detection ```javascript theme={null} // Set timezone cookie document.cookie = `tz=${Intl.DateTimeFormat().resolvedOptions().timeZone}`; ``` ```go theme={null} app.Use(timezone.New(timezone.Options{ CookieName: "tz", })) ``` ## API Reference ### Functions ```go theme={null} // New creates timezone middleware func New(opts ...Options) mizu.Middleware // Get returns detected timezone func Get(c *mizu.Ctx) string // Location returns time.Location func Location(c *mizu.Ctx) *time.Location ``` ## Common Timezones | Timezone | Description | | ------------------ | -------------------------- | | `UTC` | Coordinated Universal Time | | `America/New_York` | Eastern Time | | `Europe/London` | UK Time | | `Asia/Tokyo` | Japan Time | ## Technical Details ### Implementation The timezone middleware uses a multi-source detection strategy to identify the user's timezone: 1. **Context Storage**: Timezone information is stored in the request context using a type-safe context key 2. **Info Structure**: Contains three key pieces of information: * `Name`: IANA timezone identifier (e.g., "America/New\_York") * `Location`: Parsed `time.Location` object for time conversions * `Offset`: UTC offset in seconds for the current time 3. **Lookup Order**: Configurable detection precedence via the `Lookup` option (default: "header,cookie,query") 4. **Validation**: Invalid timezone names automatically fall back to the configured default 5. **Cookie Management**: Optional automatic cookie setting with configurable max age (default: 30 days) ### Detection Flow ``` Request → Check Header → Check Cookie → Check Query → Default → Load Location → Store in Context ``` ### Helper Functions The middleware provides several convenience functions: * `Get(c)`: Returns complete timezone info * `Location(c)`: Returns `time.Location` for time operations * `Name(c)`: Returns timezone name string * `Offset(c)`: Returns UTC offset in seconds * `Now(c)`: Returns current time in detected timezone ### Convenience Constructors * `FromHeader(header)`: Only check specific header * `FromCookie(name)`: Only check specific cookie * `WithDefault(tz)`: Set custom default timezone ## Best Practices * Default to UTC for storage * Convert to user timezone for display * Use IANA timezone names * Detect via JavaScript for accuracy ## Testing The middleware includes comprehensive test coverage for all functionality: | Test Case | Description | Expected Behavior | | --------------------------------- | --------------------------------------- | ------------------------------------------------------------------ | | `TestNew` | Default middleware creation | Returns UTC timezone when no timezone is detected | | `TestWithOptions_FromHeader` | Timezone detection from header | Reads timezone from X-Timezone header (America/New\_York) | | `TestWithOptions_FromCookie` | Timezone detection from cookie | Reads timezone from cookie named "timezone" (Europe/London) | | `TestWithOptions_FromQuery` | Timezone detection from query parameter | Reads timezone from "tz" query parameter (Asia/Tokyo) | | `TestWithOptions_SetCookie` | Cookie setting functionality | Automatically sets timezone cookie when SetCookie=true | | `TestWithOptions_InvalidTimezone` | Invalid timezone handling | Falls back to UTC default when invalid timezone provided | | `TestWithOptions_CustomDefault` | Custom default timezone | Uses America/Los\_Angeles as default instead of UTC | | `TestLocation` | Location helper function | Returns correct time.Location object (Europe/Paris) | | `TestName` | Name helper function | Returns timezone name string (Australia/Sydney) | | `TestOffset` | Offset helper function | Returns correct UTC offset in seconds (0 for UTC) | | `TestNow` | Now helper function | Returns current time in detected timezone location | | `TestFromHeader` | FromHeader constructor | Creates middleware that only reads from custom header (X-User-Tz) | | `TestFromCookie` | FromCookie constructor | Creates middleware that only reads from custom cookie (user\_tz) | | `TestWithDefault` | WithDefault constructor | Creates middleware with custom default (Asia/Singapore) | | `TestLookupPrecedence` | Lookup order precedence | Header takes precedence over cookie and query when all are present | ## Related Middlewares * [language](/middlewares/language) - Language detection # Timing Source: https://docs.go-mizu.dev/middlewares/timing Server-Timing header middleware for performance monitoring. ## Overview The `timing` middleware adds Server-Timing headers to responses, allowing you to measure and report server-side performance metrics. These metrics are visible in browser DevTools. ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/timing" ``` ## Quick Start ```go theme={null} app := mizu.New() app.Use(timing.New()) ``` ## Examples ### Basic Usage ```go theme={null} app.Use(timing.New()) app.Get("/", func(c *mizu.Ctx) error { // Total time is automatically tracked return c.JSON(200, data) }) // Response header: Server-Timing: total;dur=12.34 ``` ### Track Custom Metrics ```go theme={null} app.Get("/users", func(c *mizu.Ctx) error { // Track database query time start := time.Now() users, err := db.GetUsers() timing.Add(c, "db", time.Since(start), "Database query") // Track serialization time start = time.Now() data := serialize(users) timing.Add(c, "serialize", time.Since(start), "JSON serialization") return c.JSON(200, data) }) // Server-Timing: total;dur=45.67, db;dur=30.12;desc="Database query", serialize;dur=5.23;desc="JSON serialization" ``` ### Using Start/Stop Pattern ```go theme={null} app.Get("/data", func(c *mizu.Ctx) error { // Start a timing measurement stop := timing.Start(c, "external_api") resp, err := http.Get("https://api.example.com/data") if err != nil { return err } defer resp.Body.Close() // Stop and record with description stop("External API call") return c.JSON(200, resp.Body) }) ``` ### Using Track Helper ```go theme={null} app.Get("/compute", func(c *mizu.Ctx) error { var result int // Track a function call timing.Track(c, "calculation", func() { result = expensiveCalculation() }) return c.JSON(200, map[string]int{"result": result}) }) ``` ### Multiple Metrics ```go theme={null} app.Get("/dashboard", func(c *mizu.Ctx) error { // Track multiple operations stopAuth := timing.Start(c, "auth") user := authenticateUser(c) stopAuth("Authentication") stopData := timing.Start(c, "data") dashboard := loadDashboard(user.ID) stopData("Load dashboard data") stopRender := timing.Start(c, "render") html := renderTemplate(dashboard) stopRender("Template rendering") return c.HTML(200, html) }) // Server-Timing: total;dur=120.50, auth;dur=15.20;desc="Authentication", data;dur=80.30;desc="Load dashboard data", render;dur=25.00;desc="Template rendering" ``` ## API Reference ```go theme={null} func New() mizu.Middleware func Add(c *mizu.Ctx, name string, duration time.Duration, description string) func Start(c *mizu.Ctx, name string) func(description string) func Track(c *mizu.Ctx, name string, fn func()) ``` ### Functions | Function | Description | | --------- | ----------------------------------------------------------- | | `New()` | Creates the timing middleware | | `Add()` | Adds a metric with name, duration, and optional description | | `Start()` | Starts timing and returns a stop function | | `Track()` | Times a function execution | ## Response Header Format ``` Server-Timing: ;dur=;desc="" ``` Multiple metrics are comma-separated: ``` Server-Timing: total;dur=150.00, db;dur=45.20;desc="Query users", cache;dur=2.10;desc="Redis lookup" ``` ## Viewing in Browser Open DevTools → Network → Select request → Timing tab to see: * Total server processing time * Individual metric breakdowns * Descriptions for each metric ## Technical Details ### Implementation Architecture The timing middleware uses a context-based storage mechanism to track performance metrics throughout the request lifecycle. #### Core Components 1. **timingData Structure** * Thread-safe metric storage using `sync.Mutex` * Tracks request start time * Maintains a slice of custom metrics 2. **Context Integration** * Uses typed context key for type-safe storage * Stores timing data in request context * Enables metric collection across handler chain 3. **Metric Recording** * Metrics stored as `{name, duration, description}` tuples * Total duration calculated from request start time * All durations converted to milliseconds (microseconds/1000) #### Header Generation The middleware builds the Server-Timing header after handler execution: * Total time is always included first * Custom metrics appended in order of addition * Format: `name;dur=;desc=""` * Multiple metrics joined with `, ` separator #### Thread Safety All metric operations are protected by mutex locks: * `Add()` acquires lock before appending metrics * Header generation locks before reading metrics * Safe for concurrent metric recording ## Best Practices * Track database queries, cache lookups, and external API calls * Use descriptive names for metrics * Add descriptions for clarity * Keep metric names short and consistent * Use the Start/Stop pattern for cleaner code ## Testing ### Test Coverage | Test Case | Description | Expected Behavior | | ------------------------- | ----------------------------------------------- | --------------------------------------------------------------------------- | | `TestNew` | Basic middleware functionality | Sets Server-Timing header with total duration after 10ms sleep | | `TestAdd` | Adding custom metrics with/without descriptions | Includes "db;dur=50" with description and "cache;dur=5" without description | | `TestStart` | Start/Stop pattern for timing operations | Records operation duration (\~20ms) with "Long operation" description | | `TestTrack` | Track helper for timing function execution | Records compute duration (\~15ms) without description | | `TestAdd_NoMiddleware` | Graceful handling when middleware not installed | Add() doesn't panic, returns successfully | | `TestNew_MultipleMetrics` | Multiple metrics in single request | Header contains 4 entries (total + 3 custom metrics) | ## Related Middlewares * [requestid](/middlewares/requestid) - Request tracing * [timeout](/middlewares/timeout) - Request timeout # Trace Source: https://docs.go-mizu.dev/middlewares/trace Distributed tracing context middleware for observability. ## Overview The `trace` middleware propagates distributed tracing context (trace ID, span ID) across service boundaries for observability. Use it when you need: * Distributed tracing * Request correlation * Service observability ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/trace" ``` ## Quick Start ```go theme={null} app := mizu.New() app.Use(trace.New()) app.Get("/api", func(c *mizu.Ctx) error { traceID := trace.ID(c) log.Printf("Trace: %s", traceID) return c.JSON(200, data) }) ``` ## Configuration ### Options | Option | Type | Default | Description | | -------------- | --------------- | --------------- | ------------------ | | `TraceHeader` | `string` | `"X-Trace-ID"` | Trace ID header | | `SpanHeader` | `string` | `"X-Span-ID"` | Span ID header | | `ParentHeader` | `string` | `"X-Parent-ID"` | Parent span header | | `Generator` | `func() string` | UUID | ID generator | ## Examples ### Basic Tracing ```go theme={null} app.Use(trace.New()) ``` ### W3C Trace Context ```go theme={null} app.Use(trace.W3C()) // Uses traceparent header ``` ### Custom Headers ```go theme={null} app.Use(trace.WithOptions(trace.Options{ TraceHeader: "X-Request-ID", SpanHeader: "X-Span-ID", ParentHeader: "X-Parent-Span-ID", })) ``` ### Access Trace Info ```go theme={null} app.Get("/", func(c *mizu.Ctx) error { traceID := trace.ID(c) spanID := trace.SpanID(c) parentID := trace.ParentID(c) // Include in logs log.Printf("[%s] Request processed", traceID) return c.JSON(200, data) }) ``` ### Propagate to Downstream ```go theme={null} app.Get("/", func(c *mizu.Ctx) error { // Create HTTP client with trace context req, _ := http.NewRequest("GET", "http://service-b/api", nil) trace.Inject(c, req.Header) resp, _ := http.DefaultClient.Do(req) // ... }) ``` ## API Reference ### Functions ```go theme={null} // New creates trace middleware func New() mizu.Middleware // W3C creates W3C trace context middleware func W3C() mizu.Middleware // WithOptions creates with configuration func WithOptions(opts Options) mizu.Middleware // Context access func ID(c *mizu.Ctx) string func SpanID(c *mizu.Ctx) string func ParentID(c *mizu.Ctx) string // Propagation func Inject(c *mizu.Ctx, headers http.Header) ``` ## Technical Details ### Implementation Architecture The trace middleware implements distributed tracing through: 1. **Context Propagation**: Uses Go's `context.Context` to store span information throughout the request lifecycle 2. **Span Generation**: Creates unique trace and span IDs using cryptographic randomness (16 bytes encoded as hex) 3. **Header-based Propagation**: Extracts and injects trace context via HTTP headers for cross-service communication ### Core Components **Span Structure** * `TraceID`: Unique identifier for the entire trace across services * `SpanID`: Unique identifier for this specific operation * `ParentID`: Reference to the parent span for hierarchy * `StartTime/EndTime`: Timing information for duration calculation * `Status`: Enumerated status (Unset, OK, Error) * `Tags`: Key-value metadata for context * `Events`: Timestamped events within the span **ID Generation** Uses `crypto/rand` to generate 16-byte random values, encoded as 32-character hexadecimal strings, ensuring globally unique identifiers with negligible collision probability. **Span Lifecycle** 1. Extract or generate trace ID from request headers 2. Create new span with generated span ID 3. Store span in request context 4. Set response headers for downstream propagation 5. Execute handler chain 6. Calculate duration and set final status 7. Invoke OnSpan callback if configured **Collector Pattern** The `Collector` provides a simple way to aggregate spans for testing or custom backends: * Thread-safe span collection * In-memory storage * Clear method for test isolation ### Configuration Defaults | Field | Default Value | Purpose | | -------------- | ---------------- | ------------------------------------- | | `ServiceName` | `"mizu-service"` | Identifies the service in traces | | `TraceHeader` | `"X-Trace-ID"` | HTTP header for trace ID | | `ParentHeader` | `"X-Parent-ID"` | HTTP header for parent span ID | | `OnSpan` | `nil` | Optional callback for completed spans | | `Sampler` | `nil` | When nil, all requests are traced | ## Best Practices * Use consistent format across services * Include trace ID in all logs * Propagate to all downstream calls * Consider W3C standard for interoperability ## Testing The trace middleware includes comprehensive test coverage for all features: | Test Case | Description | Expected Behavior | | ---------------------------------- | --------------------------------- | ---------------------------------------------------------------------- | | `TestNew` | Basic middleware creation | Creates span with trace ID and span ID for each request | | `TestWithOptions_ServiceName` | Custom service name configuration | Sets service tag to configured value | | `TestWithOptions_PropagateTraceID` | Existing trace ID propagation | Preserves incoming trace ID from request headers | | `TestWithOptions_ParentID` | Parent span ID propagation | Extracts and stores parent span ID from request headers | | `TestWithOptions_OnSpan` | Span completion callback | Invokes OnSpan callback with completed span including duration | | `TestWithOptions_Sampler` | Sampling control | Skips span creation when sampler returns false | | `TestAddTag` | Custom tag addition | Adds custom key-value tags to span | | `TestAddEvent` | Event logging | Records timestamped events with metadata in span | | `TestTraceID` | Trace ID retrieval | Returns current trace ID from context | | `TestSpanID` | Span ID retrieval | Returns current span ID from context | | `TestCollector` | Span collection | Collects multiple spans and supports clearing | | `TestResponseHeader` | Response header propagation | Sets X-Trace-ID header in response | | `TestHTTPHeaders` | Downstream header generation | Generates headers for propagating trace context to downstream services | ## Related Middlewares * [requestid](/middlewares/requestid) - Request IDs * [otel](/middlewares/otel) - OpenTelemetry * [logger](/middlewares/logger) - Request logging # Transformer Source: https://docs.go-mizu.dev/middlewares/transformer Request/response transformation middleware for data manipulation. ## Overview The `transformer` middleware transforms request and response data, enabling format conversion, field mapping, and data manipulation. Use it when you need: * API version compatibility * Field renaming/mapping * Format conversion ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/transformer" ``` ## Quick Start ```go theme={null} app := mizu.New() // Transform request JSON app.Use(transformer.Request(func(data map[string]any) map[string]any { // Rename field data["user_name"] = data["username"] delete(data, "username") return data })) ``` ## Configuration ### Options | Option | Type | Description | | ---------- | ------------------------------------- | ------------------ | | `Request` | `func(map[string]any) map[string]any` | Transform request | | `Response` | `func(map[string]any) map[string]any` | Transform response | ## Examples ### Request Transformation ```go theme={null} app.Use(transformer.Request(func(data map[string]any) map[string]any { // Snake case to camel case if v, ok := data["first_name"]; ok { data["firstName"] = v delete(data, "first_name") } return data })) ``` ### Response Transformation ```go theme={null} app.Use(transformer.Response(func(data map[string]any) map[string]any { // Add metadata return map[string]any{ "data": data, "version": "2.0", } })) ``` ### Field Mapping ```go theme={null} fieldMap := map[string]string{ "userName": "username", "firstName": "first_name", "lastName": "last_name", } app.Use(transformer.WithFieldMap(fieldMap)) ``` ### Version Compatibility ```go theme={null} // v1 to v2 transformation app.Use(transformer.Request(func(data map[string]any) map[string]any { // v1 had "name", v2 expects "fullName" if name, ok := data["name"]; ok { data["fullName"] = name delete(data, "name") } return data })) ``` ## API Reference ### Functions ```go theme={null} // Request creates request transformer func Request(fn func(map[string]any) map[string]any) mizu.Middleware // Response creates response transformer func Response(fn func(map[string]any) map[string]any) mizu.Middleware // WithFieldMap creates field mapping transformer func WithFieldMap(fieldMap map[string]string) mizu.Middleware ``` ## Technical Details The transformer middleware provides a flexible architecture for modifying HTTP requests and responses: ### Request Transformation Request transformers are applied sequentially before the handler executes. Each transformer receives the `*http.Request` object and can modify: * Headers (add, set, remove) * URL path and query parameters * Request body content The middleware supports multiple request transformers, applied in the order they are registered. ### Response Transformation Response transformation uses a custom `responseRecorder` that captures: * HTTP status code (defaults to 200 OK if not set) * Response headers * Response body bytes The recorder intercepts all response writes, allowing transformers to modify the complete response before it's sent to the client. Response transformers are applied sequentially and can modify: * Status codes (mapping or conditional changes) * Headers (add, set, remove) * Body content (transformation or replacement) ### Implementation Details * **Request transformers** receive `*http.Request` and return an error * **Response transformers** receive `(statusCode int, headers http.Header, body []byte)` and return `(int, http.Header, []byte, error)` * The middleware restores the original response writer after capturing to ensure proper cleanup * Headers from the recorder are copied to the original writer before writing the status and body * Empty response bodies are handled gracefully ## Best Practices * Use for API version compatibility * Keep transformations simple * Document transformation rules * Test edge cases thoroughly ## Testing The transformer middleware includes comprehensive test coverage for all transformation functions: | Test Case | Description | Expected Behavior | | --------------------------- | ------------------------------------------ | --------------------------------------------------------------- | | `TestNew` | Basic middleware initialization | Middleware works with default empty options | | `TestAddHeader` | Adding a request header | New header is added to request and accessible in handler | | `TestSetHeader` | Setting/overriding a request header | Existing header value is replaced with new value | | `TestRemoveHeader` | Removing a request header | Specified header is deleted from request | | `TestRewritePath` | URL path rewriting | Request path is transformed from old prefix to new prefix | | `TestAddQueryParam` | Adding query parameters | New query parameter is added and encoded properly | | `TestTransformBody` | Request body transformation | Request body is transformed (e.g., uppercase conversion) | | `TestAddResponseHeader` | Adding a response header | New header is added to response | | `TestSetResponseHeader` | Setting/overriding a response header | Response header is set to new value, overriding handler's value | | `TestRemoveResponseHeader` | Removing a response header | Specified header is removed from response | | `TestTransformResponseBody` | Response body transformation | Response body content is transformed before sending | | `TestMapStatusCode` | Status code mapping | HTTP status code is mapped from one value to another | | `TestReplaceBody` | Conditional body replacement | Response body is replaced based on status code condition | | `TestMultipleTransformers` | Multiple request and response transformers | All transformers are applied in sequence correctly | ## Related Middlewares * [version](/middlewares/version) - API versioning * [envelope](/middlewares/envelope) - Response wrapping * [filter](/middlewares/filter) - Field filtering # Validator Source: https://docs.go-mizu.dev/middlewares/validator Request validation middleware with declarative rules. ## Overview The `validator` middleware provides declarative request validation with built-in rules for common validation needs. It validates query parameters, form fields, headers, and path parameters. Use it when you need: * Input validation * Form validation * API parameter validation * Custom validation rules ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/validator" ``` ## Quick Start ```go theme={null} app := mizu.New() app.Post("/users", createUser, validator.New( validator.Field("email", "required", "email"), validator.Field("name", "required", "min:2", "max:50"), )) ``` ## Built-in Rules | Rule | Description | Example | | ---------- | ------------------------ | -------------------------- | | `required` | Field must not be empty | `"required"` | | `min:n` | Minimum string length | `"min:3"` | | `max:n` | Maximum string length | `"max:100"` | | `email` | Valid email format | `"email"` | | `numeric` | Numeric value | `"numeric"` | | `integer` | Integer value | `"integer"` | | `alpha` | Letters only | `"alpha"` | | `alphanum` | Letters and numbers only | `"alphanum"` | | `in:a,b,c` | Must be one of values | `"in:pending,active,done"` | | `url` | Valid URL | `"url"` | | `uuid` | Valid UUID format | `"uuid"` | ## Examples ### Basic Validation ```go theme={null} app.Post("/register", registerHandler, validator.New( validator.Field("username", "required", "alphanum", "min:3", "max:20"), validator.Field("email", "required", "email"), validator.Field("password", "required", "min:8"), )) ``` ### Optional Fields ```go theme={null} app.Put("/profile", updateProfile, validator.New( validator.OptionalField("bio", "max:500"), validator.OptionalField("website", "url"), )) ``` ### Enum Validation ```go theme={null} app.Post("/tasks", createTask, validator.New( validator.Field("title", "required", "min:1", "max:200"), validator.Field("status", "required", "in:pending,in_progress,done"), validator.Field("priority", "required", "in:low,medium,high"), )) ``` ### JSON Body Validation ```go theme={null} app.Post("/api/users", createUser, validator.JSON(map[string][]string{ "email": {"required", "email"}, "name": {"required", "min:2"}, "age": {"numeric"}, })) ``` ### Custom Error Handler ```go theme={null} app.Use(validator.WithOptions(validator.Options{ Rules: []validator.Rule{ validator.Field("email", "required", "email"), }, ErrorHandler: func(c *mizu.Ctx, errors validator.ValidationErrors) error { return c.JSON(422, map[string]any{ "message": "Validation failed", "errors": errors, }) }, })) ``` ### Custom Error Messages ```go theme={null} app.Post("/contact", contactHandler, validator.New( validator.Rule{ Field: "email", Rules: []string{"required", "email"}, Message: "Please provide a valid email address", }, validator.Rule{ Field: "message", Rules: []string{"required", "min:10"}, Message: "Message must be at least 10 characters", }, )) ``` ### Multiple Validation Groups ```go theme={null} // Login validation loginRules := validator.New( validator.Field("email", "required", "email"), validator.Field("password", "required"), ) // Registration validation registerRules := validator.New( validator.Field("email", "required", "email"), validator.Field("password", "required", "min:8"), validator.Field("name", "required", "min:2"), ) app.Post("/login", loginHandler, loginRules) app.Post("/register", registerHandler, registerRules) ``` ### Query Parameter Validation ```go theme={null} // GET /users?page=1&limit=20&status=active app.Get("/users", listUsers, validator.New( validator.OptionalField("page", "integer"), validator.OptionalField("limit", "integer"), validator.OptionalField("status", "in:active,inactive,pending"), )) ``` ## API Reference ### Functions ```go theme={null} // New creates validator with rules func New(rules ...Rule) mizu.Middleware // WithOptions creates validator with full configuration func WithOptions(opts Options) mizu.Middleware // JSON validates JSON body against rules func JSON(rules map[string][]string) mizu.Middleware // Field creates a required field rule func Field(name string, rules ...string) Rule // OptionalField creates an optional field rule func OptionalField(name string, rules ...string) Rule ``` ### Types ```go theme={null} type Rule struct { Field string Rules []string Message string Optional bool } type ValidationError struct { Field string `json:"field"` Message string `json:"message"` } type ValidationErrors []ValidationError ``` ### Field Sources The validator checks these sources in order: 1. Query parameters 2. Form values (POST/PUT) 3. Headers 4. Path parameters ## Error Response Default error response format: ```json theme={null} { "error": "validation failed", "errors": [ {"field": "email", "message": "is required"}, {"field": "name", "message": "must be at least 2 characters"} ] } ``` ## Technical Details ### Architecture The validator middleware is built on a pipeline architecture that processes validation rules sequentially: 1. **Field Value Extraction**: The middleware retrieves field values from multiple sources in order of precedence: * Query parameters (highest priority) * Form values (POST/PUT requests) * Headers * Path parameters (lowest priority) 2. **Rule Application**: Each validation rule is parsed and applied to the field value: * Rule format: `ruleName` or `ruleName:parameter` * Rules are checked sequentially for each field * First validation failure stops checking additional rules for that field 3. **Error Handling**: Validation errors are collected and either: * Passed to a custom error handler (if provided) * Returned as JSON with 400 Bad Request (default) ### Implementation Details **Validation Flow**: ``` Request → Extract Field Values → Apply Rules → Collect Errors → Error Handler or Next ``` **Rule Parsing**: * Rules are split on `:` to separate the rule name from its parameter * The `applyRule` function handles all built-in validation logic * Unknown rules are silently ignored **Optional Fields**: * When a field is marked as optional and empty, all its rules are skipped * Empty is defined as an empty string (`""`) * If an optional field has a value, all rules are applied normally **JSON Validation**: * Reads the entire request body into memory * Parses JSON into `map[string]any` * Converts non-string values using `stringify` helper * Supports string, float64, and bool type conversion **Custom Messages**: * The `Message` field in a `Rule` overrides default error messages * Custom messages apply to all validation rules for that field * If not set, each rule has its own default message ### Performance Characteristics * **Memory**: Minimal allocation for most validations; JSON validation reads entire body * **CPU**: Linear with number of fields and rules; regex-free for better performance * **Concurrency**: Safe for concurrent use; no shared state between requests ## Best Practices * Validate all user input * Use appropriate rules for data types * Provide clear error messages * Validate early in the middleware chain * Use optional fields for non-required parameters * Combine multiple rules for comprehensive validation * Consider custom error handlers for consistent API responses ## Testing ### Test Coverage The validator middleware includes comprehensive test coverage for all validation rules and scenarios: | Test Case | Description | Expected Behavior | | --------------------------- | ----------------------------------------------------------------- | --------------------------------------------------- | | **Basic Validation** | | | | Valid request | Request with all required fields meeting validation rules | Returns 200 OK | | Missing required field | Request missing a field marked as required | Returns 400 Bad Request | | Invalid email | Email field with invalid format (missing @ or .) | Returns 400 Bad Request | | Min length violation | Field value shorter than minimum length requirement | Returns 400 Bad Request | | **Optional Fields** | | | | Without optional field | Request without an optional field present | Returns 200 OK | | With valid optional field | Request with optional field that passes validation | Returns 200 OK | | With invalid optional field | Request with optional field failing validation | Returns 400 Bad Request | | **Validation Rules** | | | | Required pass | Non-empty value for required field | Validation passes | | Required fail | Empty value for required field | Validation fails | | Min pass | Value length >= minimum | Validation passes | | Min fail | Value length \< minimum | Validation fails | | Max pass | Value length maximum | Validation passes | | Max fail | Value length > maximum | Validation fails | | Email pass | Valid email format with @ and . | Validation passes | | Email fail | Invalid email format | Validation fails | | Numeric pass | Valid numeric value (e.g., "123.45") | Validation passes | | Numeric fail | Non-numeric value | Validation fails | | Integer pass | Valid integer value (e.g., "123") | Validation passes | | Integer fail | Non-integer value (e.g., "12.3") | Validation fails | | Alpha pass | Letters only value | Validation passes | | Alpha fail | Value with non-letter characters | Validation fails | | Alphanum pass | Letters and numbers only | Validation passes | | Alphanum fail | Value with special characters | Validation fails | | In pass | Value matching one of allowed options | Validation passes | | In fail | Value not in allowed options list | Validation fails | | URL pass | Valid URL with http\:// or https\:// | Validation passes | | URL fail | Invalid URL format | Validation fails | | UUID pass | Valid UUID format (36 chars with hyphens at positions 8,13,18,23) | Validation passes | | UUID fail | Invalid UUID format | Validation fails | | **Custom Error Handler** | | | | Custom error handler | Validation failure with custom error handler | Returns custom status code (422) with custom format | | **JSON Validation** | | | | Valid JSON | JSON body with valid fields | Returns 200 OK | | Invalid JSON values | JSON body with fields failing validation | Returns 400 Bad Request | | Malformed JSON | Unparseable JSON body | Returns 400 Bad Request | | **Error Formatting** | | | | Multiple validation errors | Multiple fields failing validation | Returns formatted error string with all failures | | Empty validation errors | No validation errors | Returns "validation failed" message | | **Field Helper Functions** | | | | Field creation | Creating required field rule | Returns Rule with correct field name and rules | | OptionalField creation | Creating optional field rule | Returns Rule with Optional=true | ## Related Middlewares * [contenttype](/middlewares/contenttype) - Content-Type validation * [bodylimit](/middlewares/bodylimit) - Body size limits # Vary Source: https://docs.go-mizu.dev/middlewares/vary Vary header management middleware for cache control. ## Overview The `vary` middleware manages the Vary HTTP header, telling caches which request headers affect response content. Use it when you need: * Proper cache control * Content negotiation * CDN configuration ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/vary" ``` ## Quick Start ```go theme={null} app := mizu.New() // Vary by Accept-Encoding app.Use(vary.New("Accept-Encoding")) ``` ## Configuration ### Options | Option | Type | Default | Description | | --------- | ---------- | -------- | ------------------ | | `Headers` | `[]string` | Required | Headers to vary by | ## Examples ### Single Header ```go theme={null} app.Use(vary.New("Accept-Encoding")) // Vary: Accept-Encoding ``` ### Multiple Headers ```go theme={null} app.Use(vary.New("Accept-Encoding", "Accept-Language", "Authorization")) // Vary: Accept-Encoding, Accept-Language, Authorization ``` ### Common Patterns ```go theme={null} // API with auth app.Use(vary.New("Authorization")) // Multilingual site app.Use(vary.New("Accept-Language")) // Compression app.Use(vary.New("Accept-Encoding")) ``` ## API Reference ### Functions ```go theme={null} // New creates vary middleware func New(headers ...string) mizu.Middleware // Add adds to existing Vary header func Add(c *mizu.Ctx, header string) ``` ## Technical Details ### Implementation Overview The vary middleware manages the `Vary` HTTP response header by: 1. **Header Management**: Maintains a set of headers that affect response content 2. **Duplicate Prevention**: Uses case-insensitive comparison to prevent duplicate headers 3. **Header Merging**: Combines new Vary headers with existing ones from the response 4. **Execution Order**: Processes after the handler executes to capture all Vary requirements ### Key Functions #### `add(c *mizu.Ctx, headers ...string)` * Adds headers to the Vary response header * Parses existing Vary header and creates a set for deduplication * Uses case-insensitive comparison (`strings.ToLower`) * Joins headers with comma-space separator #### `autoDetect(c *mizu.Ctx)` * Automatically detects content negotiation headers from the request * Checks for: `Accept`, `Accept-Encoding`, `Accept-Language` * Only adds headers that are present in the request ### Options Structure ```go theme={null} type Options struct { Headers []string // Explicit headers to add to Vary Auto bool // Enable automatic detection } ``` ### Convenience Functions The middleware provides several preset functions: * `AcceptEncoding()`: Adds `Accept-Encoding` to Vary * `Accept()`: Adds `Accept` to Vary * `AcceptLanguage()`: Adds `Accept-Language` to Vary * `Origin()`: Adds `Origin` to Vary (useful for CORS) * `All()`: Adds common headers: `Accept`, `Accept-Encoding`, `Accept-Language` * `Auto()`: Enables automatic header detection ## Best Practices * Include all headers that affect response * Use with compression middleware * Consider CDN caching implications * Don't over-vary (reduces cache effectiveness) ## Testing The middleware includes comprehensive tests covering various scenarios: | Test Case | Description | Expected Behavior | | ------------------------- | --------------------------------------------- | ------------------------------------------------------------------------ | | `TestNew` | Basic middleware with multiple headers | Sets Vary header with `Accept-Encoding` and `Accept-Language` | | `TestNoDuplicates` | Same header specified twice | Prevents duplicates; header appears only once in Vary | | `TestWithOptions_Auto` | Auto-detection with request headers | Automatically adds `Accept` and `Accept-Encoding` based on request | | `TestAdd` | Helper function within handler | Adds custom header `X-Custom-Header` to Vary | | `TestAcceptEncoding` | AcceptEncoding convenience function | Sets Vary header to `Accept-Encoding` | | `TestAccept` | Accept convenience function | Sets Vary header to `Accept` | | `TestAcceptLanguage` | AcceptLanguage convenience function | Sets Vary header to `Accept-Language` | | `TestOrigin` | Origin convenience function | Sets Vary header to `Origin` | | `TestAll` | All convenience function | Sets Vary header with `Accept`, `Accept-Encoding`, and `Accept-Language` | | `TestAuto` | Auto convenience function with request header | Auto-detects and adds `Accept-Language` when present in request | | `TestCombineWithExisting` | Combining with existing Vary header | Preserves existing `X-Custom` and adds `Accept-Encoding` | ## Related Middlewares * [compress](/middlewares/compress) - Response compression * [cache](/middlewares/cache) - Cache-Control headers * [language](/middlewares/language) - Language detection # Version Source: https://docs.go-mizu.dev/middlewares/version API versioning middleware for managing multiple API versions. ## Overview The `version` middleware provides API versioning support through headers, query parameters, or URL path prefixes. It helps manage multiple API versions and handles deprecation warnings. ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/version" ``` ## Quick Start ```go theme={null} app := mizu.New() app.Use(version.New(version.Options{ DefaultVersion: "v1", })) app.Get("/users", func(c *mizu.Ctx) error { v := version.Get(c) // Handle based on version return c.JSON(200, users) }) ``` ## Configuration | Option | Type | Default | Description | | ---------------- | ------------------------------- | ------------------ | ---------------------------------- | | `DefaultVersion` | `string` | - | Default version when not specified | | `Header` | `string` | `"Accept-Version"` | Header name for version | | `QueryParam` | `string` | `"version"` | Query parameter name | | `PathPrefix` | `bool` | `false` | Extract version from URL path | | `Supported` | `[]string` | - | List of supported versions | | `Deprecated` | `[]string` | - | List of deprecated versions | | `ErrorHandler` | `func(*mizu.Ctx, string) error` | - | Custom error handler | ## Examples ### Version from Header ```go theme={null} app.Use(version.New(version.Options{ DefaultVersion: "v1", Header: "Accept-Version", })) app.Get("/api/users", func(c *mizu.Ctx) error { switch version.Get(c) { case "v2": return c.JSON(200, usersV2) default: return c.JSON(200, usersV1) } }) ``` Client request: ``` GET /api/users HTTP/1.1 Accept-Version: v2 ``` ### Version from Query Parameter ```go theme={null} app.Use(version.New(version.Options{ DefaultVersion: "v1", QueryParam: "api_version", })) ``` Request: `GET /users?api_version=v2` ### Version from URL Path ```go theme={null} app.Use(version.New(version.Options{ PathPrefix: true, })) // GET /v1/users → version "v1" // GET /v2/users → version "v2" ``` ### Supported Versions Only ```go theme={null} app.Use(version.New(version.Options{ DefaultVersion: "v2", Supported: []string{"v1", "v2", "v3"}, ErrorHandler: func(c *mizu.Ctx, v string) error { return c.JSON(400, map[string]string{ "error": "Unsupported API version: " + v, "supported": "v1, v2, v3", }) }, })) ``` ### Deprecation Warnings ```go theme={null} app.Use(version.New(version.Options{ DefaultVersion: "v3", Supported: []string{"v1", "v2", "v3"}, Deprecated: []string{"v1"}, })) // Requests with v1 will include: // Deprecation: true // Sunset: See documentation for migration guide ``` ### Using Helper Functions ```go theme={null} // Version from header only app.Use(version.FromHeader("X-API-Version")) // Version from path only app.Use(version.FromPath()) // Version from query only app.Use(version.FromQuery("v")) ``` ### Version-Specific Routes ```go theme={null} app.Use(version.New(version.Options{ PathPrefix: true, })) app.Get("/v1/users", usersV1Handler) app.Get("/v2/users", usersV2Handler) app.Get("/v3/users", usersV3Handler) ``` ### Content Negotiation ```go theme={null} app.Get("/users", func(c *mizu.Ctx) error { v := version.Get(c) switch v { case "v3": return c.JSON(200, map[string]any{ "data": users, "meta": metadata, "links": links, }) case "v2": return c.JSON(200, map[string]any{ "users": users, "total": len(users), }) default: return c.JSON(200, users) } }) ``` ### Feature Flags by Version ```go theme={null} app.Get("/users", func(c *mizu.Ctx) error { v := version.Get(c) users := getUsers() // New field only in v2+ if v >= "v2" { for i := range users { users[i].Avatar = getAvatar(users[i].ID) } } return c.JSON(200, users) }) ``` ## API Reference ```go theme={null} func New(opts Options) mizu.Middleware func FromHeader(header string) mizu.Middleware func FromPath() mizu.Middleware func FromQuery(param string) mizu.Middleware func GetVersion(c *mizu.Ctx) string func Get(c *mizu.Ctx) string // Alias for GetVersion ``` ## Version Detection Order When multiple sources are configured: 1. Header (e.g., `Accept-Version: v2`) 2. Query parameter (e.g., `?version=v2`) 3. Path prefix (e.g., `/v2/users`) 4. Default version ## Response Headers For deprecated versions: ``` Deprecation: true Sunset: See documentation for migration guide ``` ## Technical Details ### Architecture The version middleware uses a context-based approach to store and retrieve version information: 1. **Version Detection**: The middleware examines multiple sources in a specific priority order 2. **Context Storage**: Detected version is stored in the request context using a private context key 3. **Validation**: Optional validation against supported/deprecated version lists 4. **Header Injection**: Automatic deprecation headers for deprecated versions ### Implementation Details **Version Detection Priority**: 1. HTTP Header (configurable, default: `Accept-Version`) 2. Query Parameter (configurable, default: `version`) 3. Path Prefix (optional, pattern: `/v{number}/...`) 4. Default Version (fallback) **Version String Validation**: * Pattern: `v` or `V` followed by digits and optional dots * Examples: `v1`, `v2`, `V1`, `v1.0`, `v1.2.3` * Invalid: `api`, empty string, `v`, `va` **Context Management**: * Uses a private `contextKey{}` struct to prevent collisions * Version stored as string in request context * Retrieved via `GetVersion(c)` or `Get(c)` helper functions **Performance Optimizations**: * Supported and deprecated versions stored in maps for O(1) lookup * Single pass through version sources with early exit * No regex matching for version strings (character-by-character validation) ### Response Headers For deprecated versions, the middleware automatically adds: * `Deprecation: true` - Indicates the version is deprecated * `Sunset: See documentation for migration guide` - Migration information ## Best Practices * Always set a default version * Document supported versions * Add deprecation warnings before removal * Use semantic versioning (v1, v2, v3) * Maintain backward compatibility within major versions * Provide migration guides for deprecated versions ## Testing The middleware includes comprehensive test coverage for all features: | Test Case | Description | Expected Behavior | | --------------------------------------- | ---------------------------------- | ----------------------------------------- | | `TestNew/from header` | Version from Accept-Version header | Extracts version "v2" from header | | `TestNew/from query` | Version from query parameter | Extracts version "v3" from ?version=v3 | | `TestNew/default version` | No version specified | Uses default version "v1" | | `TestNew_PathPrefix/v1` | Version from path prefix /v1 | Extracts version "v1" from URL path | | `TestNew_PathPrefix/v2` | Version from path prefix /v2 | Extracts version "v2" from URL path | | `TestNew_Supported/supported version` | Request with supported version | Returns 200 OK | | `TestNew_Supported/unsupported version` | Request with unsupported version | Returns 400 Bad Request | | `TestNew_Deprecated` | Request with deprecated version | Sets Deprecation header to "true" | | `TestFromHeader` | Custom header name (X-API-Version) | Extracts version "2.0" from custom header | | `TestFromPath` | Path with semantic version (v1.2) | Extracts version "v1.2" from path | | `TestFromQuery` | Custom query param (api\_version) | Extracts version "3.0" from custom param | | `TestGet` | GetVersion vs Get function | Both functions return same value | | `TestIsVersionString/v1` | Valid version string "v1" | Returns true | | `TestIsVersionString/v2` | Valid version string "v2" | Returns true | | `TestIsVersionString/V1` | Valid uppercase "V1" | Returns true | | `TestIsVersionString/v1.0` | Valid with dot "v1.0" | Returns true | | `TestIsVersionString/v1.2.3` | Valid semantic "v1.2.3" | Returns true | | `TestIsVersionString/api` | Invalid string "api" | Returns false | | `TestIsVersionString/empty` | Empty string | Returns false | | `TestIsVersionString/v` | Just "v" character | Returns false | | `TestIsVersionString/va` | Invalid "va" | Returns false | ## Related Middlewares * [maintenance](/middlewares/maintenance) - Maintenance mode * [header](/middlewares/header) - Custom headers # WebSocket Source: https://docs.go-mizu.dev/middlewares/websocket WebSocket upgrade middleware for real-time bidirectional communication. ## Overview The `websocket` middleware handles WebSocket upgrade requests, enabling real-time bidirectional communication between clients and your server. ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/websocket" ``` ## Quick Start ```go theme={null} app := mizu.New() app.Get("/ws", websocket.New(func(c *mizu.Ctx, ws *websocket.Conn) error { for { msgType, data, err := ws.ReadMessage() if err != nil { return err } // Echo message back if err := ws.WriteMessage(msgType, data); err != nil { return err } } })) ``` ## Configuration | Option | Type | Default | Description | | -------------- | -------------------------- | ----------- | ----------------------- | | `Origins` | `[]string` | All allowed | Allowed origin domains | | `Subprotocols` | `[]string` | - | Supported subprotocols | | `CheckOrigin` | `func(*http.Request) bool` | - | Custom origin validator | ## Examples ### Echo Server ```go theme={null} app.Get("/ws", websocket.New(func(c *mizu.Ctx, ws *websocket.Conn) error { for { _, data, err := ws.ReadMessage() if err != nil { return err // Client disconnected } if err := ws.WriteText(string(data)); err != nil { return err } } })) ``` ### Chat Application ```go theme={null} var clients = make(map[*websocket.Conn]bool) var broadcast = make(chan string) var mu sync.Mutex // Broadcast messages to all clients go func() { for msg := range broadcast { mu.Lock() for client := range clients { client.WriteText(msg) } mu.Unlock() } }() app.Get("/chat", websocket.New(func(c *mizu.Ctx, ws *websocket.Conn) error { // Register client mu.Lock() clients[ws] = true mu.Unlock() defer func() { mu.Lock() delete(clients, ws) mu.Unlock() }() for { _, data, err := ws.ReadMessage() if err != nil { return nil } broadcast <- string(data) } })) ``` ### Origin Validation ```go theme={null} app.Get("/ws", websocket.WithOptions( func(c *mizu.Ctx, ws *websocket.Conn) error { // Handler }, websocket.Options{ Origins: []string{ "https://example.com", "https://app.example.com", }, }, )) ``` ### Custom Origin Check ```go theme={null} app.Get("/ws", websocket.WithOptions( handler, websocket.Options{ CheckOrigin: func(r *http.Request) bool { origin := r.Header.Get("Origin") // Custom validation logic return strings.HasSuffix(origin, ".example.com") }, }, )) ``` ### Subprotocols ```go theme={null} app.Get("/ws", websocket.WithOptions( handler, websocket.Options{ Subprotocols: []string{"graphql-ws", "subscriptions-transport-ws"}, }, )) ``` ### Binary Messages ```go theme={null} app.Get("/ws", websocket.New(func(c *mizu.Ctx, ws *websocket.Conn) error { for { msgType, data, err := ws.ReadMessage() if err != nil { return err } switch msgType { case websocket.TextMessage: ws.WriteText("Received text: " + string(data)) case websocket.BinaryMessage: ws.WriteBinary(processBinary(data)) } } })) ``` ### Ping/Pong ```go theme={null} app.Get("/ws", websocket.New(func(c *mizu.Ctx, ws *websocket.Conn) error { // Send periodic pings go func() { ticker := time.NewTicker(30 * time.Second) defer ticker.Stop() for range ticker.C { if err := ws.Ping(nil); err != nil { return } } }() for { msgType, data, err := ws.ReadMessage() if err != nil { return err } // Handle pong automatically if msgType == websocket.PongMessage { continue } ws.WriteMessage(msgType, data) } })) ``` ## Message Types | Constant | Value | Description | | --------------- | ----- | --------------- | | `TextMessage` | 1 | UTF-8 text data | | `BinaryMessage` | 2 | Binary data | | `CloseMessage` | 8 | Close frame | | `PingMessage` | 9 | Ping frame | | `PongMessage` | 10 | Pong frame | ## Conn Methods ```go theme={null} func (c *Conn) ReadMessage() (messageType int, data []byte, err error) func (c *Conn) WriteMessage(messageType int, data []byte) error func (c *Conn) WriteText(text string) error func (c *Conn) WriteBinary(data []byte) error func (c *Conn) Ping(data []byte) error func (c *Conn) Pong(data []byte) error func (c *Conn) Close() error ``` ## API Reference ```go theme={null} func New(handler Handler) mizu.Middleware func WithOptions(handler Handler, opts Options) mizu.Middleware func IsWebSocketUpgrade(r *http.Request) bool type Handler func(c *mizu.Ctx, ws *Conn) error ``` ## Client Example ```javascript theme={null} const ws = new WebSocket('ws://localhost:8080/ws'); ws.onopen = () => { ws.send('Hello, server!'); }; ws.onmessage = (event) => { console.log('Received:', event.data); }; ws.onclose = () => { console.log('Connection closed'); }; ``` ## Technical Details ### WebSocket Protocol Implementation The middleware implements the WebSocket protocol (RFC 6455) with the following key components: #### Connection Upgrade Process 1. **Request Validation**: Checks for `Upgrade: websocket` and `Connection: Upgrade` headers 2. **Origin Validation**: Validates the request origin against configured allowed origins 3. **Key Exchange**: Validates `Sec-WebSocket-Key` and computes the accept key using SHA1 hash with the WebSocket GUID 4. **Subprotocol Negotiation**: Matches requested subprotocols with supported ones 5. **HTTP Hijacking**: Takes over the HTTP connection using Go's `http.Hijacker` interface 6. **101 Switching Protocols**: Sends the upgrade response with `Sec-WebSocket-Accept` header #### Frame Structure The implementation handles WebSocket frames with the following structure: * **Opcode**: Identifies the frame type (text, binary, close, ping, pong) * **Masking**: Client-to-server messages must be masked; server-to-client messages are unmasked * **Payload Length**: Supports three length encoding formats: * 0-125 bytes: Single byte length * 126-65535 bytes: 2-byte extended length * 65536+ bytes: 8-byte extended length #### Connection Management * **Thread-Safe Writes**: Uses mutex locks to ensure concurrent write safety * **Buffered I/O**: Utilizes `bufio.Reader` and `bufio.Writer` for efficient data transfer * **Resource Cleanup**: Automatically closes underlying TCP connections when handlers return * **Error Handling**: Returns errors for invalid frames, connection failures, and protocol violations #### Security Features * **SHA1 for Key Exchange**: Uses SHA1 (required by RFC 6455) for computing the `Sec-WebSocket-Accept` header * **Origin Validation**: Supports both whitelist-based and custom validation functions * **Wildcard Origins**: Allows `*` for development but should be restricted in production * **No Server Masking**: Server messages are sent unmasked per protocol specification ## Best Practices * Always validate origins in production * Handle disconnections gracefully * Use ping/pong for connection health * Clean up resources when connections close * Consider using a message broker for scaling ## Testing The middleware includes comprehensive test coverage for all major functionality: | Test Case | Description | Expected Behavior | | ----------------------------------------------------- | ------------------------------------------------- | ---------------------------------------------------------- | | `TestIsWebSocketUpgrade` | Valid WebSocket upgrade headers | Returns true for valid upgrade requests | | `TestIsWebSocketUpgrade` (case insensitive) | Headers with mixed case | Correctly identifies WebSocket upgrades regardless of case | | `TestIsWebSocketUpgrade` (connection with keep-alive) | Multiple connection values | Handles Connection header with multiple values | | `TestIsWebSocketUpgrade` (missing headers) | Missing Upgrade or Connection headers | Returns false when required headers are absent | | `TestComputeAcceptKey` | RFC 6455 test vector | Computes correct Sec-WebSocket-Accept value | | `TestNew_NonWebSocket` | Regular HTTP request | Passes through to next handler without upgrade | | `TestWithOptions_ForbiddenOrigin` | Origin not in allowed list | Returns 403 Forbidden | | `TestWithOptions_AllowedOrigin` | Origin in allowed list | Accepts the connection | | `TestWithOptions_WildcardOrigin` | Wildcard (\*) origin | Allows any origin | | `TestWithOptions_MissingKey` | Missing Sec-WebSocket-Key header | Returns 400 Bad Request | | `TestConn_WriteText` | Writing text message | Successfully writes text frame | | `TestConn_WriteBinary` | Writing binary message | Successfully writes binary frame | | `TestConn_WriteMessageLengths` | Various payload sizes (10, 126, 200, 70000 bytes) | Correctly encodes all payload length formats | | `TestConn_Close` | Connection close | Sends close frame and closes underlying connection | | `TestConn_Ping` | Ping message | Sends ping frame with payload | | `TestConn_Pong` | Pong message | Sends pong frame with payload | | `TestConn_ReadMessage` (short unmasked text) | Reading unmasked text frame | Correctly parses and returns message | | `TestConn_ReadMessage` (masked text) | Reading masked text frame | Unmasks and returns correct data | | `TestConn_ReadMessageExtendedLength` | 200-byte message (2-byte length) | Correctly decodes 126+ byte payloads | | `TestConn_ReadMessageVeryLongLength` | 70000-byte message (8-byte length) | Correctly decodes 65536+ byte payloads | | `TestConn_ReadMessageReadErrors` | Incomplete frame data | Returns appropriate errors | | `TestWithOptions_Subprotocols` | Matching subprotocol | Accepts connection with negotiated subprotocol | | `TestWithOptions_SubprotocolNotMatching` | Non-matching subprotocol | Proceeds without subprotocol | | `TestWithOptions_CustomCheckOrigin` | Custom origin validation function | Calls custom validator and respects result | | `TestWithOptions_NoOriginsAllowsAll` | No origins configured | Allows all origins by default | | `TestWithOptions_OriginNotInList` | Origin not in whitelist | Returns 403 Forbidden | | `TestMessageConstants` | Message type constants | Verifies correct constant values per RFC 6455 | | `TestErrors` | Error types | Validates error messages | ## Related Middlewares * [sse](/middlewares/sse) - Server-Sent Events * [timeout](/middlewares/timeout) - Request timeout # XML Source: https://docs.go-mizu.dev/middlewares/xml XML content handling middleware for XML request/response processing. ## Overview The `xml` middleware provides XML content negotiation, parsing, and response helpers for XML-based APIs. Use it when you need: * XML API endpoints * SOAP compatibility * Legacy system integration ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/xml" ``` ## Quick Start ```go theme={null} app := mizu.New() app.Use(xml.New()) app.Get("/data", func(c *mizu.Ctx) error { return xml.Send(c, 200, data) }) ``` ## Examples ### XML Response ```go theme={null} type User struct { XMLName xml.Name `xml:"user"` ID string `xml:"id"` Name string `xml:"name"` } app.Get("/user/:id", func(c *mizu.Ctx) error { user := getUser(c.Param("id")) return xml.Send(c, 200, user) }) // 1John ``` ### Parse XML Request ```go theme={null} app.Post("/users", func(c *mizu.Ctx) error { var user User if err := xml.Bind(c, &user); err != nil { return err } // Process user return c.NoContent() }) ``` ### Content Negotiation ```go theme={null} app.Use(xml.New()) app.Get("/data", func(c *mizu.Ctx) error { // Returns XML if Accept: application/xml // Returns JSON if Accept: application/json return c.Negotiate(200, data) }) ``` ### With Declaration ```go theme={null} app.Get("/data", func(c *mizu.Ctx) error { return xml.SendWithDeclaration(c, 200, data) }) // ... ``` ## API Reference ### Functions ```go theme={null} // New creates XML middleware func New() mizu.Middleware // Send sends XML response func Send(c *mizu.Ctx, status int, data any) error // SendWithDeclaration sends XML with declaration func SendWithDeclaration(c *mizu.Ctx, status int, data any) error // Bind parses XML request body func Bind(c *mizu.Ctx, v any) error ``` ## Content Types | Type | Description | | ----------------- | ------------ | | `application/xml` | Standard XML | | `text/xml` | Alternative | ## Technical Details ### Architecture The XML middleware uses a context-based architecture to store and retrieve XML-specific data: * **Context Keys**: Uses private `contextKey` type to store body data, options, and preferred format * **Auto-Parsing**: Optionally reads and caches XML request bodies for reusable access * **Content Negotiation**: Detects client preferences via Accept header and stores format preference ### Implementation Details **Middleware Options**: * `Indent`: Indentation string for pretty printing (default: empty) * `Prefix`: Prefix for each XML element (default: empty) * `ContentType`: Content type for responses (default: "application/xml") * `AutoParse`: Automatically parse XML request bodies (default: false) * `XMLDeclaration`: Include XML declaration in responses (default: true) **Content Type Detection**: The middleware accepts both `application/xml` and `text/xml` content types, as well as `*/*` and empty Accept headers. **XML Declaration**: By default, all responses include the standard XML declaration (``). This can be disabled via options. **Marshal Options**: * Standard marshaling: Uses `xml.Marshal` * Pretty printing: Uses `xml.MarshalIndent` with configurable indent and prefix ### Response Flow 1. Middleware checks Accept header for XML compatibility 2. If AutoParse enabled, reads and caches request body 3. Stores options in context for response functions 4. Response functions retrieve options from context 5. Marshal data with appropriate settings 6. Set Content-Type header with charset 7. Write XML declaration (if enabled) and data ## Best Practices * Use proper struct tags * Handle encoding correctly * Validate XML input * Consider namespace handling * Enable AutoParse when binding XML requests frequently * Use Pretty() middleware during development for readable output ## Testing The XML middleware includes comprehensive test coverage for all functionality: | Test Case | Description | Expected Behavior | | ---------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------ | | TestNew | Basic middleware initialization | Middleware should initialize and pass through requests successfully | | TestResponse | XML response generation | Should return XML response with correct Content-Type header and proper XML structure | | TestBind | XML request body parsing | Should parse XML request body into Go struct with AutoParse enabled | | TestBody | Raw body retrieval | Should retrieve cached XML body when AutoParse is enabled | | TestSendError | XML error response | Should send properly formatted XML error with code and message elements | | TestPretty | Pretty printing | Should format XML with newlines and indentation when Pretty middleware is used | | TestContentNegotiation | Content negotiation based on Accept header | Should detect preferred format (XML/JSON) from Accept header, defaulting to JSON | | TestRespond | Format-aware response | Should respond with XML or JSON based on content negotiation preference | | TestXMLDeclaration | XML declaration control | Should include/exclude XML declaration based on XMLDeclaration option | | TestWrap | XML wrapper element | Should wrap data in named root element correctly | ## Related Middlewares * [contenttype](/middlewares/contenttype) - Content-Type validation * [msgpack](/middlewares/msgpack) - MessagePack handling * [validator](/middlewares/validator) - Input validation # X-Requested-With Source: https://docs.go-mizu.dev/middlewares/xrequestedwith X-Requested-With header validation middleware for AJAX request detection. ## Overview The `xrequestedwith` middleware validates the X-Requested-With header, commonly used to identify AJAX requests and prevent CSRF attacks. Use it when you need: * AJAX request validation * CSRF protection layer * Request origin verification ## Installation ```go theme={null} import "github.com/go-mizu/mizu/middlewares/xrequestedwith" ``` ## Quick Start ```go theme={null} app := mizu.New() // Require X-Requested-With: XMLHttpRequest app.Use(xrequestedwith.New()) ``` ## Configuration ### Options | Option | Type | Default | Description | | ---------- | ---------- | ------------------ | ---------------- | | `Required` | `bool` | `true` | Require header | | `Value` | `string` | `"XMLHttpRequest"` | Expected value | | `Methods` | `[]string` | State-changing | Methods to check | ## Examples ### Basic Validation ```go theme={null} app.Use(xrequestedwith.New()) // Requires: X-Requested-With: XMLHttpRequest ``` ### Custom Value ```go theme={null} app.Use(xrequestedwith.New(xrequestedwith.Options{ Value: "MyApp", })) // Requires: X-Requested-With: MyApp ``` ### Specific Methods ```go theme={null} app.Use(xrequestedwith.New(xrequestedwith.Options{ Methods: []string{"POST", "PUT", "DELETE"}, })) ``` ### Detection Only ```go theme={null} app.Use(xrequestedwith.New(xrequestedwith.Options{ Required: false, // Don't reject, just detect })) app.Get("/", func(c *mizu.Ctx) error { if xrequestedwith.IsAJAX(c) { return c.JSON(200, data) } return c.HTML(200, page) }) ``` ## API Reference ### Functions ```go theme={null} // New creates X-Requested-With middleware func New(opts ...Options) mizu.Middleware // IsAJAX returns true if AJAX request func IsAJAX(c *mizu.Ctx) bool ``` ## Client Usage ```javascript theme={null} // jQuery sets this automatically $.ajax({url: '/api', type: 'POST', data: data}); // Fetch/XMLHttpRequest fetch('/api', { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' } }); ``` ## Technical Details ### Implementation Overview The middleware performs header validation through the following process: 1. **Method Filtering**: Checks if the request method is in the skip list (default: GET, HEAD, OPTIONS) 2. **Path Filtering**: Checks if the request path is in the skip paths list 3. **Header Validation**: Compares the X-Requested-With header value (case-insensitive) against the expected value 4. **Error Handling**: Returns either a custom error handler response or a default 400 Bad Request ### Internal Components * **Skip Methods Map**: Pre-built map of HTTP methods to skip (defaults to safe methods) * **Skip Paths Map**: Pre-built map of paths to exclude from validation * **Case-Insensitive Comparison**: Uses `strings.EqualFold` for header value matching * **Default Value**: "XMLHttpRequest" is used when no custom value is specified ### Function Variants | Function | Purpose | Skip Methods | | ---------------- | ----------------------- | ------------------------- | | `New()` | Standard validation | GET, HEAD, OPTIONS | | `WithOptions()` | Custom configuration | Configurable | | `Require(value)` | Custom value validation | GET, HEAD, OPTIONS | | `AJAXOnly()` | Strict AJAX validation | None (checks all methods) | | `IsAJAX()` | Detection helper | N/A (utility function) | ## Security Note While X-Requested-With adds a layer of protection, it should not be the sole CSRF defense. Combine with: * CSRF tokens * SameSite cookies * Origin validation ## Best Practices * Use as additional security layer * Combine with CSRF middleware * Don't rely on it alone * Document header requirement for clients ## Testing The middleware includes comprehensive test coverage for various scenarios: | Test Case | Description | Expected Behavior | | ------------------------------------------------- | ----------------------------------------------------- | --------------------------------------- | | **TestNew - with header** | POST request with X-Requested-With: XMLHttpRequest | Returns 200 OK | | **TestNew - without header** | POST request without X-Requested-With header | Returns 400 Bad Request | | **TestNew\_SkipsGET** | GET request without header (default skip) | Returns 200 OK (skipped validation) | | **TestWithOptions\_CustomValue - correct value** | Request with custom header value "CustomValue" | Returns 200 OK | | **TestWithOptions\_CustomValue - wrong value** | Request with XMLHttpRequest when CustomValue expected | Returns 400 Bad Request | | **TestWithOptions\_SkipPaths - skipped path** | POST to /webhook (in skip list) without header | Returns 200 OK (path skipped) | | **TestWithOptions\_SkipPaths - non-skipped path** | POST to /api (not in skip list) without header | Returns 400 Bad Request | | **TestWithOptions\_ErrorHandler** | Request without header with custom error handler | Returns 403 Forbidden with custom JSON | | **TestRequire** | Request with custom required value "FetchRequest" | Returns 200 OK | | **TestAJAXOnly - with AJAX header** | GET request with XMLHttpRequest header | Returns 200 OK | | **TestAJAXOnly - without AJAX header** | GET request without header (no methods skipped) | Returns 400 Bad Request | | **TestIsAJAX - is AJAX** | Request with X-Requested-With: XMLHttpRequest | IsAJAX() returns true | | **TestIsAJAX - not AJAX** | Request without X-Requested-With header | IsAJAX() returns false | | **TestCaseInsensitive** | POST with lowercase "xmlhttprequest" | Returns 200 OK (case-insensitive match) | ## Related Middlewares * [csrf](/middlewares/csrf) - CSRF protection * [cors](/middlewares/cors) - CORS handling * [bot](/middlewares/bot) - Bot detection # Adapters Source: https://docs.go-mizu.dev/mobile/adapters Framework-specific mobile adapters for common patterns. Mobile adapters provide framework-specific defaults and optimizations for integrating Mizu with mobile clients. ## Overview ```go theme={null} import "github.com/go-mizu/mizu/mobile/adapters" // Apply iOS-optimized defaults app.Use(adapters.IOS(mobile.Options{ RequireDeviceID: true, })) ``` ## Available Adapters ### iOS Adapter ```go theme={null} func IOS(opts mobile.Options) mizu.Middleware ``` Optimized for iOS applications: * Parses iOS User-Agent patterns * Infers APNS for push tokens * Handles iOS version formats ```go theme={null} app.Use(adapters.IOS(mobile.Options{ RequireDeviceID: true, RequireAppVersion: true, MinAppVersion: "2.0.0", })) ``` ### Android Adapter ```go theme={null} func Android(opts mobile.Options) mizu.Middleware ``` Optimized for Android applications: * Parses Android User-Agent patterns * Infers FCM for push tokens * Handles Android version formats ```go theme={null} app.Use(adapters.Android(mobile.Options{ RequireDeviceID: true, RequireAppVersion: true, MinAppVersion: "2.0.0", })) ``` ### Flutter Adapter ```go theme={null} func Flutter(opts mobile.Options) mizu.Middleware ``` Optimized for Flutter applications: * Handles both iOS and Android * Platform detection from headers * Cross-platform push token handling ```go theme={null} app.Use(adapters.Flutter(mobile.Options{ RequireDeviceID: true, })) ``` ### React Native Adapter ```go theme={null} func ReactNative(opts mobile.Options) mizu.Middleware ``` Optimized for React Native: * Expo and bare workflow support * Cross-platform handling * JavaScript bridge considerations ```go theme={null} app.Use(adapters.ReactNative(mobile.Options{ RequireDeviceID: true, })) ``` ### Capacitor Adapter ```go theme={null} func Capacitor(opts mobile.Options) mizu.Middleware ``` Optimized for Capacitor/Ionic: * Web view detection * Hybrid app handling * Native plugin headers ```go theme={null} app.Use(adapters.Capacitor(mobile.Options{ RequireDeviceID: true, })) ``` ## Using Multiple Adapters For backends serving multiple client types: ```go theme={null} // Generic mobile middleware that handles all platforms app.Use(mobile.WithOptions(mobile.Options{ RequireDeviceID: true, })) // Platform-specific routes if needed app.Group("/ios", func(r *mizu.Router) { r.Use(adapters.IOS(mobile.Options{})) r.Get("/features", iosFeatures) }) app.Group("/android", func(r *mizu.Router) { r.Use(adapters.Android(mobile.Options{})) r.Get("/features", androidFeatures) }) ``` ## Creating Custom Adapters ```go theme={null} package myadapters import ( "github.com/go-mizu/mizu" "github.com/go-mizu/mizu/mobile" ) func MyFramework(opts mobile.Options) mizu.Middleware { // Apply framework-specific defaults if opts.SkipPaths == nil { opts.SkipPaths = []string{"/health"} } // Custom header handling opts.OnMissingHeader = func(c *mizu.Ctx, header string) error { // Framework-specific error format return c.JSON(400, map[string]string{ "framework_error": "Missing " + header, }) } return mobile.WithOptions(opts) } ``` ## Next Steps Available templates Complete API documentation # Android Source: https://docs.go-mizu.dev/mobile/android Build native Android apps with Kotlin, Jetpack Compose, and Mizu backend integration. Build native Android applications with Kotlin and Jetpack Compose that seamlessly integrate with your Mizu backend. The Android template provides a complete project structure with modern architecture patterns. ## Quick Start ```bash theme={null} mizu new ./my-android-app --template mobile/android ``` This creates a complete Android project with: * Kotlin + Jetpack Compose UI * Coroutines and Flow for async * Retrofit for networking * Hilt for dependency injection * Room for local storage ## Project Structure ``` my-android-app/ ├── backend/ # Mizu Go backend │ ├── cmd/server/ │ ├── app/server/ │ ├── go.mod │ └── Makefile │ ├── android/ # Android app │ ├── app/ │ │ ├── src/main/ │ │ │ ├── java/com/example/app/ │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── MyApplication.kt │ │ │ │ ├── di/ # Dependency injection │ │ │ │ ├── data/ # Data layer │ │ │ │ │ ├── api/ │ │ │ │ │ ├── local/ │ │ │ │ │ └── repository/ │ │ │ │ ├── domain/ # Business logic │ │ │ │ ├── ui/ # UI layer │ │ │ │ │ ├── theme/ │ │ │ │ │ ├── navigation/ │ │ │ │ │ └── screens/ │ │ │ │ └── util/ │ │ │ ├── res/ │ │ │ └── AndroidManifest.xml │ │ └── build.gradle.kts │ ├── gradle/ │ ├── build.gradle.kts │ └── settings.gradle.kts │ └── Makefile ``` ## Template Options ```bash theme={null} mizu new ./my-app --template mobile/android \ --var name=MyApp \ --var package=com.example.myapp \ --var minSdk=26 \ --var ui=compose ``` | Variable | Description | Default | | --------- | ------------------------------------------ | ---------------------- | | `name` | Project name | Directory name | | `package` | Package name | `com.example.{{name}}` | | `minSdk` | Minimum SDK version | `26` | | `ui` | UI framework: `compose`, `views`, `hybrid` | `compose` | ## API Client ```kotlin theme={null} // data/api/ApiService.kt interface ApiService { @GET("api/users") suspend fun getUsers( @Query("page") page: Int = 1, @Query("per_page") perPage: Int = 20 ): PageResponse @GET("api/users/{id}") suspend fun getUser(@Path("id") id: String): User @POST("api/auth/login") suspend fun login(@Body request: LoginRequest): AuthResponse @POST("api/push/register") suspend fun registerPushToken(@Body request: PushTokenRequest) @GET("api/sync") suspend fun sync(@Query("sync_token") token: String?): SyncResponse } // data/api/MizuHeaderInterceptor.kt class MizuHeaderInterceptor( private val context: Context, private val deviceIdProvider: DeviceIdProvider ) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request().newBuilder() .header("X-Device-ID", deviceIdProvider.getDeviceId()) .header("X-App-Version", BuildConfig.VERSION_NAME) .header("X-App-Build", BuildConfig.VERSION_CODE.toString()) .header("X-Device-Model", Build.MODEL) .header("X-Platform", "android") .header("X-OS-Version", Build.VERSION.RELEASE) .header("X-Timezone", TimeZone.getDefault().id) .header("X-Locale", Locale.getDefault().toLanguageTag()) .header("X-API-Version", "v2") .build() val response = chain.proceed(request) // Check for deprecation warning if (response.header("X-API-Deprecated") == "true") { EventBus.post(ApiDeprecatedEvent()) } return response } } ``` ## Dependency Injection ```kotlin theme={null} // di/NetworkModule.kt @Module @InstallIn(SingletonComponent::class) object NetworkModule { @Provides @Singleton fun provideOkHttpClient( authInterceptor: AuthInterceptor, mizuHeaderInterceptor: MizuHeaderInterceptor ): OkHttpClient { return OkHttpClient.Builder() .addInterceptor(mizuHeaderInterceptor) .addInterceptor(authInterceptor) .addInterceptor(HttpLoggingInterceptor().apply { level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE }) .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .build() } @Provides @Singleton fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit { return Retrofit.Builder() .baseUrl(BuildConfig.API_BASE_URL) .client(okHttpClient) .addConverterFactory(GsonConverterFactory.create()) .build() } @Provides @Singleton fun provideApiService(retrofit: Retrofit): ApiService { return retrofit.create(ApiService::class.java) } } ``` ## Authentication ```kotlin theme={null} // data/repository/AuthRepository.kt class AuthRepository @Inject constructor( private val api: ApiService, private val tokenManager: TokenManager ) { suspend fun login(email: String, password: String): Result { return try { val response = api.login(LoginRequest(email, password)) tokenManager.saveTokens(response.accessToken, response.refreshToken) Result.success(response.user) } catch (e: Exception) { Result.failure(e) } } suspend fun logout() { tokenManager.clearTokens() } fun isAuthenticated(): Boolean = tokenManager.hasValidToken() } // data/local/TokenManager.kt class TokenManager @Inject constructor( private val encryptedPrefs: SharedPreferences ) { fun saveTokens(accessToken: String, refreshToken: String) { encryptedPrefs.edit() .putString("access_token", accessToken) .putString("refresh_token", refreshToken) .apply() } fun getAccessToken(): String? = encryptedPrefs.getString("access_token", null) fun hasValidToken(): Boolean = getAccessToken() != null fun clearTokens() { encryptedPrefs.edit().clear().apply() } } ``` ## Compose UI ### Navigation ```kotlin theme={null} // ui/navigation/NavGraph.kt @Composable fun NavGraph( navController: NavHostController = rememberNavController(), startDestination: String = Screen.Home.route ) { NavHost(navController = navController, startDestination = startDestination) { composable(Screen.Home.route) { HomeScreen( onNavigateToDetail = { id -> navController.navigate(Screen.Detail.createRoute(id)) } ) } composable( route = Screen.Detail.route, arguments = listOf(navArgument("id") { type = NavType.StringType }) ) { backStackEntry -> DetailScreen( id = backStackEntry.arguments?.getString("id") ?: "", onBack = { navController.popBackStack() } ) } composable(Screen.Profile.route) { ProfileScreen() } } } sealed class Screen(val route: String) { object Home : Screen("home") object Profile : Screen("profile") object Detail : Screen("detail/{id}") { fun createRoute(id: String) = "detail/$id" } } ``` ### ViewModel ```kotlin theme={null} // ui/screens/home/HomeViewModel.kt @HiltViewModel class HomeViewModel @Inject constructor( private val repository: ItemRepository ) : ViewModel() { private val _uiState = MutableStateFlow(HomeUiState.Loading) val uiState: StateFlow = _uiState.asStateFlow() init { loadItems() } fun loadItems() { viewModelScope.launch { _uiState.value = HomeUiState.Loading repository.getItems() .onSuccess { items -> _uiState.value = HomeUiState.Success(items) } .onFailure { error -> _uiState.value = HomeUiState.Error(error.message ?: "Unknown error") } } } } sealed interface HomeUiState { object Loading : HomeUiState data class Success(val items: List) : HomeUiState data class Error(val message: String) : HomeUiState } ``` ### Screen ```kotlin theme={null} // ui/screens/home/HomeScreen.kt @Composable fun HomeScreen( viewModel: HomeViewModel = hiltViewModel(), onNavigateToDetail: (String) -> Unit ) { val uiState by viewModel.uiState.collectAsState() Scaffold( topBar = { TopAppBar(title = { Text("Home") }) } ) { padding -> when (val state = uiState) { is HomeUiState.Loading -> { Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { CircularProgressIndicator() } } is HomeUiState.Error -> { ErrorContent( message = state.message, onRetry = { viewModel.loadItems() }, modifier = Modifier.padding(padding) ) } is HomeUiState.Success -> { LazyColumn( modifier = Modifier.padding(padding), contentPadding = PaddingValues(16.dp) ) { items(state.items) { item -> ItemCard( item = item, onClick = { onNavigateToDetail(item.id) } ) } } } } } } ``` ## Push Notifications ```kotlin theme={null} // data/push/MyFirebaseMessagingService.kt class MyFirebaseMessagingService : FirebaseMessagingService() { @Inject lateinit var pushRepository: PushRepository override fun onCreate() { super.onCreate() // Inject dependencies EntryPointAccessors.fromApplication( applicationContext, PushServiceEntryPoint::class.java ).inject(this) } override fun onNewToken(token: String) { CoroutineScope(Dispatchers.IO).launch { pushRepository.registerToken(token) } } override fun onMessageReceived(message: RemoteMessage) { // Handle notification message.notification?.let { notification -> showNotification(notification.title, notification.body) } // Handle data payload message.data.let { data -> handlePushData(data) } } private fun showNotification(title: String?, body: String?) { val channelId = "default" val notificationManager = getSystemService(NotificationManager::class.java) val notification = NotificationCompat.Builder(this, channelId) .setContentTitle(title) .setContentText(body) .setSmallIcon(R.drawable.ic_notification) .setAutoCancel(true) .build() notificationManager.notify(System.currentTimeMillis().toInt(), notification) } } ``` ## Deep Links ```kotlin theme={null} // AndroidManifest.xml // MainActivity.kt class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MyAppTheme { val navController = rememberNavController() LaunchedEffect(Unit) { handleIntent(intent, navController) } NavGraph(navController = navController) } } } override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) // Handle deep link from new intent } private fun handleIntent(intent: Intent, navController: NavController) { intent.data?.let { uri -> when { uri.path?.startsWith("/share/") == true -> { val id = uri.path?.removePrefix("/share/") ?: return navController.navigate(Screen.Detail.createRoute(id)) } uri.path?.startsWith("/profile/") == true -> { navController.navigate(Screen.Profile.route) } } } } } ``` ## Offline Support ```kotlin theme={null} // data/repository/SyncRepository.kt class SyncRepository @Inject constructor( private val api: ApiService, private val database: AppDatabase, private val prefs: SharedPreferences ) { private val syncTokenKey = "sync_token" suspend fun sync(): Result { return try { val token = prefs.getString(syncTokenKey, null) val response = api.sync(token) // Apply changes in transaction database.withTransaction { response.created.forEach { database.itemDao().insert(it) } response.updated.forEach { database.itemDao().update(it) } response.deleted.forEach { database.itemDao().deleteById(it) } } // Save new token prefs.edit().putString(syncTokenKey, response.syncToken).apply() Result.success(Unit) } catch (e: Exception) { Result.failure(e) } } } ``` ## Running the Project ### Start Backend ```bash theme={null} cd backend make run ``` ### Run Android App 1. Open `android/` folder in Android Studio 2. Sync Gradle files 3. Select device or emulator 4. Click Run ### Build Configuration ```kotlin theme={null} // app/build.gradle.kts android { buildTypes { debug { buildConfigField("String", "API_BASE_URL", "\"http://10.0.2.2:3000\"") } release { buildConfigField("String", "API_BASE_URL", "\"https://api.example.com\"") isMinifyEnabled = true proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt")) } } } ``` ## Best Practices ### Error Handling ```kotlin theme={null} sealed class ApiResult { data class Success(val data: T) : ApiResult() data class Error(val exception: ApiException) : ApiResult() } suspend fun safeApiCall(call: suspend () -> T): ApiResult { return try { ApiResult.Success(call()) } catch (e: HttpException) { val error = parseError(e.response()) when (error.code) { "unauthorized" -> ApiResult.Error(UnauthorizedException()) "upgrade_required" -> ApiResult.Error(UpgradeRequiredException(error.details)) else -> ApiResult.Error(ServerException(error.message)) } } catch (e: Exception) { ApiResult.Error(NetworkException(e.message)) } } ``` ### Testing ```kotlin theme={null} @HiltAndroidTest class HomeViewModelTest { @get:Rule val hiltRule = HiltAndroidRule(this) @Inject lateinit var repository: FakeItemRepository private lateinit var viewModel: HomeViewModel @Before fun setup() { hiltRule.inject() viewModel = HomeViewModel(repository) } @Test fun `loadItems success updates state`() = runTest { repository.setItems(listOf(testItem)) viewModel.loadItems() val state = viewModel.uiState.first() assertThat(state).isInstanceOf(HomeUiState.Success::class.java) } } ``` ## Next Steps Build native iOS apps Cross-platform with Flutter FCM integration App Links setup # API Reference Source: https://docs.go-mizu.dev/mobile/api-reference Complete API documentation for the mobile package. Complete reference documentation for `github.com/go-mizu/mizu/mobile`. ## Middleware ### New ```go theme={null} func New() mizu.Middleware ``` Creates mobile middleware with default options. ### WithOptions ```go theme={null} func WithOptions(opts Options) mizu.Middleware ``` Creates mobile middleware with custom options. ### VersionMiddleware ```go theme={null} func VersionMiddleware(opts VersionOptions) mizu.Middleware ``` Creates API versioning middleware. ### UniversalLinkMiddleware ```go theme={null} func UniversalLinkMiddleware(cfg UniversalLinkConfig) mizu.Middleware ``` Creates middleware that serves deep link verification files. ## Types ### Options ```go theme={null} type Options struct { RequireDeviceID bool RequireAppVersion bool AllowedPlatforms []Platform MinAppVersion string SkipPaths []string SkipUserAgent bool OnMissingHeader func(c *mizu.Ctx, header string) error OnUnsupportedPlatform func(c *mizu.Ctx, platform Platform) error OnOutdatedApp func(c *mizu.Ctx, version, minimum string) error } ``` ### Device ```go theme={null} type Device struct { Platform Platform OSVersion string AppVersion string AppBuild string DeviceID string DeviceModel string Locale string Timezone string PushToken string PushProvider PushProvider UserAgent string } ``` ### Platform ```go theme={null} type Platform string const ( PlatformIOS Platform = "ios" PlatformAndroid Platform = "android" PlatformWindows Platform = "windows" PlatformMacOS Platform = "macos" PlatformWeb Platform = "web" PlatformUnknown Platform = "unknown" ) func (p Platform) String() string func (p Platform) IsMobile() bool func (p Platform) IsDesktop() bool func (p Platform) IsNative() bool ``` ### Version ```go theme={null} type Version struct { Major int Minor int } func ParseVersion(s string) (Version, error) func (v Version) String() string func (v Version) IsZero() bool func (v Version) Compare(other Version) int func (v Version) AtLeast(major, minor int) bool func (v Version) Before(major, minor int) bool ``` ### VersionOptions ```go theme={null} type VersionOptions struct { Header string QueryParam string PathPrefix bool Default Version Supported []Version Deprecated []Version EchoVersion bool OnUnsupported func(c *mizu.Ctx, v Version) error } ``` ### Error ```go theme={null} type Error struct { Code string `json:"code"` Message string `json:"message"` Details map[string]any `json:"details,omitempty"` TraceID string `json:"trace_id,omitempty"` } func NewError(code, message string) *Error func (e *Error) WithDetails(key string, value any) *Error func (e *Error) WithTraceID(traceID string) *Error func (e *Error) Error() string ``` ### SyncToken ```go theme={null} type SyncToken string func NewSyncToken(t time.Time) SyncToken func (t SyncToken) Time() time.Time func (t SyncToken) String() string func (t SyncToken) IsEmpty() bool ``` ### SyncRequest ```go theme={null} type SyncRequest struct { Token SyncToken Resources []string FullSync bool Limit int } func ParseSyncRequest(c *mizu.Ctx) SyncRequest func (r SyncRequest) Since() time.Time func (r SyncRequest) IsInitial() bool ``` ### Delta ```go theme={null} type Delta[T any] struct { Created []T `json:"created,omitempty"` Updated []T `json:"updated,omitempty"` Deleted []string `json:"deleted,omitempty"` } func (d Delta[T]) IsEmpty() bool func (d Delta[T]) Count() int ``` ### SyncDelta ```go theme={null} type SyncDelta[T any] struct { Delta[T] SyncToken SyncToken `json:"sync_token"` HasMore bool `json:"has_more"` FullSync bool `json:"full_sync,omitempty"` } func NewSyncDelta[T any](delta Delta[T], token SyncToken, hasMore bool) SyncDelta[T] func NewFullSyncDelta[T any](delta Delta[T], token SyncToken, hasMore bool) SyncDelta[T] ``` ### PushToken ```go theme={null} type PushToken struct { Token string `json:"token"` Provider PushProvider `json:"provider"` DeviceID string `json:"device_id,omitempty"` Sandbox bool `json:"sandbox,omitempty"` CreatedAt time.Time `json:"created_at,omitempty"` UpdatedAt time.Time `json:"updated_at,omitempty"` AppVersion string `json:"app_version,omitempty"` } func ParsePushToken(c *mizu.Ctx) *PushToken ``` ### PushProvider ```go theme={null} type PushProvider string const ( PushAPNS PushProvider = "apns" PushFCM PushProvider = "fcm" PushWNS PushProvider = "wns" ) ``` ### PushPayload ```go theme={null} type PushPayload struct { Title string `json:"title,omitempty"` Body string `json:"body,omitempty"` Badge *int `json:"badge,omitempty"` Sound string `json:"sound,omitempty"` Data map[string]any `json:"data,omitempty"` Category string `json:"category,omitempty"` ThreadID string `json:"thread_id,omitempty"` ChannelID string `json:"channel_id,omitempty"` CollapseKey string `json:"collapse_key,omitempty"` Priority string `json:"priority,omitempty"` TTL int `json:"ttl,omitempty"` ContentAvailable bool `json:"content_available,omitempty"` MutableContent bool `json:"mutable_content,omitempty"` } func (p *PushPayload) WithData(key string, value any) *PushPayload func (p *PushPayload) SetBadge(n int) *PushPayload func (p *PushPayload) ToAPNS() map[string]any func (p *PushPayload) ToFCM() map[string]any ``` ### DeepLink ```go theme={null} type DeepLink struct { Scheme string Host string Paths []string Fallback string } func (d DeepLink) AppleAppSiteAssociation(teamID, bundleID string) []byte func (d DeepLink) AssetLinks(packageName, fingerprint string) []byte ``` ### AppInfo ```go theme={null} type AppInfo struct { CurrentVersion string `json:"current_version"` MinimumVersion string `json:"minimum_version"` UpdateURL string `json:"update_url"` ReleaseNotes string `json:"release_notes,omitempty"` ReleasedAt time.Time `json:"released_at,omitempty"` ForceUpdate bool `json:"force_update"` MaintenanceMode bool `json:"maintenance_mode"` MaintenanceMessage string `json:"maintenance_message,omitempty"` MaintenanceEndTime *time.Time `json:"maintenance_end_time,omitempty"` Features map[string]bool `json:"features,omitempty"` } ``` ## Functions ### DeviceFromCtx ```go theme={null} func DeviceFromCtx(c *mizu.Ctx) *Device ``` Extracts Device from request context. ### VersionFromCtx ```go theme={null} func VersionFromCtx(c *mizu.Ctx) Version ``` Extracts Version from request context. ### SendError ```go theme={null} func SendError(c *mizu.Ctx, status int, err *Error) error ``` Sends a structured error response. ### SetSyncToken ```go theme={null} func SetSyncToken(c *mizu.Ctx, token SyncToken) ``` Sets the sync token response header. ### ValidateToken ```go theme={null} func ValidateToken(token string, provider PushProvider) bool ``` Validates a push token format. ### ValidateAPNS ```go theme={null} func ValidateAPNS(token string) bool ``` Validates APNS token format. ### ValidateFCM ```go theme={null} func ValidateFCM(token string) bool ``` Validates FCM token format. ### CheckUpdate ```go theme={null} func CheckUpdate(clientVersion, latestVersion, minimumVersion string) UpdateStatus ``` Compares client version against store versions. ### NewStaticAppInfo ```go theme={null} func NewStaticAppInfo(currentVersion, minVersion, updateURL string) *StaticAppInfo ``` Creates a StaticAppInfo provider. ### NewIOSStoreURL ```go theme={null} func NewIOSStoreURL(appID string) string ``` Creates an iOS App Store URL. ### NewAndroidStoreURL ```go theme={null} func NewAndroidStoreURL(packageName string) string ``` Creates a Google Play Store URL. ## Constants ### Error Codes ```go theme={null} const ( ErrInvalidRequest = "invalid_request" ErrUnauthorized = "unauthorized" ErrForbidden = "forbidden" ErrNotFound = "not_found" ErrConflict = "conflict" ErrValidation = "validation_error" ErrRateLimited = "rate_limited" ErrUpgradeRequired = "upgrade_required" ErrMaintenance = "maintenance" ErrInternal = "internal_error" ) ``` ### Headers ```go theme={null} const ( HeaderDeviceID = "X-Device-ID" HeaderAppVersion = "X-App-Version" HeaderAppBuild = "X-App-Build" HeaderDeviceModel = "X-Device-Model" HeaderPlatform = "X-Platform" HeaderOSVersion = "X-OS-Version" HeaderTimezone = "X-Timezone" HeaderLocale = "X-Locale" HeaderPushToken = "X-Push-Token" HeaderAPIVersion = "X-API-Version" HeaderSyncToken = "X-Sync-Token" HeaderIdempotency = "X-Idempotency-Key" HeaderRequestID = "X-Request-ID" HeaderMinVersion = "X-Min-App-Version" HeaderDeprecated = "X-API-Deprecated" ) ``` # App Store Integration Source: https://docs.go-mizu.dev/mobile/appstore Version checking, force updates, and maintenance mode. The mobile package provides app store integration features including version checking, force update prompts, maintenance mode, and platform-specific store URLs. ## Quick Start ```go theme={null} import "github.com/go-mizu/mizu/mobile" // Create app info provider provider := mobile.NewStaticAppInfo( "2.0.0", // Current version "1.5.0", // Minimum version "https://apps.apple.com/app/id123456789", // Store URL ) // Add version check endpoint app.Get("/api/app-info", mobile.AppInfoHandler(provider)) ``` ## App Info ```go theme={null} type AppInfo struct { // CurrentVersion is the latest version in the store CurrentVersion string `json:"current_version"` // MinimumVersion is the minimum required version MinimumVersion string `json:"minimum_version"` // UpdateURL is the store URL for updating UpdateURL string `json:"update_url"` // ReleaseNotes is the latest release notes ReleaseNotes string `json:"release_notes,omitempty"` // ReleasedAt is when the current version was released ReleasedAt time.Time `json:"released_at,omitempty"` // ForceUpdate indicates if update is mandatory ForceUpdate bool `json:"force_update"` // MaintenanceMode indicates if the service is under maintenance MaintenanceMode bool `json:"maintenance_mode"` // MaintenanceMessage is the message to display during maintenance MaintenanceMessage string `json:"maintenance_message,omitempty"` // MaintenanceEndTime is when maintenance is expected to end MaintenanceEndTime *time.Time `json:"maintenance_end_time,omitempty"` // Features lists enabled feature flags Features map[string]bool `json:"features,omitempty"` } ``` ## Static App Info Provider For simple static configuration: ```go theme={null} provider := mobile.NewStaticAppInfo( "2.0.0", // Current version in store "1.5.0", // Minimum required version "https://apps.apple.com/app/id123456789", ) // Add platform-specific info provider.WithPlatformApp(mobile.PlatformIOS, "*", &mobile.AppInfo{ CurrentVersion: "2.0.0", MinimumVersion: "1.5.0", UpdateURL: "https://apps.apple.com/app/id123456789", ReleaseNotes: "Bug fixes and performance improvements", }) provider.WithPlatformApp(mobile.PlatformAndroid, "*", &mobile.AppInfo{ CurrentVersion: "2.0.1", // Android may have different version MinimumVersion: "1.5.0", UpdateURL: "https://play.google.com/store/apps/details?id=com.example.app", ReleaseNotes: "Bug fixes and performance improvements", }) ``` ## Dynamic App Info Provider For database-backed or API-driven configuration: ```go theme={null} type DBAppInfoProvider struct { db *sql.DB } func (p *DBAppInfoProvider) GetAppInfo( ctx context.Context, platform mobile.Platform, bundleID string, ) (*mobile.AppInfo, error) { var info mobile.AppInfo err := p.db.QueryRowContext(ctx, ` SELECT current_version, minimum_version, update_url, release_notes, released_at, force_update, maintenance_mode, maintenance_message, maintenance_end_time FROM app_versions WHERE platform = $1 AND (bundle_id = $2 OR bundle_id = '*') ORDER BY CASE WHEN bundle_id = $2 THEN 0 ELSE 1 END LIMIT 1 `, platform.String(), bundleID).Scan( &info.CurrentVersion, &info.MinimumVersion, &info.UpdateURL, &info.ReleaseNotes, &info.ReleasedAt, &info.ForceUpdate, &info.MaintenanceMode, &info.MaintenanceMessage, &info.MaintenanceEndTime, ) if err == sql.ErrNoRows { return nil, nil } if err != nil { return nil, err } return &info, nil } // Use it provider := &DBAppInfoProvider{db: db} app.Get("/api/app-info", mobile.AppInfoHandler(provider)) ``` ## Update Status The `CheckUpdate` function compares versions: ```go theme={null} status := mobile.CheckUpdate( "1.4.0", // Client version "2.0.0", // Latest version "1.5.0", // Minimum version ) // status.Available = true (1.4.0 < 2.0.0) // status.Required = true (1.4.0 < 1.5.0) ``` ### Update Status Response ```go theme={null} type UpdateStatus struct { Available bool `json:"update_available"` Required bool `json:"update_required"` CurrentVersion string `json:"current_version"` LatestVersion string `json:"latest_version"` MinimumVersion string `json:"minimum_version,omitempty"` UpdateURL string `json:"update_url,omitempty"` ReleaseNotes string `json:"release_notes,omitempty"` } ``` Example response: ```json theme={null} { "update_available": true, "update_required": true, "current_version": "1.4.0", "latest_version": "2.0.0", "minimum_version": "1.5.0", "update_url": "https://apps.apple.com/app/id123456789", "release_notes": "New features and bug fixes" } ``` ## App Info Handler The built-in handler automatically: 1. Extracts device info from mobile middleware context 2. Fetches app info from provider 3. Checks for maintenance mode 4. Compares versions and builds response 5. Sets `X-Min-App-Version` header ```go theme={null} app.Use(mobile.New()) app.Get("/api/app-info", mobile.AppInfoHandler(provider)) ``` ### Maintenance Mode Response When maintenance mode is enabled: ```json theme={null} { "maintenance": true, "message": "We're upgrading our servers. Back soon!", "end_time": "2024-01-15T10:00:00Z", "minimum_version": "1.5.0" } ``` HTTP Status: `503 Service Unavailable` ## Store URLs Helper functions for platform store URLs: ```go theme={null} // iOS App Store url := mobile.NewIOSStoreURL("123456789") // "https://apps.apple.com/app/id123456789" // Google Play Store url := mobile.NewAndroidStoreURL("com.example.app") // "https://play.google.com/store/apps/details?id=com.example.app" ``` ### Store URLs by Platform ```go theme={null} type StoreURLs struct { iOS string `json:"ios,omitempty"` Android string `json:"android,omitempty"` Windows string `json:"windows,omitempty"` Web string `json:"web,omitempty"` } urls := mobile.StoreURLs{ iOS: "https://apps.apple.com/app/id123456789", Android: "https://play.google.com/store/apps/details?id=com.example.app", Windows: "ms-windows-store://pdp/?ProductId=12345", Web: "https://example.com/download", } // Get URL for platform url := urls.URLFor(device.Platform) ``` ## Force Update Middleware Block outdated apps from accessing the API: ```go theme={null} func forceUpdateMiddleware(provider mobile.AppInfoProvider) mizu.Middleware { return func(next mizu.Handler) mizu.Handler { return func(c *mizu.Ctx) error { device := mobile.DeviceFromCtx(c) if device == nil { return next(c) } info, err := provider.GetAppInfo(c.Context(), device.Platform, "") if err != nil || info == nil { return next(c) } // Check maintenance if info.MaintenanceMode { return mobile.SendError(c, 503, mobile.NewError( mobile.ErrMaintenance, info.MaintenanceMessage, ).WithDetails("end_time", info.MaintenanceEndTime)) } // Check version if device.AppVersion != "" && info.MinimumVersion != "" { if mobile.CompareVersions(device.AppVersion, info.MinimumVersion) < 0 { c.Header().Set(mobile.HeaderMinVersion, info.MinimumVersion) return mobile.SendError(c, 426, mobile.NewError( mobile.ErrUpgradeRequired, "Please update to continue using the app", ).WithDetails("current_version", device.AppVersion). WithDetails("minimum_version", info.MinimumVersion). WithDetails("update_url", info.UpdateURL)) } } return next(c) } } } // Apply to all API routes app.Use(forceUpdateMiddleware(provider)) ``` ## Feature Flags Include feature flags in app info: ```go theme={null} provider := &FeatureFlagProvider{db: db} func (p *FeatureFlagProvider) GetAppInfo( ctx context.Context, platform mobile.Platform, bundleID string, ) (*mobile.AppInfo, error) { info := &mobile.AppInfo{ CurrentVersion: "2.0.0", MinimumVersion: "1.5.0", Features: map[string]bool{ "dark_mode": true, "new_checkout": platform == mobile.PlatformIOS, // iOS only "beta_features": false, }, } return info, nil } ``` Response: ```json theme={null} { "update_available": false, "update_required": false, "current_version": "2.0.0", "features": { "dark_mode": true, "new_checkout": true, "beta_features": false } } ``` ## Client Implementation ### iOS (Swift) ```swift theme={null} struct AppInfo: Decodable { let updateAvailable: Bool let updateRequired: Bool let currentVersion: String let latestVersion: String let minimumVersion: String? let updateUrl: String? let releaseNotes: String? let features: [String: Bool]? enum CodingKeys: String, CodingKey { case updateAvailable = "update_available" case updateRequired = "update_required" case currentVersion = "current_version" case latestVersion = "latest_version" case minimumVersion = "minimum_version" case updateUrl = "update_url" case releaseNotes = "release_notes" case features } } class AppUpdateManager { func checkForUpdates() async { do { let info: AppInfo = try await api.get("/api/app-info") if info.updateRequired { showForceUpdateAlert(info) } else if info.updateAvailable { showOptionalUpdateAlert(info) } // Cache feature flags FeatureFlags.shared.update(info.features ?? [:]) } catch let error as APIException where error.isMaintenance { showMaintenanceScreen(error) } catch { // Handle silently or show error } } private func showForceUpdateAlert(_ info: AppInfo) { let alert = UIAlertController( title: "Update Required", message: "Please update to continue using the app.", preferredStyle: .alert ) alert.addAction(UIAlertAction(title: "Update", style: .default) { _ in if let url = URL(string: info.updateUrl ?? "") { UIApplication.shared.open(url) } }) // No cancel button - force update rootViewController?.present(alert, animated: true) } } ``` ### Android (Kotlin) ```kotlin theme={null} data class AppInfo( @SerializedName("update_available") val updateAvailable: Boolean, @SerializedName("update_required") val updateRequired: Boolean, @SerializedName("current_version") val currentVersion: String, @SerializedName("latest_version") val latestVersion: String, @SerializedName("minimum_version") val minimumVersion: String?, @SerializedName("update_url") val updateUrl: String?, @SerializedName("release_notes") val releaseNotes: String?, val features: Map? ) class AppUpdateManager( private val api: ApiService, private val context: Context ) { suspend fun checkForUpdates() { try { val info = api.getAppInfo() when { info.updateRequired -> showForceUpdateDialog(info) info.updateAvailable -> showOptionalUpdateDialog(info) } // Update feature flags info.features?.let { FeatureFlags.update(it) } } catch (e: MaintenanceException) { showMaintenanceScreen(e.message, e.endTime) } } private fun showForceUpdateDialog(info: AppInfo) { AlertDialog.Builder(context) .setTitle("Update Required") .setMessage("Please update to continue using the app.") .setCancelable(false) .setPositiveButton("Update") { _, _ -> val intent = Intent(Intent.ACTION_VIEW).apply { data = Uri.parse(info.updateUrl) } context.startActivity(intent) } .show() } } ``` ### Flutter (Dart) ```dart theme={null} class AppInfo { final bool updateAvailable; final bool updateRequired; final String currentVersion; final String latestVersion; final String? minimumVersion; final String? updateUrl; final String? releaseNotes; final Map? features; AppInfo.fromJson(Map json) : updateAvailable = json['update_available'], updateRequired = json['update_required'], currentVersion = json['current_version'], latestVersion = json['latest_version'], minimumVersion = json['minimum_version'], updateUrl = json['update_url'], releaseNotes = json['release_notes'], features = json['features']?.cast(); } class AppUpdateService { Future checkForUpdates(BuildContext context) async { try { final response = await api.get('/api/app-info'); final info = AppInfo.fromJson(response.data); if (info.updateRequired) { _showForceUpdateDialog(context, info); } else if (info.updateAvailable) { _showOptionalUpdateDialog(context, info); } // Update feature flags if (info.features != null) { FeatureFlags.update(info.features!); } } on MaintenanceException catch (e) { _showMaintenanceScreen(context, e); } } void _showForceUpdateDialog(BuildContext context, AppInfo info) { showDialog( context: context, barrierDismissible: false, builder: (context) => AlertDialog( title: const Text('Update Required'), content: const Text('Please update to continue using the app.'), actions: [ TextButton( onPressed: () => launchUrl(Uri.parse(info.updateUrl!)), child: const Text('Update'), ), ], ), ); } } ``` ## Best Practices ### Check on App Launch Check for updates when the app launches and periodically while running: ```go theme={null} // Backend: track when clients check app.Get("/api/app-info", func(c *mizu.Ctx) error { device := mobile.DeviceFromCtx(c) // Log version distribution metrics.RecordAppVersion(device.Platform, device.AppVersion) return mobile.AppInfoHandler(provider)(c) }) ``` ### Gradual Rollouts Use minimum version to gradually force updates: 1. Release 2.0.0 to stores 2. Set minimum to 1.8.0 initially 3. Monitor crash rates and feedback 4. Gradually raise minimum to 2.0.0 ### Maintenance Windows Schedule maintenance during low-traffic periods: ```go theme={null} func (p *DBProvider) GetAppInfo(ctx context.Context, platform mobile.Platform, bundleID string) (*mobile.AppInfo, error) { info := &mobile.AppInfo{ CurrentVersion: "2.0.0", MinimumVersion: "1.5.0", } // Check scheduled maintenance maintenance, _ := p.db.GetScheduledMaintenance(time.Now()) if maintenance != nil { info.MaintenanceMode = true info.MaintenanceMessage = maintenance.Message info.MaintenanceEndTime = &maintenance.EndTime } return info, nil } ``` ## Next Steps Available mobile templates Production deployment guide Complete API documentation # Deep Links Source: https://docs.go-mizu.dev/mobile/deeplinks Universal Links for iOS and App Links for Android. The mobile package provides automatic generation of deep link verification files and handlers for Universal Links (iOS) and App Links (Android), enabling seamless app-to-web navigation. ## Quick Start ```go theme={null} import "github.com/go-mizu/mizu/mobile" app := mizu.New() // Add Universal/App Links middleware app.Use(mobile.UniversalLinkMiddleware(mobile.UniversalLinkConfig{ Apple: []mobile.AppleAppConfig{ { TeamID: "ABCD1234XY", BundleID: "com.example.app", Paths: []string{"/share/*", "/invite/*"}, }, }, Android: []mobile.AndroidAppConfig{ { PackageName: "com.example.app", Fingerprints: []string{"AA:BB:CC:DD:..."}, Paths: []string{"/share/*", "/invite/*"}, }, }, })) ``` This automatically serves: * `/.well-known/apple-app-site-association` for iOS * `/.well-known/assetlinks.json` for Android ## How Deep Links Work ### Universal Links (iOS) 1. User taps a link to your domain 2. iOS checks `/.well-known/apple-app-site-association` 3. If app is installed and paths match, app opens directly 4. Otherwise, Safari opens the URL ### App Links (Android) 1. User taps a link to your domain 2. Android checks `/.well-known/assetlinks.json` 3. If app is installed and verified, app opens 4. Otherwise, browser opens the URL ## Configuration ### Apple App Configuration ```go theme={null} type AppleAppConfig struct { // TeamID is the Apple Developer Team ID (10 characters) TeamID string // BundleID is the iOS app bundle identifier BundleID string // Paths are the URL paths to handle (supports * wildcards) Paths []string } ``` Find your Team ID in the [Apple Developer Portal](https://developer.apple.com/account). ### Android App Configuration ```go theme={null} type AndroidAppConfig struct { // PackageName is the Android app package name PackageName string // Fingerprints are SHA256 certificate fingerprints Fingerprints []string // Paths are the URL paths to handle Paths []string } ``` Get your signing certificate fingerprint: ```bash theme={null} # For debug keystore keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android # For release keystore keytool -list -v -keystore your-release-key.keystore -alias your-alias ``` ### Full Configuration ```go theme={null} app.Use(mobile.UniversalLinkMiddleware(mobile.UniversalLinkConfig{ Apple: []mobile.AppleAppConfig{ { TeamID: "ABCD1234XY", BundleID: "com.example.app", Paths: []string{"*"}, // All paths }, { TeamID: "ABCD1234XY", BundleID: "com.example.app.dev", Paths: []string{"/dev/*"}, // Dev app for /dev/* only }, }, Android: []mobile.AndroidAppConfig{ { PackageName: "com.example.app", Fingerprints: []string{ "AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99", }, }, }, WebCredentials: []string{ "com.example.app", // For password autofill }, Fallback: "https://example.com", })) ``` ## Generated Files ### apple-app-site-association ```json theme={null} { "applinks": { "details": [ { "appIDs": ["ABCD1234XY.com.example.app"], "components": [ {"/": "/share/*"}, {"/": "/invite/*"} ] } ] } } ``` ### assetlinks.json ```json theme={null} [ { "relation": ["delegate_permission/common.handle_all_urls"], "target": { "namespace": "android_app", "package_name": "com.example.app", "sha256_cert_fingerprints": [ "AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99" ] } } ] ``` ## Simple Deep Link Configuration For simple single-app setups: ```go theme={null} link := mobile.DeepLink{ Scheme: "myapp", // Custom URL scheme Host: "example.com", // Universal link domain Paths: []string{"/share/*", "/invite/*"}, Fallback: "https://example.com", } app.Use(mobile.DeepLinkMiddleware( link, "ABCD1234XY", // Apple Team ID "com.example.app", // iOS Bundle ID "com.example.app", // Android Package "AA:BB:CC:...", // Android SHA256 fingerprint )) ``` ## Deep Link Handler Handle deep links with smart fallback to web: ```go theme={null} // Redirect to app or web based on device app.Get("/share/:id", mobile.DeepLinkHandler( "myapp", // URL scheme "https://example.com/share", // Web fallback )) ``` ### How It Works 1. Parses device from mobile middleware context 2. For mobile devices: renders HTML that attempts app deep link 3. Falls back to web URL after 2.5 seconds 4. For desktop/web: redirects directly to fallback ### Generated HTML For mobile devices, serves: ```html theme={null} Opening App...

    Opening app...

    If the app doesn't open, click here

    ``` ## Custom Deep Link Handling ```go theme={null} app.Get("/invite/:code", func(c *mizu.Ctx) error { code := c.Param("code") device := mobile.DeviceFromCtx(c) // Validate invite code invite, err := db.GetInvite(code) if err != nil { return c.Redirect(302, "https://example.com/invalid-invite") } // Build deep link deepLink := fmt.Sprintf("myapp://invite/%s", code) fallback := fmt.Sprintf("https://example.com/invite/%s", code) // Mobile: try app first if device != nil && device.Platform.IsMobile() { return c.HTML(200, renderDeepLinkPage(deepLink, fallback)) } // Desktop: show web page return c.Redirect(302, fallback) }) func renderDeepLinkPage(deepLink, fallback string) string { return fmt.Sprintf(` Opening App...

    Opening in App...

    If the app doesn't open automatically:

    Open in Browser `, fallback, deepLink, fallback) } ``` ## Path Patterns ### Wildcard Matching ```go theme={null} Paths: []string{ "/share/*", // Matches /share/anything "/user/*/posts", // Matches /user/123/posts "*", // Matches all paths } ``` ### Excluding Paths (iOS) ```go theme={null} // In iOS, prefix with "NOT " to exclude Paths: []string{ "*", // Match all "NOT /api/*", // Except API routes "NOT /admin/*", // Except admin routes } ``` ## Deferred Deep Links Handle deep links when app is installed after click: ```go theme={null} // Store pending deep link app.Get("/invite/:code", func(c *mizu.Ctx) error { code := c.Param("code") device := mobile.DeviceFromCtx(c) // Generate unique tracking ID trackingID := uuid.New().String() // Store pending deep link cache.Set(trackingID, &PendingDeepLink{ Path: "/invite/" + code, CreatedAt: time.Now(), DeviceInfo: device, }, 24*time.Hour) // Set cookie for web fallback c.Cookie(&http.Cookie{ Name: "pending_deeplink", Value: trackingID, MaxAge: 86400, Path: "/", HttpOnly: true, Secure: true, }) // Try to open app return mobile.DeepLinkHandler("myapp", "/install?track="+trackingID)(c) }) // Called when app opens for first time app.Get("/api/check-pending-deeplink", func(c *mizu.Ctx) error { device := mobile.DeviceFromCtx(c) // Find pending deep link for this device pending, err := db.FindPendingDeepLink(device.DeviceID) if err != nil || pending == nil { return c.JSON(200, map[string]any{"found": false}) } // Mark as claimed db.ClaimPendingDeepLink(pending.ID) return c.JSON(200, map[string]any{ "found": true, "path": pending.Path, }) }) ``` ## Testing Deep Links ### iOS Simulator ```bash theme={null} # Test Universal Link xcrun simctl openurl booted "https://example.com/share/123" # Test Custom Scheme xcrun simctl openurl booted "myapp://share/123" ``` ### Android Emulator ```bash theme={null} # Test App Link adb shell am start -a android.intent.action.VIEW \ -d "https://example.com/share/123" \ com.example.app # Test Custom Scheme adb shell am start -a android.intent.action.VIEW \ -d "myapp://share/123" \ com.example.app ``` ### Verify Configuration ```bash theme={null} # Check apple-app-site-association curl https://example.com/.well-known/apple-app-site-association # Check assetlinks.json curl https://example.com/.well-known/assetlinks.json # Validate AASA (Apple tool) # https://search.developer.apple.com/appsearch-validation-tool/ # Validate Android (Google tool) # https://developers.google.com/digital-asset-links/tools/generator ``` ## Client Implementation ### iOS (Swift) ```swift theme={null} // In SceneDelegate func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, let url = userActivity.webpageURL else { return } handleDeepLink(url) } func handleDeepLink(_ url: URL) { guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { return } switch components.path { case let path where path.hasPrefix("/share/"): let id = String(path.dropFirst("/share/".count)) router.navigate(to: .share(id: id)) case let path where path.hasPrefix("/invite/"): let code = String(path.dropFirst("/invite/".count)) router.navigate(to: .invite(code: code)) default: router.navigate(to: .home) } } ``` ### Android (Kotlin) ```kotlin theme={null} // In AndroidManifest.xml // In MainActivity override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) handleIntent(intent) } override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) handleIntent(intent) } private fun handleIntent(intent: Intent) { val data = intent.data ?: return when { data.path?.startsWith("/share/") == true -> { val id = data.path?.removePrefix("/share/") navigateToShare(id) } data.path?.startsWith("/invite/") == true -> { val code = data.path?.removePrefix("/invite/") navigateToInvite(code) } } } ``` ### Flutter (Dart) ```dart theme={null} import 'package:uni_links/uni_links.dart'; class DeepLinkService { StreamSubscription? _subscription; void initialize() { // Handle initial link getInitialUri().then(_handleUri); // Handle incoming links _subscription = uriLinkStream.listen(_handleUri); } void _handleUri(Uri? uri) { if (uri == null) return; final path = uri.path; if (path.startsWith('/share/')) { final id = path.replaceFirst('/share/', ''); router.go('/share/$id'); } else if (path.startsWith('/invite/')) { final code = path.replaceFirst('/invite/', ''); router.go('/invite/$code'); } } void dispose() { _subscription?.cancel(); } } ``` ## Best Practices ### HTTPS Required Both Universal Links and App Links require HTTPS. The verification files must be served over HTTPS. ### CDN Considerations If using a CDN, ensure the verification files are not cached aggressively: ```go theme={null} app.Get("/.well-known/*", func(c *mizu.Ctx) error { c.Header().Set("Cache-Control", "max-age=3600") // 1 hour return next(c) }) ``` ### Handle All Cases Always provide fallbacks for: * App not installed * Outdated app version * Invalid deep link paths ### Track Deep Link Attribution Track where users come from: ```go theme={null} app.Get("/share/:id", func(c *mizu.Ctx) error { source := c.Query("utm_source", "direct") campaign := c.Query("utm_campaign", "") analytics.Track("deep_link_clicked", map[string]any{ "path": c.Request().URL.Path, "source": source, "campaign": campaign, "platform": mobile.DeviceFromCtx(c).Platform.String(), }) // Continue with deep link handling... }) ``` ## Next Steps Version checking and force updates Cross-platform push support Complete API documentation # Device Detection Source: https://docs.go-mizu.dev/mobile/device Parse and validate device information from mobile clients. The mobile middleware automatically parses device information from HTTP headers and User-Agent strings, providing rich context about the client device in your handlers. ## Quick Start ```go theme={null} import "github.com/go-mizu/mizu/mobile" app := mizu.New() // Add mobile middleware app.Use(mobile.New()) app.Get("/api/profile", func(c *mizu.Ctx) error { device := mobile.DeviceFromCtx(c) return c.JSON(200, map[string]any{ "platform": device.Platform.String(), "os_version": device.OSVersion, "app_version": device.AppVersion, "device_id": device.DeviceID, "model": device.DeviceModel, "timezone": device.Timezone, "locale": device.Locale, }) }) ``` ## The Device Struct ```go theme={null} type Device struct { // Platform is the operating system (ios, android, windows, etc.) Platform Platform // OSVersion is the OS version (e.g., "17.0", "14.0") OSVersion string // AppVersion is the client app version (e.g., "1.2.3") AppVersion string // AppBuild is the build number (e.g., "123", "2024.01.15") AppBuild string // DeviceID is a unique device identifier DeviceID string // DeviceModel is the device model (e.g., "iPhone15,2", "Pixel 8") DeviceModel string // Locale is the device locale (e.g., "en-US", "ja-JP") Locale string // Timezone is the IANA timezone (e.g., "America/New_York") Timezone string // PushToken is the push notification token (if provided) PushToken string // PushProvider is APNS, FCM, or WNS PushProvider PushProvider // UserAgent is the raw User-Agent header UserAgent string } ``` ## Platform Types The `Platform` type represents the client operating system: ```go theme={null} const ( PlatformIOS Platform = "ios" PlatformAndroid Platform = "android" PlatformWindows Platform = "windows" PlatformMacOS Platform = "macos" PlatformWeb Platform = "web" PlatformUnknown Platform = "unknown" ) ``` ### Platform Methods ```go theme={null} // String returns the platform as a string device.Platform.String() // "ios" // IsMobile returns true for iOS and Android device.Platform.IsMobile() // true // IsDesktop returns true for Windows and macOS device.Platform.IsDesktop() // false // IsNative returns true for all except web and unknown device.Platform.IsNative() // true ``` ## Header Parsing The middleware parses these standard headers: | Header | Field | Example | | ---------------- | ----------- | -------------------------------------- | | `X-Device-ID` | DeviceID | `550e8400-e29b-41d4-a716-446655440000` | | `X-App-Version` | AppVersion | `2.1.0` | | `X-App-Build` | AppBuild | `2024.01.15` | | `X-Device-Model` | DeviceModel | `iPhone15,2` | | `X-Platform` | Platform | `ios` | | `X-OS-Version` | OSVersion | `17.0` | | `X-Timezone` | Timezone | `America/New_York` | | `X-Locale` | Locale | `en-US` | | `X-Push-Token` | PushToken | `abc123...` | ### Platform Detection Priority 1. **Explicit header** - `X-Platform` header takes priority 2. **User-Agent parsing** - Falls back to User-Agent analysis 3. **Unknown** - Default if no detection succeeds ```go theme={null} // Header takes priority // Request: X-Platform: ios device.Platform // PlatformIOS // Falls back to User-Agent // Request: User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 17_0...) device.Platform // PlatformIOS ``` ## Configuration Options ### Basic Options ```go theme={null} app.Use(mobile.WithOptions(mobile.Options{ // Require X-Device-ID header RequireDeviceID: true, // Require X-App-Version header RequireAppVersion: true, // Skip specific paths SkipPaths: []string{"/health", "/metrics"}, })) ``` ### Platform Restrictions ```go theme={null} app.Use(mobile.WithOptions(mobile.Options{ // Only allow iOS and Android AllowedPlatforms: []mobile.Platform{ mobile.PlatformIOS, mobile.PlatformAndroid, }, })) ``` ### Minimum App Version ```go theme={null} app.Use(mobile.WithOptions(mobile.Options{ RequireAppVersion: true, MinAppVersion: "1.5.0", })) ``` Requests with versions below 1.5.0 receive: ```json theme={null} { "code": "upgrade_required", "message": "Please update to the latest version", "details": { "current_version": "1.4.0", "minimum_version": "1.5.0" } } ``` With response header: `X-Min-App-Version: 1.5.0` ### Custom Error Handlers ```go theme={null} app.Use(mobile.WithOptions(mobile.Options{ RequireDeviceID: true, OnMissingHeader: func(c *mizu.Ctx, header string) error { return c.JSON(400, map[string]string{ "error": "Missing header: " + header, }) }, OnUnsupportedPlatform: func(c *mizu.Ctx, platform mobile.Platform) error { return c.JSON(400, map[string]string{ "error": "Platform not supported: " + platform.String(), }) }, OnOutdatedApp: func(c *mizu.Ctx, version, minimum string) error { return c.JSON(426, map[string]any{ "error": "Update required", "current_version": version, "minimum_version": minimum, "store_url": "https://apps.apple.com/app/id123", }) }, })) ``` ### Performance Optimization ```go theme={null} app.Use(mobile.WithOptions(mobile.Options{ // Skip User-Agent parsing for performance // Only use explicit headers SkipUserAgent: true, })) ``` ## Using Device Context ### Access in Handlers ```go theme={null} func handler(c *mizu.Ctx) error { device := mobile.DeviceFromCtx(c) // Device may be nil if middleware not applied if device == nil { return c.JSON(400, "Device context not available") } // Use device info log.Printf("Request from %s app v%s", device.Platform, device.AppVersion) return c.JSON(200, data) } ``` ### Platform-Specific Logic ```go theme={null} func handler(c *mizu.Ctx) error { device := mobile.DeviceFromCtx(c) response := map[string]any{ "features": getFeatures(), } switch device.Platform { case mobile.PlatformIOS: response["store_url"] = "https://apps.apple.com/app/id123" response["review_url"] = "https://apps.apple.com/app/id123?action=write-review" case mobile.PlatformAndroid: response["store_url"] = "https://play.google.com/store/apps/details?id=com.example" response["review_url"] = "market://details?id=com.example" case mobile.PlatformWeb: response["signup_url"] = "https://example.com/signup" } return c.JSON(200, response) } ``` ### Version-Based Features ```go theme={null} func handler(c *mizu.Ctx) error { device := mobile.DeviceFromCtx(c) features := map[string]bool{ "dark_mode": true, "biometrics": true, } // Feature requires app version 2.0+ if mobile.CompareVersions(device.AppVersion, "2.0.0") >= 0 { features["widgets"] = true } // Feature requires iOS 16+ if device.Platform == mobile.PlatformIOS { if mobile.CompareVersions(device.OSVersion, "16.0") >= 0 { features["live_activities"] = true } } return c.JSON(200, features) } ``` ### Timezone-Aware Responses ```go theme={null} func handler(c *mizu.Ctx) error { device := mobile.DeviceFromCtx(c) loc, err := time.LoadLocation(device.Timezone) if err != nil { loc = time.UTC } events := getEvents() for i := range events { events[i].LocalTime = events[i].Time.In(loc).Format(time.RFC3339) } return c.JSON(200, events) } ``` ### Localized Responses ```go theme={null} func handler(c *mizu.Ctx) error { device := mobile.DeviceFromCtx(c) // Parse locale lang := "en" if device.Locale != "" { lang = strings.Split(device.Locale, "-")[0] } messages := map[string]map[string]string{ "en": {"welcome": "Welcome!"}, "es": {"welcome": "Bienvenido!"}, "ja": {"welcome": "ようこそ!"}, } msg, ok := messages[lang] if !ok { msg = messages["en"] } return c.JSON(200, msg) } ``` ## User-Agent Patterns The middleware recognizes these User-Agent patterns: **iOS:** ``` Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)... Mozilla/5.0 (iPad; CPU OS 17_0 like Mac OS X)... ``` **Android:** ``` Mozilla/5.0 (Linux; Android 14; Pixel 8)... ``` **Windows:** ``` Mozilla/5.0 (Windows NT 10.0; Win64; x64)... ``` **macOS:** ``` Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)... ``` ## Client Implementation ### iOS (Swift) ```swift theme={null} extension URLRequest { mutating func addMizuHeaders(deviceId: String) { setValue(deviceId, forHTTPHeaderField: "X-Device-ID") setValue(Bundle.main.appVersion, forHTTPHeaderField: "X-App-Version") setValue(Bundle.main.buildNumber, forHTTPHeaderField: "X-App-Build") setValue(UIDevice.current.modelIdentifier, forHTTPHeaderField: "X-Device-Model") setValue("ios", forHTTPHeaderField: "X-Platform") setValue(UIDevice.current.systemVersion, forHTTPHeaderField: "X-OS-Version") setValue(TimeZone.current.identifier, forHTTPHeaderField: "X-Timezone") setValue(Locale.current.identifier, forHTTPHeaderField: "X-Locale") } } extension Bundle { var appVersion: String { infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0" } var buildNumber: String { infoDictionary?["CFBundleVersion"] as? String ?? "0" } } ``` ### Android (Kotlin) ```kotlin theme={null} class MizuHeaderInterceptor( private val context: Context, private val deviceId: String ) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request().newBuilder() .header("X-Device-ID", deviceId) .header("X-App-Version", BuildConfig.VERSION_NAME) .header("X-App-Build", BuildConfig.VERSION_CODE.toString()) .header("X-Device-Model", Build.MODEL) .header("X-Platform", "android") .header("X-OS-Version", Build.VERSION.RELEASE) .header("X-Timezone", TimeZone.getDefault().id) .header("X-Locale", Locale.getDefault().toLanguageTag()) .build() return chain.proceed(request) } } ``` ### Flutter (Dart) ```dart theme={null} class MizuClient { final String deviceId; final PackageInfo packageInfo; Map get headers => { 'X-Device-ID': deviceId, 'X-App-Version': packageInfo.version, 'X-App-Build': packageInfo.buildNumber, 'X-Platform': Platform.operatingSystem, 'X-OS-Version': Platform.operatingSystemVersion, 'X-Timezone': DateTime.now().timeZoneName, 'X-Locale': Platform.localeName, }; } ``` ### React Native (TypeScript) ```typescript theme={null} import DeviceInfo from 'react-native-device-info'; import { Platform, NativeModules } from 'react-native'; const getMizuHeaders = async () => ({ 'X-Device-ID': await DeviceInfo.getUniqueId(), 'X-App-Version': DeviceInfo.getVersion(), 'X-App-Build': DeviceInfo.getBuildNumber(), 'X-Device-Model': DeviceInfo.getModel(), 'X-Platform': Platform.OS, 'X-OS-Version': Platform.Version.toString(), 'X-Timezone': Intl.DateTimeFormat().resolvedOptions().timeZone, 'X-Locale': NativeModules.SettingsManager?.settings?.AppleLocale ?? NativeModules.I18nManager?.localeIdentifier ?? 'en-US', }); ``` ## Next Steps Manage API versions with deprecation warnings Return consistent error responses Handle push token registration Complete API documentation # .NET MAUI Source: https://docs.go-mizu.dev/mobile/dotnet Build cross-platform apps with C#, .NET MAUI, and Mizu backend integration. Build cross-platform mobile applications with .NET MAUI and C# that integrate seamlessly with your Mizu backend. The .NET MAUI template provides a complete project with MVVM architecture. ## Quick Start ```bash theme={null} mizu new ./my-maui-app --template mobile/dotnet ``` This creates a .NET MAUI project with: * C# with .NET 8 * MVVM pattern * HttpClient for networking * Shell navigation * iOS, Android, Windows, and macOS support ## Project Structure ``` my-maui-app/ ├── backend/ # Mizu Go backend │ └── ... │ ├── src/ │ ├── MyApp/ │ │ ├── App.xaml │ │ ├── AppShell.xaml │ │ ├── MauiProgram.cs │ │ ├── Services/ │ │ │ ├── ApiService.cs │ │ │ └── AuthService.cs │ │ ├── ViewModels/ │ │ ├── Views/ │ │ └── Models/ │ └── MyApp.csproj │ └── Makefile ``` ## API Service ```csharp theme={null} // Services/ApiService.cs public class ApiService { private readonly HttpClient _client; private readonly IDeviceInfo _deviceInfo; public ApiService(IDeviceInfo deviceInfo) { _deviceInfo = deviceInfo; _client = new HttpClient { BaseAddress = new Uri(Configuration.ApiUrl) }; } private void AddMizuHeaders(HttpRequestMessage request) { request.Headers.Add("X-Device-ID", _deviceInfo.DeviceId); request.Headers.Add("X-App-Version", AppInfo.VersionString); request.Headers.Add("X-Platform", DeviceInfo.Platform.ToString().ToLower()); request.Headers.Add("X-OS-Version", DeviceInfo.VersionString); request.Headers.Add("X-API-Version", "v2"); } public async Task GetAsync(string path) { var request = new HttpRequestMessage(HttpMethod.Get, path); AddMizuHeaders(request); var response = await _client.SendAsync(request); response.EnsureSuccessStatusCode(); var json = await response.Content.ReadAsStringAsync(); return JsonSerializer.Deserialize(json); } public async Task PostAsync(string path, TRequest data) { var request = new HttpRequestMessage(HttpMethod.Post, path) { Content = new StringContent( JsonSerializer.Serialize(data), Encoding.UTF8, "application/json" ) }; AddMizuHeaders(request); var response = await _client.SendAsync(request); response.EnsureSuccessStatusCode(); var json = await response.Content.ReadAsStringAsync(); return JsonSerializer.Deserialize(json); } } ``` ## ViewModel ```csharp theme={null} // ViewModels/ItemsViewModel.cs public partial class ItemsViewModel : ObservableObject { private readonly ApiService _api; [ObservableProperty] private ObservableCollection _items = new(); [ObservableProperty] private bool _isLoading; public ItemsViewModel(ApiService api) { _api = api; } [RelayCommand] private async Task LoadItemsAsync() { if (IsLoading) return; try { IsLoading = true; var response = await _api.GetAsync>("/api/items"); Items.Clear(); foreach (var item in response.Data) { Items.Add(item); } } catch (Exception ex) { await Shell.Current.DisplayAlert("Error", ex.Message, "OK"); } finally { IsLoading = false; } } } ``` ## View ```xml theme={null} ``` ## Template Options ```bash theme={null} mizu new ./my-app --template mobile/dotnet \ --var name=MyApp \ --var namespace=MyCompany.MyApp \ --var bundleId=com.example.app \ --var platforms=ios,android \ --var nav=shell \ --var mvvm=toolkit ``` | Variable | Description | Default | | ----------- | ------------------------------------ | ----------------- | | `name` | Project name | Directory name | | `namespace` | C# namespace | `name` | | `bundleId` | Bundle identifier | `com.example.app` | | `platforms` | Target platforms | `ios,android` | | `nav` | Navigation: `shell`, `plain` | `shell` | | `mvvm` | MVVM framework: `default`, `toolkit` | `default` | ## Next Steps Build Progressive Web Apps All mobile templates # Structured Errors Source: https://docs.go-mizu.dev/mobile/errors Return consistent, mobile-friendly error responses. The mobile package provides structured error handling with consistent response formats, error codes, and detailed context for mobile clients. ## Quick Start ```go theme={null} import "github.com/go-mizu/mizu/mobile" func handler(c *mizu.Ctx) error { if !authorized { return mobile.SendError(c, 401, mobile.NewError( mobile.ErrUnauthorized, "Invalid credentials", ).WithDetails("reason", "token_expired")) } return c.JSON(200, data) } ``` Response: ```json theme={null} { "code": "unauthorized", "message": "Invalid credentials", "details": { "reason": "token_expired" }, "trace_id": "req-abc-123" } ``` ## Error Codes Standard error codes for mobile clients: ```go theme={null} const ( ErrInvalidRequest = "invalid_request" ErrUnauthorized = "unauthorized" ErrForbidden = "forbidden" ErrNotFound = "not_found" ErrConflict = "conflict" ErrValidation = "validation_error" ErrRateLimited = "rate_limited" ErrUpgradeRequired = "upgrade_required" ErrMaintenance = "maintenance" ErrInternal = "internal_error" ) ``` ## The Error Type ```go theme={null} type Error struct { Code string `json:"code"` Message string `json:"message"` Details map[string]any `json:"details,omitempty"` TraceID string `json:"trace_id,omitempty"` } ``` ### Creating Errors ```go theme={null} // Basic error err := mobile.NewError(mobile.ErrValidation, "Email is invalid") // With details err := mobile.NewError(mobile.ErrValidation, "Validation failed"). WithDetails("field", "email"). WithDetails("error", "invalid format") // With trace ID err := mobile.NewError(mobile.ErrInternal, "Something went wrong"). WithTraceID(c.Request().Header.Get("X-Request-ID")) ``` ### Error Methods ```go theme={null} err := mobile.NewError(mobile.ErrValidation, "Validation failed") // Add single detail err = err.WithDetails("field", "email") // Add trace ID err = err.WithTraceID("req-123") // Check error type err.Code == mobile.ErrValidation // true ``` ## Sending Errors ### Basic Usage ```go theme={null} func handler(c *mizu.Ctx) error { return mobile.SendError(c, 400, mobile.NewError( mobile.ErrInvalidRequest, "Missing required field", )) } ``` ### With HTTP Status Codes | Error Code | HTTP Status | When to Use | | ------------------ | ----------- | ----------------------- | | `invalid_request` | 400 | Bad request format | | `validation_error` | 400 | Field validation failed | | `unauthorized` | 401 | Authentication required | | `forbidden` | 403 | Permission denied | | `not_found` | 404 | Resource not found | | `conflict` | 409 | Resource conflict | | `rate_limited` | 429 | Too many requests | | `upgrade_required` | 426 | App update needed | | `maintenance` | 503 | Service unavailable | | `internal_error` | 500 | Server error | ## Common Patterns ### Authentication Errors ```go theme={null} func authMiddleware() mizu.Middleware { return func(next mizu.Handler) mizu.Handler { return func(c *mizu.Ctx) error { token := c.Request().Header.Get("Authorization") if token == "" { return mobile.SendError(c, 401, mobile.NewError( mobile.ErrUnauthorized, "Authentication required", ).WithDetails("reason", "missing_token")) } user, err := validateToken(token) if err != nil { return mobile.SendError(c, 401, mobile.NewError( mobile.ErrUnauthorized, "Invalid token", ).WithDetails("reason", err.Error())) } // Continue with authenticated user return next(c) } } } ``` ### Validation Errors ```go theme={null} type CreateUserRequest struct { Email string `json:"email"` Password string `json:"password"` Name string `json:"name"` } func createUser(c *mizu.Ctx) error { var req CreateUserRequest if err := c.BodyJSON(&req); err != nil { return mobile.SendError(c, 400, mobile.NewError( mobile.ErrInvalidRequest, "Invalid JSON body", )) } // Validate fields errors := make(map[string]string) if req.Email == "" { errors["email"] = "required" } else if !isValidEmail(req.Email) { errors["email"] = "invalid format" } if len(req.Password) < 8 { errors["password"] = "minimum 8 characters" } if req.Name == "" { errors["name"] = "required" } if len(errors) > 0 { return mobile.SendError(c, 400, mobile.NewError( mobile.ErrValidation, "Validation failed", ).WithDetails("errors", errors)) } // Create user... return c.JSON(201, user) } ``` Response: ```json theme={null} { "code": "validation_error", "message": "Validation failed", "details": { "errors": { "email": "invalid format", "password": "minimum 8 characters" } } } ``` ### Not Found Errors ```go theme={null} func getUser(c *mizu.Ctx) error { id := c.Param("id") user, err := db.GetUser(id) if err == sql.ErrNoRows { return mobile.SendError(c, 404, mobile.NewError( mobile.ErrNotFound, "User not found", ).WithDetails("id", id)) } if err != nil { return mobile.SendError(c, 500, mobile.NewError( mobile.ErrInternal, "Failed to fetch user", )) } return c.JSON(200, user) } ``` ### Rate Limiting Errors ```go theme={null} func rateLimitHandler(c *mizu.Ctx) error { return mobile.SendError(c, 429, mobile.NewError( mobile.ErrRateLimited, "Too many requests", ).WithDetails("retry_after", 60). WithDetails("limit", 100). WithDetails("remaining", 0)) } ``` ### Upgrade Required ```go theme={null} func checkVersion(c *mizu.Ctx) error { device := mobile.DeviceFromCtx(c) if mobile.CompareVersions(device.AppVersion, "2.0.0") < 0 { return mobile.SendError(c, 426, mobile.NewError( mobile.ErrUpgradeRequired, "Please update to the latest version", ).WithDetails("current_version", device.AppVersion). WithDetails("minimum_version", "2.0.0"). WithDetails("store_url", getStoreURL(device.Platform))) } return nil } ``` ### Maintenance Mode ```go theme={null} func maintenanceMiddleware(enabled bool, endTime time.Time) mizu.Middleware { return func(next mizu.Handler) mizu.Handler { return func(c *mizu.Ctx) error { if enabled { return mobile.SendError(c, 503, mobile.NewError( mobile.ErrMaintenance, "Service is under maintenance", ).WithDetails("end_time", endTime.Format(time.RFC3339)). WithDetails("message", "We'll be back shortly")) } return next(c) } } } ``` ## Error Response Format All errors follow a consistent format: ```json theme={null} { "code": "error_code", "message": "Human-readable message", "details": { "field1": "value1", "field2": "value2" }, "trace_id": "request-trace-id" } ``` ### Adding Trace IDs ```go theme={null} app.Use(func(next mizu.Handler) mizu.Handler { return func(c *mizu.Ctx) error { // Get or generate trace ID traceID := c.Request().Header.Get("X-Request-ID") if traceID == "" { traceID = generateTraceID() } // Store in context for error handlers c.Set("trace_id", traceID) // Add to response header c.Header().Set("X-Request-ID", traceID) return next(c) } }) func handler(c *mizu.Ctx) error { traceID := c.Get("trace_id").(string) return mobile.SendError(c, 500, mobile.NewError( mobile.ErrInternal, "Something went wrong", ).WithTraceID(traceID)) } ``` ## Client Implementation ### iOS (Swift) ```swift theme={null} struct APIError: Decodable { let code: String let message: String let details: [String: AnyCodable]? let traceId: String? enum CodingKeys: String, CodingKey { case code, message, details case traceId = "trace_id" } } enum APIErrorCode: String { case invalidRequest = "invalid_request" case unauthorized = "unauthorized" case forbidden = "forbidden" case notFound = "not_found" case validation = "validation_error" case rateLimited = "rate_limited" case upgradeRequired = "upgrade_required" case maintenance = "maintenance" case internalError = "internal_error" } class APIClient { func handleError(_ data: Data, statusCode: Int) -> Error { guard let error = try? JSONDecoder().decode(APIError.self, from: data) else { return GenericError(message: "Unknown error", code: statusCode) } switch APIErrorCode(rawValue: error.code) { case .unauthorized: return AuthError.unauthorized(error.message) case .upgradeRequired: let storeURL = error.details?["store_url"]?.value as? String return UpgradeRequiredError(storeURL: storeURL) case .validation: let errors = error.details?["errors"]?.value as? [String: String] return ValidationError(fields: errors ?? [:]) default: return GenericError(message: error.message, code: statusCode) } } } ``` ### Android (Kotlin) ```kotlin theme={null} data class ApiError( val code: String, val message: String, val details: Map? = null, @SerializedName("trace_id") val traceId: String? = null ) sealed class ApiException(message: String) : Exception(message) { class Unauthorized(message: String) : ApiException(message) class NotFound(message: String) : ApiException(message) class ValidationError(val errors: Map) : ApiException("Validation failed") class UpgradeRequired(val storeUrl: String?) : ApiException("Update required") class Maintenance(val endTime: String?) : ApiException("Under maintenance") class Unknown(message: String) : ApiException(message) } fun parseError(response: Response): ApiException { val error = response.body?.let { gson.fromJson(it.string(), ApiError::class.java) } ?: return ApiException.Unknown("Unknown error") return when (error.code) { "unauthorized" -> ApiException.Unauthorized(error.message) "not_found" -> ApiException.NotFound(error.message) "validation_error" -> { @Suppress("UNCHECKED_CAST") val errors = error.details?.get("errors") as? Map ?: emptyMap() ApiException.ValidationError(errors) } "upgrade_required" -> { val storeUrl = error.details?.get("store_url") as? String ApiException.UpgradeRequired(storeUrl) } "maintenance" -> { val endTime = error.details?.get("end_time") as? String ApiException.Maintenance(endTime) } else -> ApiException.Unknown(error.message) } } ``` ### Flutter (Dart) ```dart theme={null} class ApiError { final String code; final String message; final Map? details; final String? traceId; ApiError({ required this.code, required this.message, this.details, this.traceId, }); factory ApiError.fromJson(Map json) { return ApiError( code: json['code'], message: json['message'], details: json['details'], traceId: json['trace_id'], ); } } class ApiException implements Exception { final ApiError error; final int statusCode; ApiException(this.error, this.statusCode); bool get isUnauthorized => error.code == 'unauthorized'; bool get isNotFound => error.code == 'not_found'; bool get isValidationError => error.code == 'validation_error'; bool get isUpgradeRequired => error.code == 'upgrade_required'; bool get isMaintenance => error.code == 'maintenance'; Map? get validationErrors { if (!isValidationError) return null; return (error.details?['errors'] as Map?)?.cast(); } String? get storeUrl => error.details?['store_url']; } ``` ## Best Practices ### Use Appropriate Status Codes * **400** - Client error (bad input) * **401** - Authentication required * **403** - Permission denied * **404** - Resource not found * **409** - Conflict (duplicate, etc.) * **422** - Validation error (alternative to 400) * **426** - Upgrade required * **429** - Rate limited * **500** - Server error * **503** - Maintenance ### Be Specific in Error Messages ```go theme={null} // Bad mobile.NewError(mobile.ErrValidation, "Invalid input") // Good mobile.NewError(mobile.ErrValidation, "Email address is invalid"). WithDetails("field", "email"). WithDetails("provided", user.Email). WithDetails("expected", "valid email format") ``` ### Include Actionable Information ```go theme={null} // Rate limit with retry info mobile.NewError(mobile.ErrRateLimited, "Too many requests"). WithDetails("retry_after", 60) // Upgrade with store URL mobile.NewError(mobile.ErrUpgradeRequired, "Update required"). WithDetails("store_url", "https://apps.apple.com/...") ``` ### Log Errors Server-Side ```go theme={null} func errorLogger() mizu.Middleware { return func(next mizu.Handler) mizu.Handler { return func(c *mizu.Ctx) error { err := next(c) if err != nil { log.Error("Request failed", "error", err, "path", c.Request().URL.Path, "method", c.Request().Method, "device", mobile.DeviceFromCtx(c).DeviceID, ) } return err } } } ``` ## Next Steps Page and cursor-based pagination Delta synchronization Complete API documentation # Flutter Source: https://docs.go-mizu.dev/mobile/flutter Build cross-platform apps with Dart, Riverpod, and Mizu backend integration. Build cross-platform mobile applications with Flutter and Dart that integrate seamlessly with your Mizu backend. The Flutter template provides a complete project with state management, networking, and platform-specific configurations. ## Quick Start ```bash theme={null} mizu new ./my-flutter-app --template mobile/flutter ``` This creates a complete Flutter project with: * Dart with null safety * Riverpod for state management * Dio for HTTP networking * go\_router for navigation * Platform-specific setup for iOS and Android ## Project Structure ``` my-flutter-app/ ├── backend/ # Mizu Go backend │ ├── cmd/server/ │ ├── app/server/ │ └── Makefile │ ├── flutter/ # Flutter app │ ├── lib/ │ │ ├── main.dart │ │ ├── app/ │ │ │ ├── app.dart │ │ │ └── router.dart │ │ ├── core/ │ │ │ ├── api/ │ │ │ │ ├── api_client.dart │ │ │ │ ├── api_interceptor.dart │ │ │ │ └── endpoints.dart │ │ │ ├── providers/ │ │ │ └── storage/ │ │ ├── features/ │ │ │ ├── auth/ │ │ │ ├── home/ │ │ │ └── settings/ │ │ └── shared/ │ │ ├── widgets/ │ │ └── extensions/ │ ├── ios/ │ ├── android/ │ ├── pubspec.yaml │ └── analysis_options.yaml │ └── Makefile ``` ## Template Options ```bash theme={null} mizu new ./my-app --template mobile/flutter \ --var name=my_app \ --var org=com.example \ --var platforms=ios,android \ --var ui=material ``` | Variable | Description | Default | | ----------- | ------------------------------------- | -------------- | | `name` | Project name | Directory name | | `org` | Organization identifier | `com.example` | | `platforms` | Target platforms | `ios,android` | | `ui` | UI framework: `material`, `cupertino` | `material` | ## API Client ```dart theme={null} // core/api/api_client.dart import 'package:dio/dio.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'api_client.g.dart'; @riverpod ApiClient apiClient(ApiClientRef ref) { final dio = Dio(BaseOptions( baseUrl: const String.fromEnvironment('API_URL', defaultValue: 'http://localhost:3000'), connectTimeout: const Duration(seconds: 30), receiveTimeout: const Duration(seconds: 30), )); dio.interceptors.add(MizuInterceptor(ref)); dio.interceptors.add(AuthInterceptor(ref)); return ApiClient(dio); } class ApiClient { final Dio _dio; ApiClient(this._dio); Future> get(String path, {Map? params}) { return _dio.get(path, queryParameters: params); } Future> post(String path, {dynamic data}) { return _dio.post(path, data: data); } Future> put(String path, {dynamic data}) { return _dio.put(path, data: data); } Future> delete(String path) { return _dio.delete(path); } } ``` ### Mizu Header Interceptor ```dart theme={null} // core/api/mizu_interceptor.dart class MizuInterceptor extends Interceptor { final Ref ref; MizuInterceptor(this.ref); @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) async { final deviceInfo = await ref.read(deviceInfoProvider.future); options.headers.addAll({ 'X-Device-ID': deviceInfo.deviceId, 'X-App-Version': deviceInfo.appVersion, 'X-App-Build': deviceInfo.buildNumber, 'X-Platform': Platform.operatingSystem, 'X-OS-Version': Platform.operatingSystemVersion, 'X-Timezone': DateTime.now().timeZoneName, 'X-Locale': Platform.localeName, 'X-API-Version': 'v2', }); handler.next(options); } @override void onResponse(Response response, ResponseInterceptorHandler handler) { // Check for deprecation warning if (response.headers.value('X-API-Deprecated') == 'true') { ref.read(apiDeprecationProvider.notifier).setDeprecated(true); } handler.next(response); } } ``` ## State Management with Riverpod ### Authentication ```dart theme={null} // features/auth/auth_provider.dart @riverpod class Auth extends _$Auth { @override FutureOr build() async { final storage = ref.watch(secureStorageProvider); final token = await storage.read(key: 'access_token'); if (token != null) { try { final user = await _fetchCurrentUser(); return AuthState.authenticated(user); } catch (e) { await storage.delete(key: 'access_token'); } } return const AuthState.unauthenticated(); } Future login(String email, String password) async { state = const AsyncValue.loading(); state = await AsyncValue.guard(() async { final api = ref.read(apiClientProvider); final response = await api.post>( '/api/auth/login', data: {'email': email, 'password': password}, ); final authResponse = AuthResponse.fromJson(response.data!); // Save tokens final storage = ref.read(secureStorageProvider); await storage.write(key: 'access_token', value: authResponse.accessToken); await storage.write(key: 'refresh_token', value: authResponse.refreshToken); return AuthState.authenticated(authResponse.user); }); } Future logout() async { final storage = ref.read(secureStorageProvider); await storage.deleteAll(); state = const AsyncData(AuthState.unauthenticated()); } } @freezed class AuthState with _$AuthState { const factory AuthState.authenticated(User user) = _Authenticated; const factory AuthState.unauthenticated() = _Unauthenticated; } ``` ### Data Fetching ```dart theme={null} // features/home/items_provider.dart @riverpod class Items extends _$Items { @override FutureOr> build() => _fetchItems(); Future> _fetchItems() async { final api = ref.read(apiClientProvider); final response = await api.get>('/api/items'); final pageResponse = PageResponse.fromJson( response.data!, (json) => Item.fromJson(json as Map), ); return pageResponse.data; } Future refresh() async { state = const AsyncValue.loading(); state = await AsyncValue.guard(_fetchItems); } Future addItem(CreateItemRequest request) async { final api = ref.read(apiClientProvider); final response = await api.post>( '/api/items', data: request.toJson(), ); final newItem = Item.fromJson(response.data!); state = state.whenData((items) => [...items, newItem]); } } ``` ## UI Components ### App Setup ```dart theme={null} // main.dart void main() async { WidgetsFlutterBinding.ensureInitialized(); // Initialize Firebase for push notifications await Firebase.initializeApp(); runApp( const ProviderScope( child: MyApp(), ), ); } // app/app.dart class MyApp extends ConsumerWidget { const MyApp({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final router = ref.watch(routerProvider); return MaterialApp.router( title: 'My App', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), useMaterial3: true, ), routerConfig: router, ); } } ``` ### Navigation ```dart theme={null} // app/router.dart @riverpod GoRouter router(RouterRef ref) { final authState = ref.watch(authProvider); return GoRouter( initialLocation: '/', redirect: (context, state) { final isAuthenticated = authState.valueOrNull?.maybeWhen( authenticated: (_) => true, orElse: () => false, ) ?? false; final isAuthRoute = state.matchedLocation.startsWith('/auth'); if (!isAuthenticated && !isAuthRoute) { return '/auth/login'; } if (isAuthenticated && isAuthRoute) { return '/'; } return null; }, routes: [ GoRoute( path: '/', builder: (context, state) => const HomeScreen(), routes: [ GoRoute( path: 'item/:id', builder: (context, state) => ItemDetailScreen( id: state.pathParameters['id']!, ), ), ], ), GoRoute( path: '/auth/login', builder: (context, state) => const LoginScreen(), ), GoRoute( path: '/settings', builder: (context, state) => const SettingsScreen(), ), ], ); } ``` ### Screen Example ```dart theme={null} // features/home/home_screen.dart class HomeScreen extends ConsumerWidget { const HomeScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final itemsAsync = ref.watch(itemsProvider); return Scaffold( appBar: AppBar( title: const Text('Home'), actions: [ IconButton( icon: const Icon(Icons.settings), onPressed: () => context.push('/settings'), ), ], ), body: itemsAsync.when( loading: () => const Center(child: CircularProgressIndicator()), error: (error, stack) => ErrorWidget( error: error, onRetry: () => ref.invalidate(itemsProvider), ), data: (items) => RefreshIndicator( onRefresh: () => ref.read(itemsProvider.notifier).refresh(), child: ListView.builder( itemCount: items.length, itemBuilder: (context, index) { final item = items[index]; return ItemCard( item: item, onTap: () => context.push('/item/${item.id}'), ); }, ), ), ), floatingActionButton: FloatingActionButton( onPressed: () => _showAddDialog(context, ref), child: const Icon(Icons.add), ), ); } } ``` ## Push Notifications ```dart theme={null} // core/services/push_service.dart class PushService { final ApiClient _api; PushService(this._api); Future initialize() async { // Request permission final settings = await FirebaseMessaging.instance.requestPermission(); if (settings.authorizationStatus != AuthorizationStatus.authorized) { return; } // Get token final token = await FirebaseMessaging.instance.getToken(); if (token != null) { await _registerToken(token); } // Listen for token refresh FirebaseMessaging.instance.onTokenRefresh.listen(_registerToken); // Handle foreground messages FirebaseMessaging.onMessage.listen(_handleForegroundMessage); // Handle background messages FirebaseMessaging.onBackgroundMessage(_handleBackgroundMessage); } Future _registerToken(String token) async { await _api.post('/api/push/register', data: { 'token': token, 'provider': Platform.isIOS ? 'apns' : 'fcm', }); } void _handleForegroundMessage(RemoteMessage message) { // Show local notification or update UI debugPrint('Foreground message: ${message.notification?.title}'); } } @pragma('vm:entry-point') Future _handleBackgroundMessage(RemoteMessage message) async { debugPrint('Background message: ${message.notification?.title}'); } ``` ## Deep Links ```dart theme={null} // app/deep_link_handler.dart @riverpod class DeepLinkHandler extends _$DeepLinkHandler { @override void build() { _initDeepLinks(); } void _initDeepLinks() async { // Handle initial link final initialUri = await getInitialUri(); if (initialUri != null) { _handleUri(initialUri); } // Listen for incoming links uriLinkStream.listen(_handleUri); } void _handleUri(Uri uri) { final router = ref.read(routerProvider); if (uri.path.startsWith('/share/')) { final id = uri.path.replaceFirst('/share/', ''); router.push('/item/$id'); } else if (uri.path.startsWith('/profile/')) { router.push('/profile'); } } } ``` ## Offline Sync ```dart theme={null} // core/services/sync_service.dart @riverpod class SyncService extends _$SyncService { String? _syncToken; @override FutureOr build() { _loadSyncToken(); } Future sync() async { final api = ref.read(apiClientProvider); final params = {}; if (_syncToken != null) { params['sync_token'] = _syncToken; } final response = await api.get>( '/api/sync', params: params, ); final syncResponse = SyncResponse.fromJson(response.data!); // Apply changes to local database final db = ref.read(databaseProvider); await db.transaction(() async { for (final item in syncResponse.created) { await db.items.insertOnConflictUpdate(item); } for (final item in syncResponse.updated) { await db.items.update(item); } for (final id in syncResponse.deleted) { await db.items.deleteWhere((t) => t.id.equals(id)); } }); // Save sync token _syncToken = syncResponse.syncToken; await _saveSyncToken(); } Future _loadSyncToken() async { final prefs = await SharedPreferences.getInstance(); _syncToken = prefs.getString('sync_token'); } Future _saveSyncToken() async { final prefs = await SharedPreferences.getInstance(); if (_syncToken != null) { await prefs.setString('sync_token', _syncToken!); } } } ``` ## Running the Project ### Start Backend ```bash theme={null} cd backend make run ``` ### Run Flutter App ```bash theme={null} cd flutter # iOS flutter run -d ios # Android flutter run -d android # Web (if enabled) flutter run -d chrome ``` ### Build for Release ```bash theme={null} # iOS flutter build ios --release # Android flutter build appbundle --release ``` ## Best Practices ### Error Handling ```dart theme={null} extension AsyncValueX on AsyncValue { Widget when({ required Widget Function(T data) data, required Widget Function(Object error, StackTrace stack) error, required Widget Function() loading, }) { return this.when( data: data, error: (e, s) { if (e is DioException) { return error(ApiError.fromDioError(e), s); } return error(e, s); }, loading: loading, ); } } ``` ### Testing ```dart theme={null} void main() { group('ItemsProvider', () { test('fetches items successfully', () async { final container = ProviderContainer( overrides: [ apiClientProvider.overrideWithValue(MockApiClient()), ], ); await container.read(itemsProvider.future); expect(container.read(itemsProvider).value, isNotEmpty); }); }); } ``` ## Next Steps Build with React Native Cross-platform push Delta synchronization All mobile templates # Unity Game Source: https://docs.go-mizu.dev/mobile/game Build mobile games with Unity, C#, and Mizu backend integration. Build cross-platform mobile games with Unity and C# that integrate with your Mizu backend for user accounts, leaderboards, multiplayer, and analytics. ## Quick Start ```bash theme={null} mizu new ./my-game --template mobile/game ``` This creates a Unity project with: * Unity 2022 LTS * UniTask for async operations * REST client for API calls * WebSocket support for multiplayer * Analytics integration ## Project Structure ``` my-game/ ├── backend/ # Mizu Go backend │ ├── cmd/server/ │ └── app/server/ │ ├── handlers/ │ │ ├── auth.go │ │ ├── leaderboard.go │ │ └── multiplayer.go │ └── ... │ ├── unity/ # Unity project │ ├── Assets/ │ │ ├── Scripts/ │ │ │ ├── API/ │ │ │ │ ├── ApiClient.cs │ │ │ │ └── Models/ │ │ │ ├── Game/ │ │ │ ├── Multiplayer/ │ │ │ └── UI/ │ │ ├── Scenes/ │ │ ├── Prefabs/ │ │ └── Resources/ │ └── ProjectSettings/ │ └── Makefile ``` ## API Client ```csharp theme={null} // Assets/Scripts/API/ApiClient.cs using System; using System.Text; using System.Threading; using Cysharp.Threading.Tasks; using UnityEngine; using UnityEngine.Networking; public class ApiClient : MonoBehaviour { [SerializeField] private string baseUrl = "http://localhost:3000"; private string deviceId; private string accessToken; private void Awake() { deviceId = SystemInfo.deviceUniqueIdentifier; } public async UniTask GetAsync(string path, CancellationToken ct = default) { using var request = UnityWebRequest.Get($"{baseUrl}{path}"); AddHeaders(request); await request.SendWebRequest().WithCancellation(ct); if (request.result != UnityWebRequest.Result.Success) { throw new ApiException(request.error, request.responseCode); } return JsonUtility.FromJson(request.downloadHandler.text); } public async UniTask PostAsync( string path, TRequest data, CancellationToken ct = default) { var json = JsonUtility.ToJson(data); var bytes = Encoding.UTF8.GetBytes(json); using var request = new UnityWebRequest($"{baseUrl}{path}", "POST") { uploadHandler = new UploadHandlerRaw(bytes), downloadHandler = new DownloadHandlerBuffer() }; AddHeaders(request); request.SetRequestHeader("Content-Type", "application/json"); await request.SendWebRequest().WithCancellation(ct); if (request.result != UnityWebRequest.Result.Success) { throw new ApiException(request.error, request.responseCode); } return JsonUtility.FromJson(request.downloadHandler.text); } private void AddHeaders(UnityWebRequest request) { request.SetRequestHeader("X-Device-ID", deviceId); request.SetRequestHeader("X-App-Version", Application.version); request.SetRequestHeader("X-Platform", GetPlatform()); request.SetRequestHeader("X-API-Version", "v2"); if (!string.IsNullOrEmpty(accessToken)) { request.SetRequestHeader("Authorization", $"Bearer {accessToken}"); } } private string GetPlatform() { return Application.platform switch { RuntimePlatform.IPhonePlayer => "ios", RuntimePlatform.Android => "android", RuntimePlatform.WebGLPlayer => "web", _ => "unknown" }; } public void SetAccessToken(string token) => accessToken = token; } ``` ## Leaderboard ```csharp theme={null} // Assets/Scripts/API/LeaderboardService.cs public class LeaderboardService { private readonly ApiClient _api; public LeaderboardService(ApiClient api) => _api = api; public async UniTask GetTopScores(int limit = 10) { var response = await _api.GetAsync( $"/api/leaderboard?limit={limit}" ); return response.entries; } public async UniTask SubmitScore(int score) { await _api.PostAsync( "/api/leaderboard", new ScoreSubmission { score = score } ); } } [Serializable] public class LeaderboardEntry { public string username; public int score; public int rank; } ``` ## Multiplayer WebSocket ```csharp theme={null} // Assets/Scripts/Multiplayer/MultiplayerClient.cs using System; using NativeWebSocket; using Cysharp.Threading.Tasks; public class MultiplayerClient : MonoBehaviour { private WebSocket ws; public event Action OnGameStateUpdated; public event Action OnPlayerAction; public async UniTask ConnectAsync(string roomId, string token) { var url = $"ws://localhost:3000/ws/game/{roomId}?token={token}"; ws = new WebSocket(url); ws.OnMessage += OnMessage; ws.OnClose += code => Debug.Log($"WS closed: {code}"); ws.OnError += error => Debug.LogError($"WS error: {error}"); await ws.Connect(); } private void OnMessage(byte[] bytes) { var json = System.Text.Encoding.UTF8.GetString(bytes); var message = JsonUtility.FromJson(json); switch (message.type) { case "game_state": OnGameStateUpdated?.Invoke(message.gameState); break; case "player_action": OnPlayerAction?.Invoke(message.action); break; } } public void SendAction(PlayerAction action) { var json = JsonUtility.ToJson(new ClientMessage { type = "action", action = action }); ws.SendText(json); } private void Update() { ws?.DispatchMessageQueue(); } private async void OnDestroy() { if (ws != null) { await ws.Close(); } } } ``` ## Template Options ```bash theme={null} mizu new ./my-app --template mobile/game \ --var name=MyGame \ --var mode=2d \ --var platforms=ios,android \ --var multiplayer=true \ --var analytics=true ``` | Variable | Description | Default | | ------------- | --------------------- | -------------- | | `name` | Project name | Directory name | | `mode` | Game mode: `2d`, `3d` | `2d` | | `platforms` | Target platforms | `ios,android` | | `multiplayer` | Enable multiplayer | `false` | | `analytics` | Enable analytics | `true` | ## Next Steps All mobile templates Complete API documentation # HTTP Headers Source: https://docs.go-mizu.dev/mobile/headers Standard mobile headers for request and response. The mobile package uses standard HTTP headers for device detection, versioning, and synchronization. ## Request Headers Headers sent by mobile clients: | Header | Description | Example | | ------------------- | ------------------------ | -------------------------------------- | | `X-Device-ID` | Unique device identifier | `550e8400-e29b-41d4-a716-446655440000` | | `X-App-Version` | Client app version | `2.1.0` | | `X-App-Build` | Build number | `2024.01.15` | | `X-Device-Model` | Device model | `iPhone15,2` | | `X-Platform` | Operating system | `ios`, `android` | | `X-OS-Version` | OS version | `17.0`, `14` | | `X-Timezone` | IANA timezone | `America/New_York` | | `X-Locale` | Device locale | `en-US`, `ja-JP` | | `X-Push-Token` | Push notification token | `abc123...` | | `X-API-Version` | API version | `v2`, `v2.1` | | `X-Sync-Token` | Last sync token | `YWJjMTIz...` | | `X-Idempotency-Key` | Request idempotency | `req-abc-123` | | `X-Request-ID` | Request trace ID | `trace-123` | ## Response Headers Headers sent by the server: | Header | Description | Example | | ------------------- | ------------------------ | ------------- | | `X-API-Version` | API version used | `v2` | | `X-API-Deprecated` | Deprecation warning | `true` | | `X-Min-App-Version` | Minimum required version | `1.5.0` | | `X-Sync-Token` | New sync token | `ZGVmNDU2...` | | `X-Request-ID` | Request trace ID | `trace-123` | ## Header Details ### X-Device-ID A unique identifier for the device. Should persist across app reinstalls if possible. **iOS:** ```swift theme={null} UIDevice.current.identifierForVendor?.uuidString ``` **Android:** ```kotlin theme={null} Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID) ``` ### X-App-Version Semantic version of the client application. ``` X-App-Version: 2.1.0 ``` ### X-Platform Operating system identifier. Used for platform detection when User-Agent parsing is disabled. Valid values: * `ios` * `android` * `windows` * `macos` * `web` ### X-API-Version API version requested by the client. Supports formats: * `v2` * `v2.1` * `2` * `2.1` ### X-Sync-Token Opaque token for delta synchronization. Clients store and send back on subsequent sync requests. ### X-API-Deprecated Response header indicating the requested API version is deprecated. Clients should plan to upgrade. ``` X-API-Deprecated: true ``` ### X-Min-App-Version Response header indicating the minimum required app version. Clients below this version should prompt for update. ``` X-Min-App-Version: 1.5.0 ``` ## Client Implementation ### iOS (Swift) ```swift theme={null} extension URLRequest { mutating func addMizuHeaders() { setValue(DeviceInfo.deviceId, forHTTPHeaderField: "X-Device-ID") setValue(Bundle.main.appVersion, forHTTPHeaderField: "X-App-Version") setValue(Bundle.main.buildNumber, forHTTPHeaderField: "X-App-Build") setValue(UIDevice.current.modelIdentifier, forHTTPHeaderField: "X-Device-Model") setValue("ios", forHTTPHeaderField: "X-Platform") setValue(UIDevice.current.systemVersion, forHTTPHeaderField: "X-OS-Version") setValue(TimeZone.current.identifier, forHTTPHeaderField: "X-Timezone") setValue(Locale.current.identifier, forHTTPHeaderField: "X-Locale") setValue("v2", forHTTPHeaderField: "X-API-Version") } } ``` ### Android (Kotlin) ```kotlin theme={null} class MizuHeaderInterceptor(private val context: Context) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request().newBuilder() .header("X-Device-ID", getDeviceId()) .header("X-App-Version", BuildConfig.VERSION_NAME) .header("X-App-Build", BuildConfig.VERSION_CODE.toString()) .header("X-Device-Model", Build.MODEL) .header("X-Platform", "android") .header("X-OS-Version", Build.VERSION.RELEASE) .header("X-Timezone", TimeZone.getDefault().id) .header("X-Locale", Locale.getDefault().toLanguageTag()) .header("X-API-Version", "v2") .build() return chain.proceed(request) } } ``` ### Flutter (Dart) ```dart theme={null} final headers = { 'X-Device-ID': deviceId, 'X-App-Version': packageInfo.version, 'X-App-Build': packageInfo.buildNumber, 'X-Platform': Platform.operatingSystem, 'X-OS-Version': Platform.operatingSystemVersion, 'X-Timezone': DateTime.now().timeZoneName, 'X-Locale': Platform.localeName, 'X-API-Version': 'v2', }; ``` ## Next Steps How headers are parsed Complete API documentation # iOS Source: https://docs.go-mizu.dev/mobile/ios Build native iOS apps with Swift, SwiftUI, and Mizu backend integration. Build native iOS applications with Swift and SwiftUI that seamlessly integrate with your Mizu backend. The iOS template provides a complete project structure with networking, state management, and platform best practices. ## Quick Start ```bash theme={null} mizu new ./my-ios-app --template mobile/ios ``` This creates a complete iOS project with: * Swift + SwiftUI architecture * Pre-configured Mizu API client * Authentication flow * Offline support foundation * Push notification setup ## Project Structure ``` my-ios-app/ ├── backend/ # Mizu Go backend │ ├── cmd/server/ │ │ └── main.go │ ├── app/server/ │ │ ├── app.go │ │ ├── config.go │ │ └── routes.go │ ├── go.mod │ └── Makefile │ ├── ios/ # iOS app │ ├── App/ │ │ ├── MyApp.swift # App entry point │ │ ├── ContentView.swift │ │ └── AppDelegate.swift │ ├── Core/ │ │ ├── API/ │ │ │ ├── APIClient.swift │ │ │ ├── APIError.swift │ │ │ └── Endpoints.swift │ │ ├── Auth/ │ │ │ ├── AuthManager.swift │ │ │ └── KeychainHelper.swift │ │ └── Storage/ │ │ └── UserDefaults+Extensions.swift │ ├── Features/ │ │ ├── Home/ │ │ ├── Profile/ │ │ └── Settings/ │ ├── Shared/ │ │ ├── Components/ │ │ ├── Extensions/ │ │ └── Modifiers/ │ ├── Resources/ │ │ └── Assets.xcassets │ └── MyApp.xcodeproj │ └── Makefile ``` ## Template Options ```bash theme={null} mizu new ./my-app --template mobile/ios \ --var name=MyApp \ --var bundleId=com.example.myapp \ --var minIOS=16.0 \ --var ui=swiftui ``` | Variable | Description | Default | | ---------- | ------------------------------------------ | ---------------------- | | `name` | Project name | Directory name | | `bundleId` | Bundle identifier | `com.example.{{name}}` | | `minIOS` | Minimum iOS version | `16.0` | | `ui` | UI framework: `swiftui`, `uikit`, `hybrid` | `swiftui` | ## API Client The template includes a pre-configured API client: ```swift theme={null} // Core/API/APIClient.swift class APIClient { static let shared = APIClient() private let baseURL: URL private let session: URLSession private let deviceID: String init() { self.baseURL = URL(string: Configuration.apiBaseURL)! self.deviceID = DeviceInfo.uniqueID let config = URLSessionConfiguration.default config.timeoutIntervalForRequest = 30 config.httpAdditionalHeaders = [ "Content-Type": "application/json", "Accept": "application/json", ] self.session = URLSession(configuration: config) } func request(_ endpoint: Endpoint) async throws -> T { var request = URLRequest(url: baseURL.appendingPathComponent(endpoint.path)) request.httpMethod = endpoint.method.rawValue // Add mobile headers request.addMizuHeaders(deviceID: deviceID) // Add auth if available if let token = AuthManager.shared.accessToken { request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") } // Add body if let body = endpoint.body { request.httpBody = try JSONEncoder().encode(body) } let (data, response) = try await session.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { throw APIError.invalidResponse } // Handle errors if httpResponse.statusCode >= 400 { let error = try JSONDecoder().decode(APIErrorResponse.self, from: data) throw APIError.server(error, statusCode: httpResponse.statusCode) } // Check deprecation warning if httpResponse.value(forHTTPHeaderField: "X-API-Deprecated") == "true" { NotificationCenter.default.post(name: .apiDeprecated, object: nil) } return try JSONDecoder().decode(T.self, from: data) } } // Add Mizu headers extension URLRequest { mutating func addMizuHeaders(deviceID: String) { setValue(deviceID, forHTTPHeaderField: "X-Device-ID") setValue(Bundle.main.appVersion, forHTTPHeaderField: "X-App-Version") setValue(Bundle.main.buildNumber, forHTTPHeaderField: "X-App-Build") setValue(UIDevice.current.modelIdentifier, forHTTPHeaderField: "X-Device-Model") setValue("ios", forHTTPHeaderField: "X-Platform") setValue(UIDevice.current.systemVersion, forHTTPHeaderField: "X-OS-Version") setValue(TimeZone.current.identifier, forHTTPHeaderField: "X-Timezone") setValue(Locale.current.identifier, forHTTPHeaderField: "X-Locale") setValue("v2", forHTTPHeaderField: "X-API-Version") } } ``` ## Authentication ```swift theme={null} // Core/Auth/AuthManager.swift @MainActor class AuthManager: ObservableObject { static let shared = AuthManager() @Published var isAuthenticated = false @Published var currentUser: User? private let keychain = KeychainHelper.shared var accessToken: String? { keychain.read(key: "access_token") } func login(email: String, password: String) async throws { let response: AuthResponse = try await APIClient.shared.request( .login(email: email, password: password) ) keychain.save(response.accessToken, key: "access_token") keychain.save(response.refreshToken, key: "refresh_token") currentUser = response.user isAuthenticated = true } func logout() { keychain.delete(key: "access_token") keychain.delete(key: "refresh_token") currentUser = nil isAuthenticated = false } func refreshTokenIfNeeded() async throws { guard let refreshToken = keychain.read(key: "refresh_token") else { throw AuthError.notAuthenticated } let response: RefreshResponse = try await APIClient.shared.request( .refreshToken(token: refreshToken) ) keychain.save(response.accessToken, key: "access_token") } } ``` ## SwiftUI Integration ### App Entry Point ```swift theme={null} // App/MyApp.swift @main struct MyApp: App { @StateObject private var authManager = AuthManager.shared @StateObject private var appState = AppState() init() { // Configure appearance configureAppearance() // Check for updates Task { await AppUpdateManager.shared.checkForUpdates() } } var body: some Scene { WindowGroup { Group { if authManager.isAuthenticated { MainTabView() } else { LoginView() } } .environmentObject(authManager) .environmentObject(appState) } } } ``` ### View Model Pattern ```swift theme={null} // Features/Home/HomeViewModel.swift @MainActor class HomeViewModel: ObservableObject { @Published var items: [Item] = [] @Published var isLoading = false @Published var error: Error? func loadItems() async { isLoading = true error = nil do { let response: PageResponse = try await APIClient.shared.request(.getItems()) items = response.data } catch { self.error = error } isLoading = false } } // Features/Home/HomeView.swift struct HomeView: View { @StateObject private var viewModel = HomeViewModel() var body: some View { NavigationStack { Group { if viewModel.isLoading { ProgressView() } else if let error = viewModel.error { ErrorView(error: error, retry: { Task { await viewModel.loadItems() } }) } else { List(viewModel.items) { item in ItemRow(item: item) } .refreshable { await viewModel.loadItems() } } } .navigationTitle("Home") } .task { await viewModel.loadItems() } } } ``` ## Push Notifications ```swift theme={null} // Core/Push/PushManager.swift class PushManager: NSObject, ObservableObject, UNUserNotificationCenterDelegate { static let shared = PushManager() @Published var isRegistered = false override init() { super.init() UNUserNotificationCenter.current().delegate = self } func requestPermission() async -> Bool { do { let granted = try await UNUserNotificationCenter.current() .requestAuthorization(options: [.alert, .badge, .sound]) if granted { await MainActor.run { UIApplication.shared.registerForRemoteNotifications() } } return granted } catch { return false } } func registerToken(_ deviceToken: Data) async { let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined() do { try await APIClient.shared.request( .registerPushToken(token: token, sandbox: isDebugBuild) ) as EmptyResponse isRegistered = true } catch { print("Failed to register push token: \(error)") } } // Handle foreground notifications func userNotificationCenter( _ center: UNUserNotificationCenter, willPresent notification: UNNotification ) async -> UNNotificationPresentationOptions { return [.banner, .badge, .sound] } // Handle notification tap func userNotificationCenter( _ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse ) async { let userInfo = response.notification.request.content.userInfo handleNotificationData(userInfo) } private func handleNotificationData(_ userInfo: [AnyHashable: Any]) { // Handle deep link or action if let action = userInfo["action"] as? String { NotificationCenter.default.post( name: .handlePushAction, object: nil, userInfo: ["action": action] ) } } } ``` ## Deep Links ```swift theme={null} // App/AppDelegate.swift class AppDelegate: NSObject, UIApplicationDelegate { func application( _ application: UIApplication, continue userActivity: NSUserActivity ) -> Bool { guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, let url = userActivity.webpageURL else { return false } return DeepLinkHandler.shared.handle(url) } } // Core/DeepLink/DeepLinkHandler.swift class DeepLinkHandler: ObservableObject { static let shared = DeepLinkHandler() @Published var pendingDeepLink: DeepLink? func handle(_ url: URL) -> Bool { guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { return false } let deepLink = parseDeepLink(from: components) pendingDeepLink = deepLink return deepLink != nil } private func parseDeepLink(from components: URLComponents) -> DeepLink? { let path = components.path if path.hasPrefix("/share/") { let id = String(path.dropFirst("/share/".count)) return .share(id: id) } if path.hasPrefix("/profile/") { let username = String(path.dropFirst("/profile/".count)) return .profile(username: username) } return nil } } enum DeepLink: Equatable { case share(id: String) case profile(username: String) case settings } ``` ## Offline Support ```swift theme={null} // Core/Sync/SyncManager.swift class SyncManager: ObservableObject { static let shared = SyncManager() @Published var isSyncing = false @Published var lastSyncTime: Date? private var syncToken: String? { get { UserDefaults.standard.string(forKey: "sync_token") } set { UserDefaults.standard.set(newValue, forKey: "sync_token") } } func sync() async throws { guard !isSyncing else { return } isSyncing = true defer { isSyncing = false } let endpoint: Endpoint = syncToken.map { .sync(token: $0) } ?? .sync(token: nil) let response: SyncResponse = try await APIClient.shared.request(endpoint) // Apply changes to local database try await LocalDatabase.shared.apply(delta: response) // Store new token syncToken = response.syncToken lastSyncTime = Date() } } ``` ## Running the Project ### Start Backend ```bash theme={null} cd backend make run # Server running at http://localhost:3000 ``` ### Run iOS App 1. Open `ios/MyApp.xcodeproj` in Xcode 2. Select your target device or simulator 3. Press Cmd+R to build and run ### Development Tips ```swift theme={null} // Configuration.swift enum Configuration { #if DEBUG static let apiBaseURL = "http://localhost:3000" #else static let apiBaseURL = "https://api.example.com" #endif } ``` ## Best Practices ### Error Handling ```swift theme={null} func loadData() async { do { let data: Response = try await APIClient.shared.request(.getData()) // Handle success } catch APIError.server(let error, let statusCode) { switch error.code { case "unauthorized": await AuthManager.shared.logout() case "upgrade_required": showUpdateAlert(storeURL: error.details?["store_url"]) default: showError(error.message) } } catch { showError("Something went wrong") } } ``` ### Combine with SwiftUI ```swift theme={null} // Use @Published for reactive updates class ViewModel: ObservableObject { @Published var state: ViewState = .idle enum ViewState { case idle case loading case loaded([Item]) case error(Error) } } ``` ### Testing ```swift theme={null} // Mock API client for tests protocol APIClientProtocol { func request(_ endpoint: Endpoint) async throws -> T } class MockAPIClient: APIClientProtocol { var mockResponse: Any? var mockError: Error? func request(_ endpoint: Endpoint) async throws -> T { if let error = mockError { throw error } return mockResponse as! T } } ``` ## Next Steps Build native Android apps Cross-platform push support Universal Links setup Delta synchronization # Kotlin Multiplatform Source: https://docs.go-mizu.dev/mobile/kmm Build cross-platform apps with shared Kotlin code and Mizu backend integration. Build cross-platform mobile applications with Kotlin Multiplatform Mobile (KMM) that share business logic between iOS and Android while using native UI on each platform. ## Quick Start ```bash theme={null} mizu new ./my-kmm-app --template mobile/kmm ``` This creates a KMM project with: * Shared Kotlin module for business logic * Android app with Jetpack Compose * iOS app with SwiftUI * Ktor for networking * SQLDelight for local storage (optional) ## Project Structure ``` my-kmm-app/ ├── backend/ # Mizu Go backend │ └── ... │ ├── shared/ # Shared Kotlin code │ ├── src/ │ │ ├── commonMain/ │ │ │ └── kotlin/ │ │ │ ├── api/ │ │ │ ├── models/ │ │ │ └── repository/ │ │ ├── androidMain/ │ │ └── iosMain/ │ └── build.gradle.kts │ ├── androidApp/ # Android app │ ├── src/main/ │ └── build.gradle.kts │ ├── iosApp/ # iOS app │ └── iosApp.xcodeproj │ └── build.gradle.kts ``` ## Shared API Client ```kotlin theme={null} // shared/src/commonMain/kotlin/api/ApiClient.kt class ApiClient( private val baseUrl: String, private val deviceInfo: DeviceInfo ) { private val client = HttpClient { install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) } install(DefaultRequest) { header("Content-Type", "application/json") header("X-Device-ID", deviceInfo.deviceId) header("X-App-Version", deviceInfo.appVersion) header("X-Platform", deviceInfo.platform) header("X-API-Version", "v2") } } suspend fun get(path: String, deserializer: DeserializationStrategy): T { val response = client.get("$baseUrl$path") return Json.decodeFromString(deserializer, response.bodyAsText()) } suspend fun post( path: String, body: T, serializer: SerializationStrategy, deserializer: DeserializationStrategy ): R { val response = client.post("$baseUrl$path") { setBody(Json.encodeToString(serializer, body)) } return Json.decodeFromString(deserializer, response.bodyAsText()) } } // Platform-specific device info expect class DeviceInfo { val deviceId: String val appVersion: String val platform: String } ``` ### Android Implementation ```kotlin theme={null} // shared/src/androidMain/kotlin/DeviceInfo.kt actual class DeviceInfo(private val context: Context) { actual val deviceId: String get() = Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID) actual val appVersion: String get() = context.packageManager.getPackageInfo(context.packageName, 0).versionName actual val platform: String = "android" } ``` ### iOS Implementation ```kotlin theme={null} // shared/src/iosMain/kotlin/DeviceInfo.kt actual class DeviceInfo { actual val deviceId: String get() = UIDevice.currentDevice.identifierForVendor?.UUIDString ?: "" actual val appVersion: String get() = NSBundle.mainBundle.infoDictionary?.get("CFBundleShortVersionString") as? String ?: "" actual val platform: String = "ios" } ``` ## Shared Repository ```kotlin theme={null} // shared/src/commonMain/kotlin/repository/ItemRepository.kt class ItemRepository(private val api: ApiClient) { suspend fun getItems(): Result> { return try { val response = api.get("/api/items", PageResponse.serializer(Item.serializer())) Result.success(response.data) } catch (e: Exception) { Result.failure(e) } } suspend fun getItem(id: String): Result { return try { val item = api.get("/api/items/$id", Item.serializer()) Result.success(item) } catch (e: Exception) { Result.failure(e) } } suspend fun createItem(request: CreateItemRequest): Result { return try { val item = api.post( "/api/items", request, CreateItemRequest.serializer(), Item.serializer() ) Result.success(item) } catch (e: Exception) { Result.failure(e) } } } ``` ## Android App ```kotlin theme={null} // androidApp/src/main/kotlin/MainActivity.kt class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val deviceInfo = DeviceInfo(applicationContext) val api = ApiClient(BuildConfig.API_URL, deviceInfo) val repository = ItemRepository(api) setContent { MyAppTheme { App(repository) } } } } @Composable fun App(repository: ItemRepository) { val viewModel = remember { ItemsViewModel(repository) } val items by viewModel.items.collectAsState() val isLoading by viewModel.isLoading.collectAsState() Scaffold( topBar = { TopAppBar(title = { Text("Items") }) } ) { padding -> if (isLoading) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { CircularProgressIndicator() } } else { LazyColumn(Modifier.padding(padding)) { items(items) { item -> ItemCard(item) } } } } } ``` ## iOS App ```swift theme={null} // iosApp/ContentView.swift import SwiftUI import shared struct ContentView: View { @StateObject private var viewModel = ItemsViewModel() var body: some View { NavigationView { Group { if viewModel.isLoading { ProgressView() } else { List(viewModel.items, id: \.id) { item in ItemRow(item: item) } } } .navigationTitle("Items") } .task { await viewModel.loadItems() } } } @MainActor class ItemsViewModel: ObservableObject { @Published var items: [Item] = [] @Published var isLoading = false private let repository: ItemRepository init() { let deviceInfo = DeviceInfo() let api = ApiClient(baseUrl: Configuration.apiUrl, deviceInfo: deviceInfo) self.repository = ItemRepository(api: api) } func loadItems() async { isLoading = true defer { isLoading = false } do { let result = try await repository.getItems() if let items = result.getOrNull() { self.items = items } } catch { print("Error loading items: \(error)") } } } ``` ## Template Options ```bash theme={null} mizu new ./my-app --template mobile/kmm \ --var name=MyApp \ --var package=com.example.app \ --var platforms=android,ios \ --var storage=sqldelight ``` | Variable | Description | Default | | ----------- | ----------------------------------- | ----------------- | | `name` | Project name | Directory name | | `package` | Package name | `com.example.app` | | `platforms` | Target platforms | `android,ios` | | `storage` | Local storage: `none`, `sqldelight` | `none` | ## Next Steps Build with .NET MAUI All mobile templates # Overview Source: https://docs.go-mizu.dev/mobile/overview Build production-ready mobile backends with Mizu's comprehensive mobile integration. Mizu provides powerful mobile backend integration that handles the unique requirements of iOS, Android, and cross-platform applications. Whether you're building a native Swift app, a Flutter application, or a React Native project, Mizu has you covered. ## What is Mizu Mobile? The mobile package provides everything you need to build robust mobile backends: **Device Detection:** * Parse device information from headers and User-Agent * Platform detection (iOS, Android, Windows, macOS) * App version tracking and validation * Device model and locale information **API Versioning:** * Semantic version middleware with deprecation warnings * Version detection from headers, query params, or URL paths * Graceful version migration support **Offline Sync:** * Delta synchronization with opaque sync tokens * Conflict resolution strategies * Full sync and incremental sync support **Push Notifications:** * Cross-platform token management (APNS, FCM, WNS) * Token validation and provider detection * Unified payload format with platform conversion **Deep Links:** * Universal Links for iOS (apple-app-site-association) * App Links for Android (assetlinks.json) * Smart fallback to web **App Store Integration:** * Version checking and force updates * Maintenance mode support * Platform-specific store URLs ## Supported Platforms Mizu supports native and cross-platform development: | Platform | Framework | Template | | ---------------- | -------------------- | -------------------- | | **iOS** | Swift + SwiftUI | `mobile/ios` | | **Android** | Kotlin + Compose | `mobile/android` | | **Flutter** | Dart + Riverpod | `mobile/flutter` | | **React Native** | TypeScript + Expo | `mobile/reactnative` | | **PWA** | React + Vite | `mobile/pwa` | | **KMM** | Kotlin Multiplatform | `mobile/kmm` | | **.NET MAUI** | C# + MVVM | `mobile/dotnet` | | **Unity** | C# | `mobile/game` | ## How It Works ### Request Flow ``` Mobile App Request ↓ ┌──────────────────────────────────┐ │ Mobile Middleware │ │ - Parse device headers │ │ - Validate app version │ │ - Check platform │ └──────────────┬───────────────────┘ ↓ ┌──────────────────────────────────┐ │ Version Middleware (optional) │ │ - Parse API version │ │ - Set deprecation headers │ └──────────────┬───────────────────┘ ↓ ┌──────────────────────────────────┐ │ Your Handler │ │ - Access device via DeviceFromCtx│ │ - Version-aware logic │ │ - Return structured response │ └──────────────────────────────────┘ ``` ### Mobile Headers The middleware parses standard mobile headers: **Request Headers:** ``` X-Device-ID: 550e8400-e29b-41d4-a716-446655440000 X-App-Version: 2.1.0 X-App-Build: 2024.01.15 X-Device-Model: iPhone15,2 X-Platform: ios X-OS-Version: 17.0 X-Timezone: America/New_York X-Locale: en-US X-API-Version: v2 ``` **Response Headers:** ``` X-API-Version: v2 X-API-Deprecated: true X-Min-App-Version: 1.5.0 X-Sync-Token: abc123... ``` ## Feature Comparison ### Native vs Cross-Platform | Feature | Native (iOS/Android) | Cross-Platform | | -------------------- | -------------------- | ----------------- | | **Performance** | Best | Good to Excellent | | **Platform APIs** | Full access | Limited/bridged | | **Development time** | Longer | Shorter | | **Code sharing** | None | High | | **Team expertise** | Platform-specific | Single codebase | | **App size** | Smaller | Larger | **Choose Native when:** * Maximum performance is critical * Deep platform integration needed * Separate iOS/Android teams available * Platform-specific UX required **Choose Cross-Platform when:** * Faster time to market needed * Single development team * Consistent UX across platforms * Budget constraints ### When to Use Mizu Mobile Mizu Mobile is ideal for: * **Offline-first apps** - Built-in sync support * **Multi-platform backends** - Single Go backend for all clients * **Version-managed APIs** - Graceful deprecation and migration * **Push-enabled apps** - Cross-platform push token management * **Deep-linked apps** - Universal/App Links support ## Quick Comparison ### Traditional REST vs Mizu Mobile **Traditional REST:** ```go theme={null} app.Get("/api/users", func(c *mizu.Ctx) error { // No device context // No version awareness // Manual header parsing return c.JSON(200, users) }) ``` **Mizu Mobile:** ```go theme={null} app.Use(mobile.New()) app.Use(mobile.VersionMiddleware(mobile.VersionOptions{ Supported: []mobile.Version{{2, 0}, {1, 0}}, })) app.Get("/api/users", func(c *mizu.Ctx) error { device := mobile.DeviceFromCtx(c) version := mobile.VersionFromCtx(c) // Platform-specific response if device.Platform == mobile.PlatformIOS { // iOS-optimized response } // Version-aware logic if version.AtLeast(2, 0) { return c.JSON(200, v2Users) } return c.JSON(200, v1Users) }) ``` ## Architecture ``` ┌─────────────────────────────────────────────────────────┐ │ Mobile Clients │ │ iOS App Android App Flutter React Native PWA │ └────────────────────────┬────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ Mizu Backend │ │ ┌─────────────┐ ┌──────────────┐ ┌───────────────┐ │ │ │ Mobile │ │ Version │ │ Your │ │ │ │ Middleware │→ │ Middleware │→ │ Handlers │ │ │ └─────────────┘ └──────────────┘ └───────────────┘ │ │ │ │ ┌─────────────┐ ┌──────────────┐ ┌───────────────┐ │ │ │ Sync │ │ Push │ │ Deep Links │ │ │ │ Engine │ │ Tokens │ │ Handler │ │ │ └─────────────┘ └──────────────┘ └───────────────┘ │ └─────────────────────────────────────────────────────────┘ │ ▼ ┌───────────────┴───────────────┐ │ Database │ │ Users, Data, Push Tokens │ └───────────────────────────────┘ ``` ## What's Next? Ready to build your first mobile backend with Mizu? Create your first mobile backend in minutes Learn about device parsing and validation Build a native iOS app with Mizu Build a cross-platform Flutter app # Pagination Source: https://docs.go-mizu.dev/mobile/pagination Page-based and cursor-based pagination helpers for mobile APIs. The mobile package provides built-in pagination helpers that work great with mobile clients, supporting both traditional page-based and cursor-based pagination. ## Quick Start ### Page-Based Pagination ```go theme={null} app.Get("/api/users", func(c *mizu.Ctx) error { page := mobile.ParsePageRequest(c) users, total := db.ListUsers(page.Offset(), page.Limit()) return c.JSON(200, mobile.NewPage(users, page, total)) }) ``` Request: `GET /api/users?page=2&per_page=20` Response: ```json theme={null} { "data": [...], "page": 2, "per_page": 20, "total": 150, "total_pages": 8 } ``` ### Cursor-Based Pagination ```go theme={null} app.Get("/api/feed", func(c *mizu.Ctx) error { page := mobile.ParsePageRequest(c) items, nextCursor := db.ListAfter(page.CursorValue(), page.Limit()) hasMore := len(items) == page.Limit() return c.JSON(200, mobile.NewCursorPage(items, nextCursor, "", hasMore)) }) ``` Request: `GET /api/feed?cursor=eyJpZCI6MTAwfQ&limit=20` Response: ```json theme={null} { "data": [...], "next_cursor": "eyJpZCI6MTIwfQ", "has_more": true } ``` ## Page Request The `PageRequest` type holds pagination parameters: ```go theme={null} type PageRequest struct { Page int // 1-indexed page number PerPage int // Items per page Cursor string // Cursor for cursor-based pagination Limit int // Limit (same as PerPage, for cursor-based) } ``` ### Parsing Page Requests ```go theme={null} page := mobile.ParsePageRequest(c) // Query params parsed: // ?page=2 → page.Page = 2 // ?per_page=20 → page.PerPage = 20 // ?limit=20 → page.Limit = 20 // ?cursor=abc123 → page.Cursor = "abc123" ``` ### Default Values * `Page`: 1 * `PerPage`: 20 * `Limit`: 20 * Maximum: 100 (capped automatically) ### Page Request Methods ```go theme={null} page := mobile.ParsePageRequest(c) // Get offset for SQL OFFSET clause page.Offset() // (page.Page - 1) * page.PerPage // Get limit for SQL LIMIT clause page.Limit() // page.PerPage (or page.Limit) // Check if cursor-based page.IsCursor() // true if Cursor is set // Get cursor value page.CursorValue() // decoded cursor string ``` ## Page-Based Pagination Traditional pagination with page numbers and totals. ### Response Format ```go theme={null} type PageResponse[T any] struct { Data []T `json:"data"` Page int `json:"page"` PerPage int `json:"per_page"` Total int `json:"total"` TotalPages int `json:"total_pages"` } ``` ### Creating Page Responses ```go theme={null} func listUsers(c *mizu.Ctx) error { page := mobile.ParsePageRequest(c) // Query database users := db.Query("SELECT * FROM users LIMIT ? OFFSET ?", page.Limit(), page.Offset()) total := db.QueryRow("SELECT COUNT(*) FROM users") return c.JSON(200, mobile.NewPage(users, page, total)) } ``` ### With Filtering ```go theme={null} func listUsers(c *mizu.Ctx) error { page := mobile.ParsePageRequest(c) role := c.Query("role") var users []User var total int query := db.Model(&User{}) if role != "" { query = query.Where("role = ?", role) } query.Count(&total) query.Offset(page.Offset()).Limit(page.Limit()).Find(&users) return c.JSON(200, mobile.NewPage(users, page, total)) } ``` ### With Sorting ```go theme={null} func listUsers(c *mizu.Ctx) error { page := mobile.ParsePageRequest(c) sortBy := c.Query("sort", "created_at") order := c.Query("order", "desc") users, total := db.ListUsers( page.Offset(), page.Limit(), sortBy, order, ) response := mobile.NewPage(users, page, total) // Add sorting info to response return c.JSON(200, map[string]any{ "data": response.Data, "page": response.Page, "per_page": response.PerPage, "total": response.Total, "total_pages": response.TotalPages, "sort": sortBy, "order": order, }) } ``` ## Cursor-Based Pagination Efficient pagination for infinite scroll and real-time feeds. ### Response Format ```go theme={null} type CursorResponse[T any] struct { Data []T `json:"data"` NextCursor string `json:"next_cursor,omitempty"` PrevCursor string `json:"prev_cursor,omitempty"` HasMore bool `json:"has_more"` } ``` ### Creating Cursor Responses ```go theme={null} func listFeed(c *mizu.Ctx) error { page := mobile.ParsePageRequest(c) // Parse cursor (e.g., base64 encoded ID) var afterID int64 if page.IsCursor() { afterID, _ = decodeCursor(page.CursorValue()) } // Query items after cursor items := db.Query(` SELECT * FROM posts WHERE id > ? ORDER BY id LIMIT ? `, afterID, page.Limit()) // Generate next cursor from last item var nextCursor string if len(items) > 0 { nextCursor = encodeCursor(items[len(items)-1].ID) } hasMore := len(items) == page.Limit() return c.JSON(200, mobile.NewCursorPage(items, nextCursor, "", hasMore)) } ``` ### Bidirectional Cursors ```go theme={null} func listMessages(c *mizu.Ctx) error { page := mobile.ParsePageRequest(c) before := c.Query("before") after := c.Query("after") var messages []Message var prevCursor, nextCursor string if after != "" { // Load newer messages afterID, _ := decodeCursor(after) messages = db.Query(` SELECT * FROM messages WHERE id > ? ORDER BY id ASC LIMIT ? `, afterID, page.Limit()) if len(messages) > 0 { nextCursor = encodeCursor(messages[len(messages)-1].ID) prevCursor = encodeCursor(messages[0].ID) } } else if before != "" { // Load older messages beforeID, _ := decodeCursor(before) messages = db.Query(` SELECT * FROM messages WHERE id < ? ORDER BY id DESC LIMIT ? `, beforeID, page.Limit()) // Reverse for chronological order slices.Reverse(messages) if len(messages) > 0 { nextCursor = encodeCursor(messages[len(messages)-1].ID) prevCursor = encodeCursor(messages[0].ID) } } else { // Initial load - get latest messages = db.Query(` SELECT * FROM messages ORDER BY id DESC LIMIT ? `, page.Limit()) slices.Reverse(messages) if len(messages) > 0 { nextCursor = encodeCursor(messages[len(messages)-1].ID) } } hasMore := len(messages) == page.Limit() return c.JSON(200, mobile.NewCursorPage( messages, nextCursor, prevCursor, hasMore, )) } ``` ### Timestamp-Based Cursors ```go theme={null} func listNotifications(c *mizu.Ctx) error { page := mobile.ParsePageRequest(c) var since time.Time if page.IsCursor() { since, _ = time.Parse(time.RFC3339Nano, page.CursorValue()) } notifications := db.Query(` SELECT * FROM notifications WHERE created_at < ? ORDER BY created_at DESC LIMIT ? `, since, page.Limit()) var nextCursor string if len(notifications) > 0 { last := notifications[len(notifications)-1] nextCursor = last.CreatedAt.Format(time.RFC3339Nano) } hasMore := len(notifications) == page.Limit() return c.JSON(200, mobile.NewCursorPage( notifications, nextCursor, "", hasMore, )) } ``` ## Cursor Encoding ### Simple ID Cursors ```go theme={null} func encodeCursor(id int64) string { return base64.RawURLEncoding.EncodeToString( []byte(strconv.FormatInt(id, 10)), ) } func decodeCursor(cursor string) (int64, error) { data, err := base64.RawURLEncoding.DecodeString(cursor) if err != nil { return 0, err } return strconv.ParseInt(string(data), 10, 64) } ``` ### Composite Cursors ```go theme={null} type Cursor struct { ID int64 `json:"id"` CreatedAt time.Time `json:"created_at"` } func encodeCursor(c Cursor) string { data, _ := json.Marshal(c) return base64.RawURLEncoding.EncodeToString(data) } func decodeCursor(s string) (Cursor, error) { var c Cursor data, err := base64.RawURLEncoding.DecodeString(s) if err != nil { return c, err } err = json.Unmarshal(data, &c) return c, err } ``` ## When to Use Each ### Page-Based Pagination **Use when:** * Displaying page numbers (1, 2, 3...) * Users need to jump to specific pages * Total count is needed * Data is relatively stable **Avoid when:** * Real-time feeds with frequent insertions * Large datasets (counting is expensive) * Infinite scroll UX ### Cursor-Based Pagination **Use when:** * Infinite scroll UI * Real-time feeds * Large datasets * Data changes frequently **Avoid when:** * Need to show page numbers * Need to jump to specific pages * Total count is required ## Client Implementation ### iOS (Swift) ```swift theme={null} // Page-based struct PageResponse: Decodable { let data: [T] let page: Int let perPage: Int let total: Int let totalPages: Int enum CodingKeys: String, CodingKey { case data, page, total case perPage = "per_page" case totalPages = "total_pages" } } // Cursor-based struct CursorResponse: Decodable { let data: [T] let nextCursor: String? let prevCursor: String? let hasMore: Bool enum CodingKeys: String, CodingKey { case data case nextCursor = "next_cursor" case prevCursor = "prev_cursor" case hasMore = "has_more" } } class FeedViewModel: ObservableObject { @Published var items: [FeedItem] = [] @Published var hasMore = true @Published var isLoading = false private var cursor: String? func loadMore() async { guard !isLoading, hasMore else { return } isLoading = true var endpoint = "/api/feed?limit=20" if let cursor = cursor { endpoint += "&cursor=\(cursor)" } let response: CursorResponse = try await api.get(endpoint) items.append(contentsOf: response.data) cursor = response.nextCursor hasMore = response.hasMore isLoading = false } } ``` ### Android (Kotlin) ```kotlin theme={null} // Page-based data class PageResponse( val data: List, val page: Int, @SerializedName("per_page") val perPage: Int, val total: Int, @SerializedName("total_pages") val totalPages: Int ) // Cursor-based data class CursorResponse( val data: List, @SerializedName("next_cursor") val nextCursor: String?, @SerializedName("prev_cursor") val prevCursor: String?, @SerializedName("has_more") val hasMore: Boolean ) class FeedPagingSource( private val api: ApiService ) : PagingSource() { override suspend fun load(params: LoadParams): LoadResult { return try { val response = api.getFeed( cursor = params.key, limit = params.loadSize ) LoadResult.Page( data = response.data, prevKey = response.prevCursor, nextKey = if (response.hasMore) response.nextCursor else null ) } catch (e: Exception) { LoadResult.Error(e) } } } ``` ### Flutter (Dart) ```dart theme={null} class PageResponse { final List data; final int page; final int perPage; final int total; final int totalPages; PageResponse({ required this.data, required this.page, required this.perPage, required this.total, required this.totalPages, }); } class CursorResponse { final List data; final String? nextCursor; final String? prevCursor; final bool hasMore; CursorResponse({ required this.data, this.nextCursor, this.prevCursor, required this.hasMore, }); } class FeedNotifier extends StateNotifier { String? _cursor; Future loadMore() async { if (state.isLoading || !state.hasMore) return; state = state.copyWith(isLoading: true); final response = await api.getFeed(cursor: _cursor, limit: 20); state = state.copyWith( items: [...state.items, ...response.data], hasMore: response.hasMore, isLoading: false, ); _cursor = response.nextCursor; } } ``` ## Next Steps Delta synchronization for offline-first apps Consistent error responses Complete API documentation # Production Source: https://docs.go-mizu.dev/mobile/production Deploy mobile backends to production with best practices. This guide covers production deployment considerations for Mizu mobile backends. ## Environment Configuration ```go theme={null} type Config struct { Port string Environment string MinAppVersion string MaintenanceMode bool } func LoadConfig() *Config { return &Config{ Port: getEnv("PORT", "3000"), Environment: getEnv("ENVIRONMENT", "development"), MinAppVersion: getEnv("MIN_APP_VERSION", "1.0.0"), MaintenanceMode: getEnv("MAINTENANCE_MODE", "false") == "true", } } ``` ## API Versioning Strategy ### Version Header ```go theme={null} app.Use(mobile.VersionMiddleware(mobile.VersionOptions{ Header: "X-API-Version", Supported: []mobile.Version{{3, 0}, {2, 0}}, Deprecated: []mobile.Version{{2, 0}}, Default: mobile.Version{Major: 3}, })) ``` ### Deprecation Timeline 1. **Month 1**: Release v3, mark v2 as deprecated 2. **Month 2**: Log v2 usage, notify users 3. **Month 3**: Raise minimum app version 4. **Month 4**: Remove v2 support ## Performance Optimization ### Response Caching ```go theme={null} app.Use(cache.New(cache.Config{ Expiration: 5 * time.Minute, KeyGenerator: func(c *mizu.Ctx) string { device := mobile.DeviceFromCtx(c) return c.Path() + ":" + device.Platform.String() }, })) ``` ### Gzip Compression ```go theme={null} app.Use(compress.New(compress.Config{ Level: compress.LevelBestSpeed, })) ``` ## Monitoring ### Request Logging ```go theme={null} app.Use(func(next mizu.Handler) mizu.Handler { return func(c *mizu.Ctx) error { device := mobile.DeviceFromCtx(c) start := time.Now() err := next(c) log.Info("request", "path", c.Path(), "method", c.Method(), "status", c.Response().StatusCode, "duration", time.Since(start), "platform", device.Platform, "app_version", device.AppVersion, ) return err } }) ``` ### Version Distribution ```go theme={null} // Track app version distribution metrics.RecordAppVersion(device.Platform, device.AppVersion) ``` ## Health Checks ```go theme={null} app.Get("/health", func(c *mizu.Ctx) error { return c.JSON(200, map[string]any{ "status": "healthy", "time": time.Now().UTC(), }) }) app.Get("/ready", func(c *mizu.Ctx) error { // Check database, cache, etc. if err := db.Ping(); err != nil { return c.JSON(503, map[string]string{ "status": "not ready", "error": "database unavailable", }) } return c.JSON(200, map[string]string{"status": "ready"}) }) ``` ## Graceful Shutdown ```go theme={null} func main() { app := mizu.New() setupRoutes(app) // Graceful shutdown app.ListenWithShutdown(":3000", 30*time.Second) } ``` ## Next Steps Security best practices Debug common issues # Push Notifications Source: https://docs.go-mizu.dev/mobile/push Cross-platform push token management for APNS, FCM, and WNS. The mobile package provides cross-platform push notification support, handling token registration, validation, and payload formatting for Apple Push Notification Service (APNS), Firebase Cloud Messaging (FCM), and Windows Notification Service (WNS). ## Quick Start ```go theme={null} import "github.com/go-mizu/mizu/mobile" app.Post("/api/push/register", func(c *mizu.Ctx) error { token := mobile.ParsePushToken(c) if token == nil { return mobile.SendError(c, 400, mobile.NewError( mobile.ErrInvalidRequest, "Missing push token", )) } // Validate token format if !mobile.ValidateToken(token.Token, token.Provider) { return mobile.SendError(c, 400, mobile.NewError( mobile.ErrValidation, "Invalid push token format", )) } // Save to database db.SavePushToken(token) return c.NoContent() }) ``` ## Push Providers ```go theme={null} const ( PushAPNS PushProvider = "apns" // Apple Push Notification Service PushFCM PushProvider = "fcm" // Firebase Cloud Messaging PushWNS PushProvider = "wns" // Windows Notification Service ) ``` ## The PushToken Type ```go theme={null} type PushToken struct { // Token is the push token value Token string `json:"token"` // Provider is the push service (apns, fcm, wns) Provider PushProvider `json:"provider"` // DeviceID is the associated device identifier DeviceID string `json:"device_id,omitempty"` // Sandbox indicates APNS sandbox environment Sandbox bool `json:"sandbox,omitempty"` // CreatedAt is when the token was registered CreatedAt time.Time `json:"created_at,omitempty"` // UpdatedAt is when the token was last updated UpdatedAt time.Time `json:"updated_at,omitempty"` // AppVersion is the app version when token was registered AppVersion string `json:"app_version,omitempty"` } ``` ## Token Parsing ### From Headers ```go theme={null} // Parse from X-Push-Token header token := mobile.ParsePushToken(c) // token.Token = header value // token.Provider = inferred from device context // token.DeviceID = from X-Device-ID header // token.AppVersion = from X-App-Version header ``` ### From Request Body ```go theme={null} type RegisterRequest struct { Token string `json:"token"` Provider mobile.PushProvider `json:"provider,omitempty"` Sandbox bool `json:"sandbox,omitempty"` Topics []string `json:"topics,omitempty"` } func registerPush(c *mizu.Ctx) error { var req RegisterRequest if err := c.BodyJSON(&req); err != nil { return mobile.SendError(c, 400, mobile.NewError( mobile.ErrInvalidRequest, "Invalid request")) } device := mobile.DeviceFromCtx(c) token := &mobile.PushToken{ Token: req.Token, Provider: req.Provider, DeviceID: device.DeviceID, Sandbox: req.Sandbox, AppVersion: device.AppVersion, CreatedAt: time.Now(), } // Auto-detect provider if not specified if token.Provider == "" { token.Provider = inferProvider(device.Platform) } // Validate and save... return c.NoContent() } ``` ## Token Validation ### APNS Tokens APNS tokens are 64 hexadecimal characters (32 bytes): ```go theme={null} // Validates APNS token format mobile.ValidateAPNS(token) // true/false // Example valid token: // "c4f3db1e2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d" ``` ### FCM Tokens FCM tokens are 140-200 characters, alphanumeric with `:` and `-`: ```go theme={null} // Validates FCM token format mobile.ValidateFCM(token) // true/false ``` ### Generic Validation ```go theme={null} // Validates based on provider mobile.ValidateToken(token, provider) // true/false // Auto-detects provider from format provider := detectProvider(token) ``` ## Push Registration Endpoint ```go theme={null} type PushRegistration struct { Token string `json:"token"` Provider mobile.PushProvider `json:"provider,omitempty"` Sandbox bool `json:"sandbox,omitempty"` Topics []string `json:"topics,omitempty"` } func (r *PushRegistration) Validate() error { if r.Token == "" { return mobile.NewError(mobile.ErrValidation, "push token is required") } if r.Provider != "" && !mobile.ValidateToken(r.Token, r.Provider) { return mobile.NewError(mobile.ErrValidation, "invalid push token format"). WithDetails("provider", r.Provider.String()) } return nil } func registerPush(c *mizu.Ctx) error { var reg mobile.PushRegistration if err := c.BodyJSON(®); err != nil { return mobile.SendError(c, 400, mobile.NewError( mobile.ErrInvalidRequest, "Invalid JSON")) } if err := reg.Validate(); err != nil { return mobile.SendError(c, 400, err.(*mobile.Error)) } device := mobile.DeviceFromCtx(c) // Store token err := db.UpsertPushToken(&PushTokenRecord{ Token: reg.Token, Provider: reg.Provider, DeviceID: device.DeviceID, UserID: getUserID(c), Topics: reg.Topics, Sandbox: reg.Sandbox, AppVersion: device.AppVersion, Platform: device.Platform.String(), UpdatedAt: time.Now(), }) if err != nil { return mobile.SendError(c, 500, mobile.NewError( mobile.ErrInternal, "Failed to save token")) } return c.NoContent() } ``` ## Push Payload The `PushPayload` type provides a unified format that converts to platform-specific payloads: ```go theme={null} type PushPayload struct { Title string `json:"title,omitempty"` Body string `json:"body,omitempty"` Badge *int `json:"badge,omitempty"` Sound string `json:"sound,omitempty"` Data map[string]any `json:"data,omitempty"` Category string `json:"category,omitempty"` ThreadID string `json:"thread_id,omitempty"` // iOS ChannelID string `json:"channel_id,omitempty"` // Android CollapseKey string `json:"collapse_key,omitempty"` // Android Priority string `json:"priority,omitempty"` TTL int `json:"ttl,omitempty"` ContentAvailable bool `json:"content_available,omitempty"` // iOS background MutableContent bool `json:"mutable_content,omitempty"` // iOS extensions } ``` ### Creating Payloads ```go theme={null} // Simple notification payload := &mobile.PushPayload{ Title: "New Message", Body: "You have a new message from Alice", Sound: "default", } // With data payload := &mobile.PushPayload{ Title: "New Order", Body: "Order #1234 has been placed", } payload.WithData("order_id", "1234") payload.WithData("action", "view_order") // With badge payload := &mobile.PushPayload{ Title: "3 new messages", Body: "You have unread messages", } payload.SetBadge(3) // Silent notification (iOS background fetch) payload := &mobile.PushPayload{ ContentAvailable: true, } payload.WithData("type", "sync") ``` ### Converting to APNS Format ```go theme={null} apnsPayload := payload.ToAPNS() // { // "aps": { // "alert": { // "title": "New Message", // "body": "You have a new message from Alice" // }, // "sound": "default", // "badge": 3 // }, // "order_id": "1234" // } ``` ### Converting to FCM Format ```go theme={null} fcmPayload := payload.ToFCM() // { // "notification": { // "title": "New Message", // "body": "You have a new message from Alice" // }, // "android": { // "notification": { // "channel_id": "messages" // } // }, // "data": { // "order_id": "1234" // } // } ``` ## Sending Notifications ### With APNS (Apple) ```go theme={null} import "github.com/sideshow/apns2" func sendAPNS(token *mobile.PushToken, payload *mobile.PushPayload) error { notification := &apns2.Notification{ DeviceToken: token.Token, Topic: "com.example.app", Payload: payload.ToAPNS(), } client := apns2.NewClient(cert) if token.Sandbox { client = client.Development() } else { client = client.Production() } res, err := client.Push(notification) if err != nil { return err } if !res.Sent() { // Handle failure (e.g., invalid token) if res.Reason == apns2.ReasonBadDeviceToken { db.DeletePushToken(token.Token) } return fmt.Errorf("APNS error: %s", res.Reason) } return nil } ``` ### With FCM (Firebase) ```go theme={null} import firebase "firebase.google.com/go/v4/messaging" func sendFCM(token *mobile.PushToken, payload *mobile.PushPayload) error { fcmPayload := payload.ToFCM() message := &firebase.Message{ Token: token.Token, Notification: &firebase.Notification{ Title: payload.Title, Body: payload.Body, }, Data: fcmPayload["data"].(map[string]string), } if android, ok := fcmPayload["android"].(map[string]any); ok { message.Android = &firebase.AndroidConfig{ Priority: "high", } if channelID, ok := android["notification"].(map[string]any)["channel_id"].(string); ok { message.Android.Notification = &firebase.AndroidNotification{ ChannelID: channelID, } } } _, err := fcmClient.Send(ctx, message) if err != nil { if firebase.IsRegistrationTokenNotRegistered(err) { db.DeletePushToken(token.Token) } return err } return nil } ``` ### Platform-Agnostic Sending ```go theme={null} func sendPush(token *mobile.PushToken, payload *mobile.PushPayload) error { switch token.Provider { case mobile.PushAPNS: return sendAPNS(token, payload) case mobile.PushFCM: return sendFCM(token, payload) case mobile.PushWNS: return sendWNS(token, payload) default: return fmt.Errorf("unknown provider: %s", token.Provider) } } // Send to all user devices func notifyUser(userID string, payload *mobile.PushPayload) error { tokens, err := db.GetPushTokensForUser(userID) if err != nil { return err } var errs []error for _, token := range tokens { if err := sendPush(token, payload); err != nil { errs = append(errs, err) } } if len(errs) > 0 { return fmt.Errorf("failed to send %d notifications", len(errs)) } return nil } ``` ## Topic Subscriptions ```go theme={null} type TopicSubscription struct { Token string `json:"token"` Topics []string `json:"topics"` } func subscribeTopics(c *mizu.Ctx) error { var req TopicSubscription c.BodyJSON(&req) device := mobile.DeviceFromCtx(c) // Update topics in database err := db.UpdateTokenTopics(req.Token, req.Topics) if err != nil { return mobile.SendError(c, 500, mobile.NewError( mobile.ErrInternal, "Failed to update topics")) } // For FCM, also subscribe on Firebase if device.Platform == mobile.PlatformAndroid { for _, topic := range req.Topics { fcmClient.SubscribeToTopic(ctx, []string{req.Token}, topic) } } return c.NoContent() } // Send to topic func sendToTopic(topic string, payload *mobile.PushPayload) error { // FCM native topic messaging message := &firebase.Message{ Topic: topic, Notification: &firebase.Notification{ Title: payload.Title, Body: payload.Body, }, } _, err := fcmClient.Send(ctx, message) return err } ``` ## Token Cleanup Remove invalid tokens: ```go theme={null} func cleanupTokens() { // Get tokens not updated in 30 days staleTokens := db.GetStaleTokens(30 * 24 * time.Hour) for _, token := range staleTokens { // Try to send a silent push to validate err := sendPush(token, &mobile.PushPayload{ ContentAvailable: true, }) if isInvalidTokenError(err) { db.DeletePushToken(token.Token) } } } func isInvalidTokenError(err error) bool { if err == nil { return false } // Check for APNS invalid token // Check for FCM unregistered token // etc. return strings.Contains(err.Error(), "InvalidToken") || strings.Contains(err.Error(), "NotRegistered") } ``` ## Client Implementation ### iOS (Swift) ```swift theme={null} import UserNotifications class PushManager: NSObject, UNUserNotificationCenterDelegate { func requestPermission() { UNUserNotificationCenter.current().requestAuthorization( options: [.alert, .badge, .sound] ) { granted, error in if granted { DispatchQueue.main.async { UIApplication.shared.registerForRemoteNotifications() } } } } func registerToken(_ deviceToken: Data) { let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined() Task { try await api.post("/api/push/register", body: [ "token": token, "provider": "apns", "sandbox": isDebug ]) } } } // In AppDelegate func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { PushManager.shared.registerToken(deviceToken) } ``` ### Android (Kotlin) ```kotlin theme={null} class MyFirebaseMessagingService : FirebaseMessagingService() { override fun onNewToken(token: String) { // Register token with server CoroutineScope(Dispatchers.IO).launch { api.post("/api/push/register") { contentType(ContentType.Application.Json) setBody(mapOf( "token" to token, "provider" to "fcm" )) } } } override fun onMessageReceived(message: RemoteMessage) { message.notification?.let { notification -> showNotification(notification.title, notification.body) } // Handle data payload message.data.let { data -> handlePushData(data) } } } ``` ### Flutter (Dart) ```dart theme={null} import 'package:firebase_messaging/firebase_messaging.dart'; class PushService { final FirebaseMessaging _messaging = FirebaseMessaging.instance; Future initialize() async { // Request permission await _messaging.requestPermission(); // Get token final token = await _messaging.getToken(); if (token != null) { await _registerToken(token); } // Listen for token refresh _messaging.onTokenRefresh.listen(_registerToken); // Handle foreground messages FirebaseMessaging.onMessage.listen(_handleMessage); } Future _registerToken(String token) async { await api.post('/api/push/register', data: { 'token': token, 'provider': Platform.isIOS ? 'apns' : 'fcm', }); } void _handleMessage(RemoteMessage message) { // Show local notification or update UI } } ``` ## Next Steps Universal and App Links Version checking and updates Complete API documentation # Progressive Web App Source: https://docs.go-mizu.dev/mobile/pwa Build offline-capable web apps with React, Vite, and Mizu backend integration. Build Progressive Web Apps (PWAs) that provide native-like experiences in the browser with offline support, push notifications, and home screen installation. ## Quick Start ```bash theme={null} mizu new ./my-pwa --template mobile/pwa ``` This creates a PWA with: * React + TypeScript * Vite for building * Service worker with offline support * Web Push notifications * App manifest for installation ## Project Structure ``` my-pwa/ ├── backend/ # Mizu Go backend │ └── ... │ ├── frontend/ │ ├── src/ │ │ ├── App.tsx │ │ ├── api/ │ │ ├── components/ │ │ ├── hooks/ │ │ └── sw.ts # Service worker │ ├── public/ │ │ ├── manifest.json │ │ └── icons/ │ ├── package.json │ └── vite.config.ts │ └── Makefile ``` ## Service Worker ```typescript theme={null} // src/sw.ts import { precacheAndRoute } from 'workbox-precaching'; import { registerRoute } from 'workbox-routing'; import { NetworkFirst, CacheFirst } from 'workbox-strategies'; // Precache static assets precacheAndRoute(self.__WB_MANIFEST); // API requests: Network first, fall back to cache registerRoute( ({ url }) => url.pathname.startsWith('/api'), new NetworkFirst({ cacheName: 'api-cache', networkTimeoutSeconds: 10, }) ); // Images: Cache first registerRoute( ({ request }) => request.destination === 'image', new CacheFirst({ cacheName: 'image-cache', }) ); // Push notifications self.addEventListener('push', (event) => { const data = event.data?.json(); event.waitUntil( self.registration.showNotification(data.title, { body: data.body, icon: '/icons/icon-192.png', data: data.data, }) ); }); ``` ## Web Push Notifications ```typescript theme={null} // src/hooks/usePushNotifications.ts export function usePushNotifications() { const subscribe = async () => { const permission = await Notification.requestPermission(); if (permission !== 'granted') return null; const registration = await navigator.serviceWorker.ready; const subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: import.meta.env.VITE_VAPID_PUBLIC_KEY, }); // Register with backend await api.post('/api/push/register', { token: JSON.stringify(subscription), provider: 'web', }); return subscription; }; return { subscribe }; } ``` ## App Manifest ```json theme={null} { "name": "My App", "short_name": "MyApp", "start_url": "/", "display": "standalone", "background_color": "#ffffff", "theme_color": "#007aff", "icons": [ { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" } ] } ``` ## Template Options ```bash theme={null} mizu new ./my-app --template mobile/pwa \ --var name=my-app \ --var workbox=true \ --var ui=tailwind \ --var push=true ``` | Variable | Description | Default | | --------- | ------------------------------- | -------------- | | `name` | Project name | Directory name | | `workbox` | Use Workbox for service worker | `false` | | `ui` | UI framework: `css`, `tailwind` | `css` | | `push` | Enable push notifications | `true` | ## Next Steps Build games with Unity All mobile templates # Quick Start Source: https://docs.go-mizu.dev/mobile/quick-start Create your first mobile backend with Mizu in minutes. This guide walks you through creating a mobile backend with Mizu. You'll have a working API with device detection, versioning, and mobile-optimized responses in under 5 minutes. ## Prerequisites Before starting, make sure you have: * **Go 1.22 or later** - [Download Go](https://go.dev/dl/) * **Mizu CLI** - Install with `go install github.com/go-mizu/mizu/cmd/cli@latest` Verify your installation: ```bash theme={null} go version # Should show 1.22 or later mizu version # Should show the installed Mizu CLI version ``` ## Create Your First Mobile Backend ### Step 1: Create a New Project Create a minimal Mizu project: ```bash theme={null} mizu new ./my-mobile-api --template minimal cd my-mobile-api ``` ### Step 2: Add Mobile Middleware Update your `main.go` to add mobile support: ```go theme={null} package main import ( "github.com/go-mizu/mizu" "github.com/go-mizu/mizu/mobile" ) func main() { app := mizu.New() // Add mobile device detection app.Use(mobile.New()) // Add API versioning app.Use(mobile.VersionMiddleware(mobile.VersionOptions{ Supported: []mobile.Version{{2, 0}, {1, 0}}, Deprecated: []mobile.Version{{1, 0}}, Default: mobile.Version{Major: 2}, })) // API routes app.Get("/api/hello", handleHello) app.Get("/api/users", handleUsers) app.Listen(":3000") } func handleHello(c *mizu.Ctx) error { device := mobile.DeviceFromCtx(c) return c.JSON(200, map[string]any{ "message": "Hello from Mizu Mobile!", "platform": device.Platform.String(), "app_version": device.AppVersion, "device_id": device.DeviceID, }) } func handleUsers(c *mizu.Ctx) error { version := mobile.VersionFromCtx(c) users := []map[string]any{ {"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}, } // Version-aware response if version.AtLeast(2, 0) { // v2: Include metadata return c.JSON(200, map[string]any{ "data": users, "count": len(users), }) } // v1: Simple array return c.JSON(200, users) } ``` ### Step 3: Run the Server ```bash theme={null} go run main.go ``` Your mobile backend is now running at `http://localhost:3000`. ## Test Your API ### Test with Mobile Headers ```bash theme={null} # Basic request with mobile headers curl -H "X-Device-ID: device-123" \ -H "X-App-Version: 2.1.0" \ -H "X-Platform: ios" \ http://localhost:3000/api/hello ``` Response: ```json theme={null} { "message": "Hello from Mizu Mobile!", "platform": "ios", "app_version": "2.1.0", "device_id": "device-123" } ``` ### Test API Versioning ```bash theme={null} # v2 API (default) curl -H "X-API-Version: v2" \ http://localhost:3000/api/users # Response: {"data": [...], "count": 2} # v1 API (deprecated) curl -H "X-API-Version: v1" \ http://localhost:3000/api/users # Response: [...] (simple array) # Response header: X-API-Deprecated: true ``` ## Add More Features ### Require Device ID ```go theme={null} app.Use(mobile.WithOptions(mobile.Options{ RequireDeviceID: true, })) ``` Now requests without `X-Device-ID` return 400 Bad Request. ### Enforce Minimum App Version ```go theme={null} app.Use(mobile.WithOptions(mobile.Options{ RequireAppVersion: true, MinAppVersion: "1.5.0", })) ``` Requests with app versions below 1.5.0 return 426 Upgrade Required. ### Add Offline Sync ```go theme={null} app.Get("/api/sync", func(c *mizu.Ctx) error { req := mobile.ParseSyncRequest(c) var delta mobile.Delta[Item] if req.IsInitial() { // Full sync delta.Created = getAllItems() } else { // Delta sync since := req.Since() delta.Created = getCreatedSince(since) delta.Updated = getUpdatedSince(since) delta.Deleted = getDeletedSince(since) } token := mobile.NewSyncToken(time.Now()) return c.JSON(200, mobile.NewSyncDelta(delta, token, false)) }) ``` ### Add Push Token Registration ```go theme={null} app.Post("/api/push/register", func(c *mizu.Ctx) error { token := mobile.ParsePushToken(c) if token == nil { return mobile.SendError(c, 400, mobile.NewError( mobile.ErrInvalidRequest, "Missing push token", )) } // Validate token format if !mobile.ValidateToken(token.Token, token.Provider) { return mobile.SendError(c, 400, mobile.NewError( mobile.ErrValidation, "Invalid push token format", )) } // Save token to database // db.SavePushToken(token) return c.NoContent() }) ``` ## Create a Mobile Client ### Using a Template Create a complete mobile client with matching SDK: ```bash theme={null} # iOS app mizu new ./my-ios-app --template mobile/ios # Android app mizu new ./my-android-app --template mobile/android # Flutter app mizu new ./my-flutter-app --template mobile/flutter # React Native app mizu new ./my-rn-app --template mobile/reactnative ``` ### Manual Client Setup #### iOS (Swift) ```swift theme={null} class APIClient { let baseURL = "http://localhost:3000" let deviceID = UIDevice.current.identifierForVendor?.uuidString ?? "" func request(_ endpoint: String) async throws -> Data { var request = URLRequest(url: URL(string: baseURL + endpoint)!) // Add mobile headers request.setValue(deviceID, forHTTPHeaderField: "X-Device-ID") request.setValue(Bundle.main.appVersion, forHTTPHeaderField: "X-App-Version") request.setValue("ios", forHTTPHeaderField: "X-Platform") request.setValue("v2", forHTTPHeaderField: "X-API-Version") let (data, _) = try await URLSession.shared.data(for: request) return data } } ``` #### Android (Kotlin) ```kotlin theme={null} class ApiClient(private val baseUrl: String) { private val client = OkHttpClient() private val deviceId = Settings.Secure.getString( context.contentResolver, Settings.Secure.ANDROID_ID ) fun createRequest(endpoint: String): Request { return Request.Builder() .url("$baseUrl$endpoint") .header("X-Device-ID", deviceId) .header("X-App-Version", BuildConfig.VERSION_NAME) .header("X-Platform", "android") .header("X-API-Version", "v2") .build() } } ``` #### Flutter (Dart) ```dart theme={null} class ApiClient { final String baseUrl; final String deviceId; ApiClient({required this.baseUrl, required this.deviceId}); Future get(String endpoint) { return http.get( Uri.parse('$baseUrl$endpoint'), headers: { 'X-Device-ID': deviceId, 'X-App-Version': packageInfo.version, 'X-Platform': Platform.operatingSystem, 'X-API-Version': 'v2', }, ); } } ``` ## Project Structure A typical mobile backend project: ``` my-mobile-api/ ├── cmd/ │ └── server/ │ └── main.go # Entry point ├── app/ │ └── server/ │ ├── app.go # Mizu app setup │ ├── config.go # Configuration │ ├── routes.go # Route definitions │ └── handlers/ │ ├── users.go # User handlers │ ├── sync.go # Sync handlers │ └── push.go # Push handlers ├── internal/ │ ├── models/ # Data models │ └── services/ # Business logic ├── go.mod └── Makefile ``` ## Common Patterns ### Platform-Specific Responses ```go theme={null} func handler(c *mizu.Ctx) error { device := mobile.DeviceFromCtx(c) response := map[string]any{ "message": "Hello", } // Platform-specific data switch device.Platform { case mobile.PlatformIOS: response["store_url"] = "https://apps.apple.com/app/id123" case mobile.PlatformAndroid: response["store_url"] = "https://play.google.com/store/apps/details?id=com.example" } return c.JSON(200, response) } ``` ### Graceful Version Migration ```go theme={null} func handler(c *mizu.Ctx) error { version := mobile.VersionFromCtx(c) switch { case version.AtLeast(3, 0): return handleV3(c) case version.AtLeast(2, 0): return handleV2(c) default: return handleV1(c) } } ``` ### Structured Error Responses ```go theme={null} func handler(c *mizu.Ctx) error { if !authorized { return mobile.SendError(c, 401, mobile.NewError( mobile.ErrUnauthorized, "Invalid credentials", ).WithDetails("reason", "token_expired")) } if !valid { return mobile.SendError(c, 400, mobile.NewError( mobile.ErrValidation, "Invalid request", ).WithDetails("field", "email"). WithDetails("error", "invalid format")) } return c.JSON(200, data) } ``` ## Next Steps You've created your first mobile backend! Here's what to explore next: Deep dive into device parsing and validation Learn about version management Implement delta synchronization Handle push token registration # React Native Source: https://docs.go-mizu.dev/mobile/reactnative Build cross-platform apps with TypeScript, Expo, and Mizu backend integration. Build cross-platform mobile applications with React Native and TypeScript that integrate seamlessly with your Mizu backend. The React Native template provides a complete project with Expo, navigation, and state management. ## Quick Start ```bash theme={null} mizu new ./my-rn-app --template mobile/reactnative ``` This creates a complete React Native project with: * TypeScript * Expo managed workflow * React Navigation * Zustand for state management * Axios for networking ## Project Structure ``` my-rn-app/ ├── backend/ # Mizu Go backend │ ├── cmd/server/ │ ├── app/server/ │ └── Makefile │ ├── app/ # React Native app │ ├── src/ │ │ ├── App.tsx │ │ ├── api/ │ │ │ ├── client.ts │ │ │ ├── endpoints.ts │ │ │ └── types.ts │ │ ├── components/ │ │ ├── hooks/ │ │ ├── navigation/ │ │ ├── screens/ │ │ ├── stores/ │ │ └── utils/ │ ├── app.json │ ├── package.json │ └── tsconfig.json │ └── Makefile ``` ## Template Options ```bash theme={null} mizu new ./my-app --template mobile/reactnative \ --var name=MyApp \ --var bundleId=com.example.app \ --var platforms=ios,android \ --var expo=true ``` | Variable | Description | Default | | ----------- | ------------------------- | ----------------- | | `name` | Project name | Directory name | | `bundleId` | Bundle identifier | `com.example.app` | | `platforms` | Target platforms | `ios,android` | | `expo` | Use Expo managed workflow | `true` | ## API Client ```typescript theme={null} // src/api/client.ts import axios, { AxiosInstance, InternalAxiosRequestConfig } from 'axios'; import DeviceInfo from 'react-native-device-info'; import { Platform } from 'react-native'; import { useAuthStore } from '../stores/authStore'; const API_URL = process.env.EXPO_PUBLIC_API_URL ?? 'http://localhost:3000'; class ApiClient { private client: AxiosInstance; private deviceId: string | null = null; constructor() { this.client = axios.create({ baseURL: API_URL, timeout: 30000, headers: { 'Content-Type': 'application/json', }, }); this.setupInterceptors(); } private async setupInterceptors() { // Request interceptor this.client.interceptors.request.use( async (config: InternalAxiosRequestConfig) => { // Add Mizu headers await this.addMizuHeaders(config); // Add auth token const token = useAuthStore.getState().accessToken; if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; } ); // Response interceptor this.client.interceptors.response.use( (response) => { // Check for deprecation warning if (response.headers['x-api-deprecated'] === 'true') { console.warn('API version is deprecated'); } return response; }, (error) => { if (error.response?.status === 401) { useAuthStore.getState().logout(); } return Promise.reject(error); } ); } private async addMizuHeaders(config: InternalAxiosRequestConfig) { if (!this.deviceId) { this.deviceId = await DeviceInfo.getUniqueId(); } config.headers['X-Device-ID'] = this.deviceId; config.headers['X-App-Version'] = DeviceInfo.getVersion(); config.headers['X-App-Build'] = DeviceInfo.getBuildNumber(); config.headers['X-Device-Model'] = DeviceInfo.getModel(); config.headers['X-Platform'] = Platform.OS; config.headers['X-OS-Version'] = Platform.Version.toString(); config.headers['X-Timezone'] = Intl.DateTimeFormat().resolvedOptions().timeZone; config.headers['X-API-Version'] = 'v2'; } get = (url: string, params?: object) => this.client.get(url, { params }); post = (url: string, data?: object) => this.client.post(url, data); put = (url: string, data?: object) => this.client.put(url, data); delete = (url: string) => this.client.delete(url); } export const api = new ApiClient(); ``` ## State Management with Zustand ### Auth Store ```typescript theme={null} // src/stores/authStore.ts import { create } from 'zustand'; import { persist, createJSONStorage } from 'zustand/middleware'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { api } from '../api/client'; interface User { id: string; email: string; name: string; } interface AuthState { user: User | null; accessToken: string | null; refreshToken: string | null; isAuthenticated: boolean; login: (email: string, password: string) => Promise; logout: () => void; refreshAuth: () => Promise; } export const useAuthStore = create()( persist( (set, get) => ({ user: null, accessToken: null, refreshToken: null, isAuthenticated: false, login: async (email: string, password: string) => { const { data } = await api.post<{ user: User; access_token: string; refresh_token: string; }>('/api/auth/login', { email, password }); set({ user: data.user, accessToken: data.access_token, refreshToken: data.refresh_token, isAuthenticated: true, }); }, logout: () => { set({ user: null, accessToken: null, refreshToken: null, isAuthenticated: false, }); }, refreshAuth: async () => { const { refreshToken } = get(); if (!refreshToken) throw new Error('No refresh token'); const { data } = await api.post<{ access_token: string; }>('/api/auth/refresh', { token: refreshToken }); set({ accessToken: data.access_token }); }, }), { name: 'auth-storage', storage: createJSONStorage(() => AsyncStorage), partialize: (state) => ({ user: state.user, accessToken: state.accessToken, refreshToken: state.refreshToken, isAuthenticated: state.isAuthenticated, }), } ) ); ``` ### Items Store ```typescript theme={null} // src/stores/itemsStore.ts import { create } from 'zustand'; import { api } from '../api/client'; interface Item { id: string; name: string; description: string; } interface ItemsState { items: Item[]; isLoading: boolean; error: string | null; fetchItems: () => Promise; addItem: (item: Omit) => Promise; removeItem: (id: string) => Promise; } export const useItemsStore = create()((set, get) => ({ items: [], isLoading: false, error: null, fetchItems: async () => { set({ isLoading: true, error: null }); try { const { data } = await api.get<{ data: Item[] }>('/api/items'); set({ items: data.data, isLoading: false }); } catch (error) { set({ error: 'Failed to fetch items', isLoading: false }); } }, addItem: async (item) => { const { data } = await api.post('/api/items', item); set({ items: [...get().items, data] }); }, removeItem: async (id) => { await api.delete(`/api/items/${id}`); set({ items: get().items.filter((i) => i.id !== id) }); }, })); ``` ## Navigation ```typescript theme={null} // src/navigation/RootNavigator.tsx import { NavigationContainer } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { useAuthStore } from '../stores/authStore'; // Screens import { LoginScreen } from '../screens/LoginScreen'; import { HomeScreen } from '../screens/HomeScreen'; import { ProfileScreen } from '../screens/ProfileScreen'; import { ItemDetailScreen } from '../screens/ItemDetailScreen'; const Stack = createNativeStackNavigator(); const Tab = createBottomTabNavigator(); function MainTabs() { return ( ); } export function RootNavigator() { const isAuthenticated = useAuthStore((s) => s.isAuthenticated); return ( {isAuthenticated ? ( <> ) : ( )} ); } ``` ## Screens ```typescript theme={null} // src/screens/HomeScreen.tsx import React, { useEffect } from 'react'; import { View, FlatList, Text, TouchableOpacity, RefreshControl, StyleSheet, } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import { useItemsStore } from '../stores/itemsStore'; export function HomeScreen() { const navigation = useNavigation(); const { items, isLoading, error, fetchItems } = useItemsStore(); useEffect(() => { fetchItems(); }, []); if (error) { return ( {error} Retry ); } return ( item.id} refreshControl={ } renderItem={({ item }) => ( navigation.navigate('ItemDetail', { id: item.id })} > {item.name} {item.description} )} ListEmptyComponent={ !isLoading ? ( No items found ) : null } /> ); } const styles = StyleSheet.create({ center: { flex: 1, justifyContent: 'center', alignItems: 'center' }, error: { color: 'red', marginBottom: 16 }, retry: { color: 'blue' }, item: { padding: 16, borderBottomWidth: 1, borderBottomColor: '#eee' }, itemTitle: { fontSize: 18, fontWeight: '600' }, itemDescription: { color: '#666', marginTop: 4 }, }); ``` ## Push Notifications ```typescript theme={null} // src/services/pushService.ts import messaging from '@react-native-firebase/messaging'; import { api } from '../api/client'; import { Platform } from 'react-native'; export async function initializePushNotifications() { // Request permission const authStatus = await messaging().requestPermission(); const enabled = authStatus === messaging.AuthorizationStatus.AUTHORIZED || authStatus === messaging.AuthorizationStatus.PROVISIONAL; if (!enabled) { console.log('Push notifications not authorized'); return; } // Get token const token = await messaging().getToken(); await registerToken(token); // Listen for token refresh messaging().onTokenRefresh(registerToken); // Handle foreground messages messaging().onMessage(async (remoteMessage) => { console.log('Foreground message:', remoteMessage); // Show local notification }); // Handle background messages messaging().setBackgroundMessageHandler(async (remoteMessage) => { console.log('Background message:', remoteMessage); }); } async function registerToken(token: string) { try { await api.post('/api/push/register', { token, provider: Platform.OS === 'ios' ? 'apns' : 'fcm', }); } catch (error) { console.error('Failed to register push token:', error); } } ``` ## Deep Links ```typescript theme={null} // src/services/deepLinkService.ts import { Linking } from 'react-native'; import { NavigationContainerRef } from '@react-navigation/native'; let navigationRef: NavigationContainerRef | null = null; export function setNavigationRef(ref: NavigationContainerRef) { navigationRef = ref; } export async function initializeDeepLinks() { // Handle initial URL const initialUrl = await Linking.getInitialURL(); if (initialUrl) { handleDeepLink(initialUrl); } // Listen for incoming URLs Linking.addEventListener('url', ({ url }) => { handleDeepLink(url); }); } function handleDeepLink(url: string) { if (!navigationRef) return; const parsed = new URL(url); const path = parsed.pathname; if (path.startsWith('/share/')) { const id = path.replace('/share/', ''); navigationRef.navigate('ItemDetail', { id }); } else if (path.startsWith('/profile/')) { navigationRef.navigate('Profile'); } } ``` ## Offline Sync ```typescript theme={null} // src/services/syncService.ts import AsyncStorage from '@react-native-async-storage/async-storage'; import { api } from '../api/client'; const SYNC_TOKEN_KEY = 'sync_token'; export async function sync() { const syncToken = await AsyncStorage.getItem(SYNC_TOKEN_KEY); const params: Record = {}; if (syncToken) { params.sync_token = syncToken; } const { data } = await api.get('/api/sync', params); // Apply changes to local storage await applyChanges(data); // Save new sync token await AsyncStorage.setItem(SYNC_TOKEN_KEY, data.sync_token); } async function applyChanges(response: SyncResponse) { // Get current items const itemsJson = await AsyncStorage.getItem('items'); const items: Map = new Map( itemsJson ? JSON.parse(itemsJson) : [] ); // Apply created for (const item of response.created) { items.set(item.id, item); } // Apply updated for (const item of response.updated) { items.set(item.id, item); } // Apply deleted for (const id of response.deleted) { items.delete(id); } // Save back await AsyncStorage.setItem('items', JSON.stringify([...items])); } ``` ## Running the Project ### Start Backend ```bash theme={null} cd backend make run ``` ### Run React Native App ```bash theme={null} cd app # Install dependencies npm install # iOS npx expo run:ios # Android npx expo run:android # Development with Expo Go npx expo start ``` ## Best Practices ### Type Safety ```typescript theme={null} // src/api/types.ts export interface PageResponse { data: T[]; page: number; per_page: number; total: number; total_pages: number; } export interface ApiError { code: string; message: string; details?: Record; } // Type-safe API calls const response = await api.get>('/api/items'); ``` ### Error Handling ```typescript theme={null} // src/utils/errorHandler.ts import { AxiosError } from 'axios'; import { Alert } from 'react-native'; export function handleApiError(error: unknown) { if (error instanceof AxiosError) { const apiError = error.response?.data as ApiError | undefined; switch (apiError?.code) { case 'upgrade_required': Alert.alert('Update Required', 'Please update the app to continue.'); break; case 'maintenance': Alert.alert('Maintenance', apiError.message); break; default: Alert.alert('Error', apiError?.message ?? 'Something went wrong'); } } } ``` ## Next Steps Build with Flutter FCM integration Universal Links All mobile templates # Security Source: https://docs.go-mizu.dev/mobile/security Security best practices for mobile backends. This guide covers security considerations for Mizu mobile backends. ## Authentication ### JWT Tokens ```go theme={null} func authMiddleware(secret []byte) mizu.Middleware { return func(next mizu.Handler) mizu.Handler { return func(c *mizu.Ctx) error { auth := c.Request().Header.Get("Authorization") if !strings.HasPrefix(auth, "Bearer ") { return mobile.SendError(c, 401, mobile.NewError( mobile.ErrUnauthorized, "Missing token")) } token := strings.TrimPrefix(auth, "Bearer ") claims, err := validateJWT(token, secret) if err != nil { return mobile.SendError(c, 401, mobile.NewError( mobile.ErrUnauthorized, "Invalid token")) } c.Set("user_id", claims.UserID) return next(c) } } } ``` ### Token Refresh ```go theme={null} app.Post("/api/auth/refresh", func(c *mizu.Ctx) error { var req struct { RefreshToken string `json:"refresh_token"` } c.BodyJSON(&req) // Validate refresh token claims, err := validateRefreshToken(req.RefreshToken) if err != nil { return mobile.SendError(c, 401, mobile.NewError( mobile.ErrUnauthorized, "Invalid refresh token")) } // Issue new access token accessToken, err := createAccessToken(claims.UserID) if err != nil { return mobile.SendError(c, 500, mobile.NewError( mobile.ErrInternal, "Failed to create token")) } return c.JSON(200, map[string]string{ "access_token": accessToken, }) }) ``` ## Rate Limiting ```go theme={null} app.Use(limiter.New(limiter.Config{ Max: 100, Expiration: time.Minute, KeyGenerator: func(c *mizu.Ctx) string { device := mobile.DeviceFromCtx(c) if device != nil && device.DeviceID != "" { return device.DeviceID } return c.IP() }, LimitReached: func(c *mizu.Ctx) error { return mobile.SendError(c, 429, mobile.NewError( mobile.ErrRateLimited, "Too many requests"). WithDetails("retry_after", 60)) }, })) ``` ## Input Validation ```go theme={null} func createUser(c *mizu.Ctx) error { var req CreateUserRequest if err := c.BodyJSON(&req); err != nil { return mobile.SendError(c, 400, mobile.NewError( mobile.ErrInvalidRequest, "Invalid JSON")) } // Validate if !isValidEmail(req.Email) { return mobile.SendError(c, 400, mobile.NewError( mobile.ErrValidation, "Invalid email")) } if len(req.Password) < 8 { return mobile.SendError(c, 400, mobile.NewError( mobile.ErrValidation, "Password too short")) } // Continue... } ``` ## Security Headers ```go theme={null} app.Use(func(next mizu.Handler) mizu.Handler { return func(c *mizu.Ctx) error { c.Header().Set("X-Content-Type-Options", "nosniff") c.Header().Set("X-Frame-Options", "DENY") c.Header().Set("X-XSS-Protection", "1; mode=block") c.Header().Set("Strict-Transport-Security", "max-age=31536000") return next(c) } }) ``` ## Client Security ### iOS: Keychain Storage ```swift theme={null} // Store tokens in Keychain, not UserDefaults KeychainHelper.save(accessToken, key: "access_token") ``` ### Android: EncryptedSharedPreferences ```kotlin theme={null} val prefs = EncryptedSharedPreferences.create( context, "secure_prefs", masterKey, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM ) ``` ### Certificate Pinning ```swift theme={null} // iOS let serverTrustPolicy = ServerTrustPolicy.pinCertificates( certificates: ServerTrustPolicy.certificates() ) ``` ```kotlin theme={null} // Android CertificatePinner.Builder() .add("api.example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") .build() ``` ## Next Steps Production deployment Complete API documentation # Offline Sync Source: https://docs.go-mizu.dev/mobile/sync Delta synchronization for offline-first mobile applications. The mobile package provides delta synchronization helpers for building offline-first mobile applications. Sync tokens enable efficient incremental updates without re-downloading entire datasets. ## Quick Start ```go theme={null} import "github.com/go-mizu/mizu/mobile" app.Get("/api/sync", func(c *mizu.Ctx) error { req := mobile.ParseSyncRequest(c) var delta mobile.Delta[Item] if req.IsInitial() { // Full sync - first time or forced delta.Created = db.GetAllItems() } else { // Delta sync - only changes since last sync since := req.Since() delta.Created = db.GetCreatedSince(since) delta.Updated = db.GetUpdatedSince(since) delta.Deleted = db.GetDeletedSince(since) } token := mobile.NewSyncToken(time.Now()) return c.JSON(200, mobile.NewSyncDelta(delta, token, false)) }) ``` ## Sync Tokens Sync tokens are opaque strings that encode a timestamp. Clients store the token and send it back on subsequent syncs to receive only changes since that point. ### Creating Tokens ```go theme={null} // Create from current time token := mobile.NewSyncToken(time.Now()) // Create from specific time token := mobile.NewSyncToken(lastModifiedAt) ``` ### Token Methods ```go theme={null} token := mobile.NewSyncToken(time.Now()) // Convert to string for response token.String() // "YWJjMTIz..." // Check if empty token.IsEmpty() // false // Extract timestamp t := token.Time() // time.Time ``` ### Response Header ```go theme={null} // Set sync token in response header mobile.SetSyncToken(c, token) // Sets: X-Sync-Token: abc123... ``` ## Sync Request Parse sync parameters from the client: ```go theme={null} type SyncRequest struct { Token SyncToken // Last sync token (empty for initial) Resources []string // Specific resources to sync FullSync bool // Force complete resync Limit int // Maximum items to return } ``` ### Parsing Requests ```go theme={null} req := mobile.ParseSyncRequest(c) // Parsed from: // Header: X-Sync-Token // Query: ?sync_token=abc123 // Query: ?full_sync=true // Query: ?resources=users,posts // Query: ?limit=100 ``` ### Request Methods ```go theme={null} req := mobile.ParseSyncRequest(c) // Check if initial sync req.IsInitial() // true if no token or full_sync=true // Get timestamp to sync from since := req.Since() // time.Time (zero for initial) // Check specific resources for _, r := range req.Resources { // Sync only requested resources } ``` ## Delta Response The `Delta` type represents changes since the last sync: ```go theme={null} type Delta[T any] struct { Created []T `json:"created,omitempty"` Updated []T `json:"updated,omitempty"` Deleted []string `json:"deleted,omitempty"` // IDs of deleted items } ``` ### Creating Delta Responses ```go theme={null} delta := mobile.Delta[Item]{ Created: newItems, Updated: modifiedItems, Deleted: deletedIDs, } // Check if empty delta.IsEmpty() // true if no changes // Count total changes delta.Count() // len(created) + len(updated) + len(deleted) ``` ### Wrapped Response ```go theme={null} type SyncDelta[T any] struct { Delta[T] SyncToken SyncToken `json:"sync_token"` HasMore bool `json:"has_more"` FullSync bool `json:"full_sync,omitempty"` } ``` ```go theme={null} // Create sync delta response := mobile.NewSyncDelta(delta, token, hasMore) // For full sync, mark it response := mobile.NewFullSyncDelta(delta, token, hasMore) ``` ## Complete Example ### Database Schema ```sql theme={null} CREATE TABLE items ( id TEXT PRIMARY KEY, name TEXT NOT NULL, data JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), deleted_at TIMESTAMPTZ -- Soft delete ); CREATE INDEX idx_items_updated ON items(updated_at); CREATE INDEX idx_items_deleted ON items(deleted_at); ``` ### Model ```go theme={null} type Item struct { ID string `json:"id" db:"id"` Name string `json:"name" db:"name"` Data any `json:"data" db:"data"` CreatedAt time.Time `json:"created_at" db:"created_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"` } ``` ### Repository ```go theme={null} type ItemRepo struct { db *sql.DB } func (r *ItemRepo) GetAll() ([]Item, error) { return r.query(` SELECT * FROM items WHERE deleted_at IS NULL ORDER BY created_at `) } func (r *ItemRepo) GetCreatedSince(since time.Time) ([]Item, error) { return r.query(` SELECT * FROM items WHERE deleted_at IS NULL AND created_at > $1 ORDER BY created_at `, since) } func (r *ItemRepo) GetUpdatedSince(since time.Time) ([]Item, error) { return r.query(` SELECT * FROM items WHERE deleted_at IS NULL AND updated_at > $1 AND created_at <= $1 -- Exclude newly created ORDER BY updated_at `, since) } func (r *ItemRepo) GetDeletedSince(since time.Time) ([]string, error) { rows, err := r.db.Query(` SELECT id FROM items WHERE deleted_at IS NOT NULL AND deleted_at > $1 `, since) if err != nil { return nil, err } defer rows.Close() var ids []string for rows.Next() { var id string rows.Scan(&id) ids = append(ids, id) } return ids, nil } ``` ### Handler ```go theme={null} func syncItems(c *mizu.Ctx) error { req := mobile.ParseSyncRequest(c) repo := &ItemRepo{db: db} var delta mobile.Delta[Item] var err error if req.IsInitial() { // Full sync delta.Created, err = repo.GetAll() if err != nil { return mobile.SendError(c, 500, mobile.NewError( mobile.ErrInternal, "Failed to fetch items")) } } else { since := req.Since() delta.Created, err = repo.GetCreatedSince(since) if err != nil { return mobile.SendError(c, 500, mobile.NewError( mobile.ErrInternal, "Failed to fetch created items")) } delta.Updated, err = repo.GetUpdatedSince(since) if err != nil { return mobile.SendError(c, 500, mobile.NewError( mobile.ErrInternal, "Failed to fetch updated items")) } delta.Deleted, err = repo.GetDeletedSince(since) if err != nil { return mobile.SendError(c, 500, mobile.NewError( mobile.ErrInternal, "Failed to fetch deleted items")) } } // Create new token with current time token := mobile.NewSyncToken(time.Now()) // Check if there are more items (pagination) hasMore := delta.Count() >= req.Limit // Set token in header too mobile.SetSyncToken(c, token) if req.IsInitial() { return c.JSON(200, mobile.NewFullSyncDelta(delta, token, hasMore)) } return c.JSON(200, mobile.NewSyncDelta(delta, token, hasMore)) } ``` ## Conflict Resolution Handle conflicts when the same item is modified on client and server: ```go theme={null} type Conflict[T any] struct { ID string `json:"id"` ClientData T `json:"client_data"` ServerData T `json:"server_data"` ClientTime time.Time `json:"client_time"` ServerTime time.Time `json:"server_time"` } type ConflictResolution string const ( ResolutionServerWins ConflictResolution = "server_wins" ResolutionClientWins ConflictResolution = "client_wins" ResolutionLatestWins ConflictResolution = "latest_wins" ResolutionManual ConflictResolution = "manual" ) ``` ### Detecting Conflicts ```go theme={null} type UpdateRequest struct { ID string `json:"id"` Data Item `json:"data"` ClientTime time.Time `json:"client_time"` } func updateItem(c *mizu.Ctx) error { var req UpdateRequest if err := c.BodyJSON(&req); err != nil { return mobile.SendError(c, 400, mobile.NewError( mobile.ErrInvalidRequest, "Invalid request")) } // Get current server version serverItem, err := repo.GetByID(req.ID) if err != nil { return mobile.SendError(c, 404, mobile.NewError( mobile.ErrNotFound, "Item not found")) } // Check for conflict if serverItem.UpdatedAt.After(req.ClientTime) { return c.JSON(409, mobile.Conflict[Item]{ ID: req.ID, ClientData: req.Data, ServerData: serverItem, ClientTime: req.ClientTime, ServerTime: serverItem.UpdatedAt, }) } // No conflict, apply update updated, err := repo.Update(req.ID, req.Data) if err != nil { return mobile.SendError(c, 500, mobile.NewError( mobile.ErrInternal, "Failed to update")) } return c.JSON(200, updated) } ``` ### Resolving Conflicts ```go theme={null} type ResolveConflictRequest struct { ID string `json:"id"` Resolution ConflictResolution `json:"resolution"` MergedData *Item `json:"merged_data,omitempty"` } func resolveConflict(c *mizu.Ctx) error { var req ResolveConflictRequest c.BodyJSON(&req) serverItem, _ := repo.GetByID(req.ID) clientItem := getClientItem(c) // From request or session var finalData Item switch req.Resolution { case mobile.ResolutionServerWins: finalData = serverItem case mobile.ResolutionClientWins: finalData = clientItem case mobile.ResolutionLatestWins: if clientItem.UpdatedAt.After(serverItem.UpdatedAt) { finalData = clientItem } else { finalData = serverItem } case mobile.ResolutionManual: if req.MergedData == nil { return mobile.SendError(c, 400, mobile.NewError( mobile.ErrInvalidRequest, "Merged data required")) } finalData = *req.MergedData } updated, _ := repo.Update(req.ID, finalData) return c.JSON(200, updated) } ``` ## Multi-Resource Sync Sync multiple resource types in a single request: ```go theme={null} type SyncResponse struct { Users mobile.SyncDelta[User] `json:"users,omitempty"` Posts mobile.SyncDelta[Post] `json:"posts,omitempty"` Comments mobile.SyncDelta[Comment] `json:"comments,omitempty"` SyncToken mobile.SyncToken `json:"sync_token"` } func syncAll(c *mizu.Ctx) error { req := mobile.ParseSyncRequest(c) since := req.Since() response := SyncResponse{} // Check which resources to sync syncUsers := len(req.Resources) == 0 || contains(req.Resources, "users") syncPosts := len(req.Resources) == 0 || contains(req.Resources, "posts") syncComments := len(req.Resources) == 0 || contains(req.Resources, "comments") if syncUsers { response.Users = getUsersDelta(since, req.IsInitial()) } if syncPosts { response.Posts = getPostsDelta(since, req.IsInitial()) } if syncComments { response.Comments = getCommentsDelta(since, req.IsInitial()) } response.SyncToken = mobile.NewSyncToken(time.Now()) return c.JSON(200, response) } ``` ## Client Implementation ### iOS (Swift) ```swift theme={null} class SyncManager { private var syncToken: String? { get { UserDefaults.standard.string(forKey: "sync_token") } set { UserDefaults.standard.set(newValue, forKey: "sync_token") } } func sync() async throws { var endpoint = "/api/sync" if let token = syncToken { endpoint += "?sync_token=\(token)" } let response: SyncResponse = try await api.get(endpoint) // Apply changes to local database try await db.write { realm in // Add created items for item in response.created { realm.add(item, update: .modified) } // Update modified items for item in response.updated { realm.add(item, update: .modified) } // Delete removed items for id in response.deleted { if let item = realm.object(ofType: Item.self, forPrimaryKey: id) { realm.delete(item) } } } // Store new sync token syncToken = response.syncToken } } ``` ### Android (Kotlin) ```kotlin theme={null} class SyncManager( private val api: ApiService, private val dao: ItemDao, private val prefs: SharedPreferences ) { private var syncToken: String? get() = prefs.getString("sync_token", null) set(value) = prefs.edit().putString("sync_token", value).apply() suspend fun sync() { val response = api.sync(syncToken) dao.withTransaction { // Insert created items dao.insertAll(response.created) // Update modified items response.updated.forEach { dao.update(it) } // Delete removed items response.deleted.forEach { dao.deleteById(it) } } // Store new sync token syncToken = response.syncToken } } ``` ### Flutter (Dart) ```dart theme={null} class SyncManager { String? _syncToken; Future sync() async { final params = _syncToken != null ? {'sync_token': _syncToken} : {}; final response = await api.get('/api/sync', queryParameters: params); final syncResponse = SyncResponse.fromJson(response.data); // Apply changes to local database await db.transaction(() async { // Add created items for (final item in syncResponse.created) { await db.items.insertOnConflictUpdate(item); } // Update modified items for (final item in syncResponse.updated) { await db.items.update(item); } // Delete removed items for (final id in syncResponse.deleted) { await db.items.deleteWhere((t) => t.id.equals(id)); } }); // Store new sync token _syncToken = syncResponse.syncToken; await prefs.setString('sync_token', _syncToken!); } } ``` ## Best Practices ### Use Soft Deletes Never hard-delete records. Use a `deleted_at` timestamp so clients can receive deletion events. ### Index Updated Timestamps Ensure `updated_at` and `deleted_at` columns are indexed for efficient delta queries. ### Handle Large Initial Syncs For large datasets, paginate initial syncs: ```go theme={null} if req.IsInitial() && delta.Count() > maxInitialSync { hasMore = true delta.Created = delta.Created[:maxInitialSync] } ``` ### Implement Retry Logic Clients should retry failed syncs with exponential backoff. ### Store Sync State Atomically Store the sync token only after successfully applying all changes locally. ## Next Steps Cross-platform push token management Universal and App Links Complete API documentation # Templates Source: https://docs.go-mizu.dev/mobile/templates Available mobile templates and how to use them. Mizu provides ready-to-use templates for mobile development across native and cross-platform frameworks. ## Available Templates ```bash theme={null} # List all mobile templates mizu new --list-templates mobile ``` | Template | Framework | Description | | -------------------- | -------------------- | ---------------------- | | `mobile/ios` | Swift + SwiftUI | Native iOS app | | `mobile/android` | Kotlin + Compose | Native Android app | | `mobile/flutter` | Dart + Riverpod | Cross-platform Flutter | | `mobile/reactnative` | TypeScript + Expo | React Native app | | `mobile/pwa` | React + Vite | Progressive Web App | | `mobile/kmm` | Kotlin Multiplatform | Shared Kotlin code | | `mobile/dotnet` | C# + .NET MAUI | Cross-platform .NET | | `mobile/game` | Unity + C# | Mobile game | ## Using Templates ### Create New Project ```bash theme={null} mizu new ./my-app --template mobile/ios ``` ### With Variables ```bash theme={null} mizu new ./my-app --template mobile/ios \ --var name=MyApp \ --var bundleId=com.example.myapp \ --var minIOS=16.0 ``` ### What's Included Each template includes: * **Backend**: Complete Mizu server with mobile middleware * **Mobile App**: Pre-configured client with API integration * **Networking**: API client with Mizu headers * **Authentication**: Login/logout flow * **Push Setup**: Push notification configuration * **Makefile**: Common development commands ## Template Comparison | Feature | iOS | Android | Flutter | RN | PWA | KMM | .NET | Unity | | ------------- | ------------ | --------- | ----------- | ------------ | ------------ | ----------- | ----------- | --------------- | | **Language** | Swift | Kotlin | Dart | TS | TS | Kotlin | C# | C# | | **UI** | SwiftUI | Compose | Flutter | React | React | Native | MAUI | Unity | | **State** | @Observable | Flow | Riverpod | Zustand | Zustand | Flow | MVVM | - | | **Network** | URLSession | Retrofit | Dio | Axios | Fetch | Ktor | HttpClient | UnityWebRequest | | **Storage** | UserDefaults | DataStore | SharedPrefs | AsyncStorage | localStorage | - | Preferences | PlayerPrefs | | **Platforms** | iOS | Android | All | iOS/Android | Web | iOS/Android | All | All | ## Template Variables ### iOS | Variable | Description | Default | | ---------- | -------------------------------- | ---------------------- | | `name` | Project name | Directory name | | `bundleId` | Bundle identifier | `com.example.{{name}}` | | `minIOS` | Minimum iOS version | `16.0` | | `ui` | UI framework: `swiftui`, `uikit` | `swiftui` | ### Android | Variable | Description | Default | | --------- | -------------------------------- | ---------------------- | | `name` | Project name | Directory name | | `package` | Package name | `com.example.{{name}}` | | `minSdk` | Minimum SDK version | `26` | | `ui` | UI framework: `compose`, `views` | `compose` | ### Flutter | Variable | Description | Default | | ----------- | ------------------------------------- | -------------- | | `name` | Project name | Directory name | | `org` | Organization identifier | `com.example` | | `platforms` | Target platforms | `ios,android` | | `ui` | UI framework: `material`, `cupertino` | `material` | ### React Native | Variable | Description | Default | | ----------- | ----------------- | ----------------- | | `name` | Project name | Directory name | | `bundleId` | Bundle identifier | `com.example.app` | | `platforms` | Target platforms | `ios,android` | | `expo` | Use Expo workflow | `true` | ### PWA | Variable | Description | Default | | --------- | ------------------------------- | -------------- | | `name` | Project name | Directory name | | `workbox` | Use Workbox | `false` | | `ui` | UI framework: `css`, `tailwind` | `css` | | `push` | Enable push notifications | `true` | ### KMM | Variable | Description | Default | | ----------- | ----------------------------------- | ----------------- | | `name` | Project name | Directory name | | `package` | Package name | `com.example.app` | | `platforms` | Target platforms | `android,ios` | | `storage` | Local storage: `none`, `sqldelight` | `none` | ### .NET MAUI | Variable | Description | Default | | ----------- | ---------------------------- | ----------------- | | `name` | Project name | Directory name | | `namespace` | C# namespace | `name` | | `bundleId` | Bundle identifier | `com.example.app` | | `platforms` | Target platforms | `ios,android` | | `nav` | Navigation: `shell`, `plain` | `shell` | ### Unity Game | Variable | Description | Default | | ------------- | --------------------- | -------------- | | `name` | Project name | Directory name | | `mode` | Game mode: `2d`, `3d` | `2d` | | `platforms` | Target platforms | `ios,android` | | `multiplayer` | Enable multiplayer | `false` | | `analytics` | Enable analytics | `true` | ## Template Structure ### Common Backend Structure All templates include a similar backend: ``` backend/ ├── cmd/server/ │ └── main.go # Entry point ├── app/server/ │ ├── app.go # App setup with mobile middleware │ ├── config.go # Configuration │ ├── routes.go # API routes │ └── handlers/ # Request handlers ├── go.mod └── Makefile ``` ### Makefile Commands All templates include: ```bash theme={null} make dev # Start development make build # Build for production make clean # Clean artifacts make test # Run tests ``` ## Next Steps Create your first mobile app Framework-specific adapters # Troubleshooting Source: https://docs.go-mizu.dev/mobile/troubleshooting Debug common mobile backend issues. Common issues and solutions when developing mobile backends with Mizu. ## Device Detection Issues ### Device is nil **Problem:** `DeviceFromCtx(c)` returns `nil`. **Cause:** Mobile middleware not applied. **Solution:** ```go theme={null} app.Use(mobile.New()) // Must be added before routes ``` ### Wrong Platform Detected **Problem:** Platform is `unknown` or incorrect. **Cause:** Missing headers or User-Agent not recognized. **Solution:** 1. Ensure client sends `X-Platform` header 2. Check User-Agent format 3. Enable User-Agent parsing: `SkipUserAgent: false` ```go theme={null} app.Use(mobile.WithOptions(mobile.Options{ SkipUserAgent: false, // Parse User-Agent })) ``` ## Version Middleware Issues ### Version Always Default **Problem:** `VersionFromCtx(c)` always returns default version. **Cause:** Client not sending version header. **Solution:** ```go theme={null} // Check header name matches app.Use(mobile.VersionMiddleware(mobile.VersionOptions{ Header: "X-API-Version", // Must match client })) ``` Client should send: ``` X-API-Version: v2 ``` ### Unsupported Version Error **Problem:** 400 error for valid version. **Cause:** Version not in `Supported` list. **Solution:** ```go theme={null} app.Use(mobile.VersionMiddleware(mobile.VersionOptions{ Supported: []mobile.Version{ {3, 0}, {2, 0}, // Add missing version }, })) ``` ## Push Notification Issues ### Invalid Token Format **Problem:** Token validation fails. **Cause:** Token format doesn't match provider. **Solution:** ```go theme={null} // Check token format if device.Platform == mobile.PlatformIOS { // APNS: 64 hex characters if !mobile.ValidateAPNS(token.Token) { // Invalid APNS token } } else { // FCM: 140-200 alphanumeric characters if !mobile.ValidateFCM(token.Token) { // Invalid FCM token } } ``` ### Provider Not Detected **Problem:** `PushProvider` is empty. **Cause:** Platform not set or unknown. **Solution:** ```go theme={null} // Explicitly set provider registration := mobile.PushRegistration{ Token: token, Provider: mobile.PushFCM, // Explicit provider } ``` ## Deep Link Issues ### AASA Not Served **Problem:** `/.well-known/apple-app-site-association` returns 404. **Cause:** Middleware not configured or path conflict. **Solution:** ```go theme={null} // Ensure middleware is added app.Use(mobile.UniversalLinkMiddleware(mobile.UniversalLinkConfig{ Apple: []mobile.AppleAppConfig{ {TeamID: "...", BundleID: "..."}, }, })) ``` ### App Not Opening from Link **Problem:** Links open in browser instead of app. **Causes:** 1. AASA not accessible over HTTPS 2. Invalid AASA format 3. App not installed **Solution:** 1. Verify AASA is served: `curl https://yourdomain.com/.well-known/apple-app-site-association` 2. Validate format: [Apple Validation Tool](https://search.developer.apple.com/appsearch-validation-tool/) 3. Check app entitlements ## Sync Issues ### Token Always Empty **Problem:** `req.Token` is always empty. **Cause:** Token not sent in correct location. **Solution:** ```go theme={null} // Check both header and query param req := mobile.ParseSyncRequest(c) // Looks for: // - Header: X-Sync-Token // - Query: ?sync_token=xxx ``` Client should send: ``` X-Sync-Token: YWJjMTIz... ``` Or: ``` GET /api/sync?sync_token=YWJjMTIz... ``` ### Full Sync Every Time **Problem:** Always getting full sync instead of delta. **Cause:** Token not stored or sent incorrectly. **Solution:** 1. Store token after each sync 2. Send token on next request 3. Check `full_sync` query param not set ## Error Response Issues ### Error Format Wrong **Problem:** Error response not matching expected format. **Cause:** Not using `mobile.SendError`. **Solution:** ```go theme={null} // Use SendError for consistent format return mobile.SendError(c, 400, mobile.NewError( mobile.ErrValidation, "Invalid request", )) ``` ### Trace ID Missing **Problem:** Error responses don't include trace ID. **Solution:** ```go theme={null} err := mobile.NewError(mobile.ErrInternal, "Error"). WithTraceID(c.Request().Header.Get("X-Request-ID")) return mobile.SendError(c, 500, err) ``` ## Debug Logging Add logging middleware to debug issues: ```go theme={null} app.Use(func(next mizu.Handler) mizu.Handler { return func(c *mizu.Ctx) error { device := mobile.DeviceFromCtx(c) log.Debug("Request", "path", c.Path(), "platform", device.Platform, "app_version", device.AppVersion, "device_id", device.DeviceID, "api_version", c.Request().Header.Get("X-API-Version"), ) return next(c) } }) ``` ## Testing Endpoints ### Test with curl ```bash theme={null} # Test device detection curl -H "X-Device-ID: test-123" \ -H "X-App-Version: 2.0.0" \ -H "X-Platform: ios" \ http://localhost:3000/api/test # Test versioning curl -H "X-API-Version: v2" \ http://localhost:3000/api/users # Test sync curl -H "X-Sync-Token: abc123" \ http://localhost:3000/api/sync ``` ### Simulate Mobile Request ```bash theme={null} # iOS User-Agent curl -A "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15" \ http://localhost:3000/api/test # Android User-Agent curl -A "Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36" \ http://localhost:3000/api/test ``` ## Next Steps Header reference Complete API documentation # API Versioning Source: https://docs.go-mizu.dev/mobile/versioning Manage API versions with semantic versioning and deprecation warnings. The mobile package provides robust API versioning middleware that supports version detection from headers, query parameters, and URL paths, with built-in deprecation warnings and graceful migration paths. ## Quick Start ```go theme={null} import "github.com/go-mizu/mizu/mobile" app := mizu.New() // Add version middleware app.Use(mobile.VersionMiddleware(mobile.VersionOptions{ Supported: []mobile.Version{{2, 0}, {1, 0}}, Deprecated: []mobile.Version{{1, 0}}, Default: mobile.Version{Major: 2}, })) app.Get("/api/users", func(c *mizu.Ctx) error { version := mobile.VersionFromCtx(c) if version.AtLeast(2, 0) { return c.JSON(200, v2Response) } return c.JSON(200, v1Response) }) ``` ## The Version Type ```go theme={null} type Version struct { Major int Minor int } ``` ### Creating Versions ```go theme={null} // Major version only v1 := mobile.Version{Major: 1} // v1 v2 := mobile.Version{Major: 2} // v2 // Major.Minor versions v1_5 := mobile.Version{Major: 1, Minor: 5} // v1.5 v2_1 := mobile.Version{Major: 2, Minor: 1} // v2.1 ``` ### Version Methods ```go theme={null} version := mobile.Version{Major: 2, Minor: 1} // String representation version.String() // "v2.1" // Check if zero/unset version.IsZero() // false // Compare versions version.Compare(mobile.Version{Major: 2, Minor: 0}) // 1 (greater) version.Compare(mobile.Version{Major: 2, Minor: 1}) // 0 (equal) version.Compare(mobile.Version{Major: 3, Minor: 0}) // -1 (less) // Convenience comparisons version.AtLeast(2, 0) // true version.AtLeast(2, 1) // true version.AtLeast(2, 2) // false version.Before(3, 0) // true version.Before(2, 1) // false ``` ### Parsing Versions ```go theme={null} // Parse from string v, err := mobile.ParseVersion("v2.1") // Version{2, 1} v, err := mobile.ParseVersion("v2") // Version{2, 0} v, err := mobile.ParseVersion("2.1") // Version{2, 1} v, err := mobile.ParseVersion("2") // Version{2, 0} ``` ## Version Middleware ### Configuration Options ```go theme={null} type VersionOptions struct { // Header is the version header name // Default: "X-API-Version" Header string // QueryParam is an alternative query parameter for version // Default: "" (disabled) QueryParam string // PathPrefix enables extraction from URL path prefix (e.g., /v1/...) // Default: false PathPrefix bool // Default is the default version when none specified // Default: Version{Major: 1} Default Version // Supported lists all supported versions // Empty means no validation Supported []Version // Deprecated lists deprecated versions (still work but warn) Deprecated []Version // OnUnsupported handles unsupported version requests OnUnsupported func(c *mizu.Ctx, v Version) error // EchoVersion includes X-API-Version in response // Default: true EchoVersion bool } ``` ### Basic Setup ```go theme={null} app.Use(mobile.VersionMiddleware(mobile.VersionOptions{ Supported: []mobile.Version{{3, 0}, {2, 0}, {1, 0}}, Deprecated: []mobile.Version{{1, 0}}, Default: mobile.Version{Major: 3}, })) ``` ### Version Detection Sources The middleware checks version from multiple sources: **1. Header (default)** ```bash theme={null} curl -H "X-API-Version: v2" http://localhost:3000/api/users ``` **2. Query Parameter** ```go theme={null} app.Use(mobile.VersionMiddleware(mobile.VersionOptions{ QueryParam: "version", })) ``` ```bash theme={null} curl "http://localhost:3000/api/users?version=v2" ``` **3. URL Path Prefix** ```go theme={null} app.Use(mobile.VersionMiddleware(mobile.VersionOptions{ PathPrefix: true, })) ``` ```bash theme={null} curl http://localhost:3000/v2/api/users ``` **Priority order:** Header > Query > Path > Default ### Supported Versions ```go theme={null} app.Use(mobile.VersionMiddleware(mobile.VersionOptions{ Supported: []mobile.Version{ {3, 0}, // v3 - latest {2, 0}, // v2 - still supported {1, 0}, // v1 - deprecated but works }, })) ``` Unsupported versions return 400 Bad Request: ```json theme={null} { "code": "invalid_request", "message": "Unsupported API version: v4", "details": { "requested": "v4", "supported": ["v3", "v2", "v1"] } } ``` ### Deprecated Versions ```go theme={null} app.Use(mobile.VersionMiddleware(mobile.VersionOptions{ Supported: []mobile.Version{{3, 0}, {2, 0}, {1, 0}}, Deprecated: []mobile.Version{{1, 0}}, })) ``` Deprecated versions work but add response header: ``` X-API-Deprecated: true ``` ### Custom Unsupported Handler ```go theme={null} app.Use(mobile.VersionMiddleware(mobile.VersionOptions{ Supported: []mobile.Version{{2, 0}}, OnUnsupported: func(c *mizu.Ctx, v mobile.Version) error { return c.JSON(400, map[string]any{ "error": "Version not supported", "requested": v.String(), "available": []string{"v2"}, "docs": "https://api.example.com/docs/migration", }) }, })) ``` ## Using Version Context ### Access in Handlers ```go theme={null} func handler(c *mizu.Ctx) error { version := mobile.VersionFromCtx(c) // Returns zero Version if middleware not applied if version.IsZero() { version = mobile.Version{Major: 1} // Default } return c.JSON(200, map[string]any{ "api_version": version.String(), }) } ``` ### Version-Aware Logic ```go theme={null} func getUsers(c *mizu.Ctx) error { version := mobile.VersionFromCtx(c) users := fetchUsers() // v3: New response format if version.AtLeast(3, 0) { return c.JSON(200, V3Response{ Data: users, Meta: getMeta(), Links: getLinks(), }) } // v2: Added pagination if version.AtLeast(2, 0) { return c.JSON(200, V2Response{ Data: users, Total: len(users), Page: 1, PerPage: 20, }) } // v1: Simple array return c.JSON(200, users) } ``` ### Switch-Based Routing ```go theme={null} func handler(c *mizu.Ctx) error { version := mobile.VersionFromCtx(c) switch { case version.AtLeast(3, 0): return handleV3(c) case version.AtLeast(2, 0): return handleV2(c) default: return handleV1(c) } } ``` ### Field-Level Versioning ```go theme={null} type User struct { ID int `json:"id"` Name string `json:"name"` Email string `json:"email"` Username string `json:"username,omitempty"` // v2+ Avatar string `json:"avatar,omitempty"` // v3+ } func getUser(c *mizu.Ctx) error { version := mobile.VersionFromCtx(c) user := fetchUser(c.Param("id")) // Create versioned response response := User{ ID: user.ID, Name: user.Name, Email: user.Email, } if version.AtLeast(2, 0) { response.Username = user.Username } if version.AtLeast(3, 0) { response.Avatar = user.Avatar } return c.JSON(200, response) } ``` ## Migration Patterns ### Adding New Fields (Non-Breaking) ```go theme={null} // v1 response type UserV1 struct { ID int `json:"id"` Name string `json:"name"` } // v2 adds email (backward compatible) type UserV2 struct { ID int `json:"id"` Name string `json:"name"` Email string `json:"email"` // New field } func getUser(c *mizu.Ctx) error { v := mobile.VersionFromCtx(c) user := fetchUser() if v.AtLeast(2, 0) { return c.JSON(200, UserV2{ ID: user.ID, Name: user.Name, Email: user.Email, }) } return c.JSON(200, UserV1{ ID: user.ID, Name: user.Name, }) } ``` ### Renaming Fields (Breaking) ```go theme={null} // v1: uses "fullname" type UserV1 struct { ID int `json:"id"` Fullname string `json:"fullname"` } // v2: renamed to "name" type UserV2 struct { ID int `json:"id"` Name string `json:"name"` } func getUser(c *mizu.Ctx) error { v := mobile.VersionFromCtx(c) user := fetchUser() if v.AtLeast(2, 0) { return c.JSON(200, UserV2{ID: user.ID, Name: user.Name}) } // v1: Use old field name return c.JSON(200, UserV1{ID: user.ID, Fullname: user.Name}) } ``` ### Changing Data Types (Breaking) ```go theme={null} // v1: role is a string type UserV1 struct { ID int `json:"id"` Role string `json:"role"` // "admin", "user" } // v2: role is an object type UserV2 struct { ID int `json:"id"` Role RoleInfo `json:"role"` } type RoleInfo struct { ID int `json:"id"` Name string `json:"name"` Permissions []string `json:"permissions"` } func getUser(c *mizu.Ctx) error { v := mobile.VersionFromCtx(c) user := fetchUser() if v.AtLeast(2, 0) { return c.JSON(200, UserV2{ ID: user.ID, Role: getRoleInfo(user.RoleID), }) } return c.JSON(200, UserV1{ ID: user.ID, Role: user.RoleName, }) } ``` ### Deprecation Workflow ```go theme={null} // Step 1: Mark as deprecated app.Use(mobile.VersionMiddleware(mobile.VersionOptions{ Supported: []mobile.Version{{3, 0}, {2, 0}, {1, 0}}, Deprecated: []mobile.Version{{1, 0}}, })) // Step 2: Log usage of deprecated versions func deprecationLogger() mizu.Middleware { return func(next mizu.Handler) mizu.Handler { return func(c *mizu.Ctx) error { v := mobile.VersionFromCtx(c) if v.Before(2, 0) { log.Warn("Deprecated API version used", "version", v.String(), "path", c.Request().URL.Path, "device", mobile.DeviceFromCtx(c).DeviceID, ) } return next(c) } } } // Step 3: Remove support (after migration period) app.Use(mobile.VersionMiddleware(mobile.VersionOptions{ Supported: []mobile.Version{{3, 0}, {2, 0}}, // v1 removed })) ``` ## Client Implementation ### iOS (Swift) ```swift theme={null} class APIClient { let apiVersion = "v2" func request(_ endpoint: String) -> URLRequest { var request = URLRequest(url: URL(string: baseURL + endpoint)!) request.setValue(apiVersion, forHTTPHeaderField: "X-API-Version") return request } func handleResponse(_ response: HTTPURLResponse) { if response.value(forHTTPHeaderField: "X-API-Deprecated") == "true" { // Warn user about deprecated API NotificationCenter.default.post( name: .apiDeprecated, object: nil ) } } } ``` ### Android (Kotlin) ```kotlin theme={null} class VersionInterceptor(private val apiVersion: String = "v2") : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request().newBuilder() .header("X-API-Version", apiVersion) .build() val response = chain.proceed(request) if (response.header("X-API-Deprecated") == "true") { // Handle deprecation warning EventBus.post(ApiDeprecatedEvent()) } return response } } ``` ### Flutter (Dart) ```dart theme={null} class ApiClient { static const apiVersion = 'v2'; Future request(String endpoint) async { final response = await http.get( Uri.parse('$baseUrl$endpoint'), headers: {'X-API-Version': apiVersion}, ); if (response.headers['x-api-deprecated'] == 'true') { // Show deprecation warning showDeprecationWarning(); } return response; } } ``` ## Best Practices ### Version Naming * Use semantic versioning: `v1`, `v1.1`, `v2` * Major versions for breaking changes * Minor versions for backward-compatible additions ### Deprecation Timeline 1. **Announce**: Notify developers of upcoming deprecation 2. **Warn**: Add to deprecated list (X-API-Deprecated header) 3. **Monitor**: Track usage of deprecated versions 4. **Remove**: Remove from supported list ### Documentation * Document all supported versions * Maintain migration guides * Provide changelogs per version ## Next Steps Return consistent error responses Page and cursor-based pagination Delta synchronization Complete API documentation # Features Source: https://docs.go-mizu.dev/overview/features What makes Mizu enjoyable to build with. Mizu provides a focused set of features for building web applications in Go. Each feature is designed to feel familiar if you already know Go's standard library. ## Routing Routing connects URLs to handler functions. Mizu uses Go 1.22's enhanced `ServeMux` which supports path parameters natively. ```go theme={null} app.Get("/users/{id}", getUser) // GET /users/123 app.Post("/users", createUser) // POST /users app.Delete("/users/{id}", deleteUser) // DELETE /users/123 ``` **Key features:** * **Path parameters** using `{param}` syntax (Go 1.22 `ServeMux` patterns) * **HTTP method helpers**: `Get`, `Post`, `Put`, `Patch`, `Delete`, `Head`, `Connect`, `Trace` * **Route groups** with `Group()` and `Prefix()` for organizing related endpoints * **Scoped middleware** with `With()` to apply middleware to specific routes * **Mount http.Handler** with `Mount()` for interoperability with standard handlers ```go theme={null} // Group routes under a prefix app.Group("/api", func(r *mizu.Router) { r.Get("/users", listUsers) // GET /api/users r.Get("/users/{id}", getUser) // GET /api/users/123 }) ``` ## Request handling Reading data from requests is explicit and straightforward. Each method does one thing clearly. | Method | Purpose | Example | | ---------------------- | ----------------------- | ------------------------------- | | `c.Param("id")` | Path parameter from URL | `/users/{id}` → `c.Param("id")` | | `c.Query("page")` | Single query parameter | `?page=2` → `c.Query("page")` | | `c.QueryValues()` | All query parameters | Returns `url.Values` | | `c.Form()` | Form data (POST) | Returns `url.Values, error` | | `c.MultipartForm(max)` | File uploads | Returns `form, cleanup, error` | | `c.BindJSON(&v, max)` | JSON body to struct | Validates and parses | | `c.Cookie("name")` | Read a cookie | Returns `*http.Cookie, error` | ```go theme={null} func getUser(c *mizu.Ctx) error { // Path parameter from /users/{id} id := c.Param("id") // Query parameter from ?include=email include := c.Query("include") return c.JSON(200, map[string]string{"id": id, "include": include}) } ``` ## Response helpers Response methods write data with appropriate headers and status codes. | Method | Content-Type | Usage | | ------------------------------ | ------------------- | --------------------- | | `c.Text(code, s)` | `text/plain` | Plain text responses | | `c.JSON(code, v)` | `application/json` | Structs, maps, slices | | `c.HTML(code, s)` | `text/html` | HTML strings | | `c.File(code, path)` | Auto-detected | Serve files from disk | | `c.Download(code, path, name)` | With disposition | Force download | | `c.Stream(fn)` | Custom | Streaming responses | | `c.SSE(ch)` | `text/event-stream` | Server-Sent Events | | `c.Redirect(code, url)` | - | HTTP redirects | | `c.NoContent()` | - | 204 No Content | ```go theme={null} // Return JSON return c.JSON(200, user) // Serve a file with content-type auto-detection return c.File(200, "./public/image.png") // Force browser to download with a specific filename return c.Download(200, "./reports/data.csv", "report-2024.csv") ``` ## Middleware Middleware wraps handlers to add behavior before or after request processing. The pattern is simple: take a handler, return a handler. ```go theme={null} type Middleware func(Handler) Handler ``` **Applying middleware:** ```go theme={null} // Global middleware - runs for all routes app.Use(myMiddleware) // Scoped middleware - only for routes on this router admin := app.With(authRequired) admin.Get("/settings", adminSettings) // Standard net/http middleware works too app.Compat.Use(existingMiddleware) ``` **Writing middleware:** ```go theme={null} func timing() mizu.Middleware { return func(next mizu.Handler) mizu.Handler { return func(c *mizu.Ctx) error { start := time.Now() err := next(c) c.Logger().Info("request", "duration", time.Since(start)) return err } } } ``` ## Logging Mizu includes a request logger that uses Go's `slog` package for structured logging. ```go theme={null} // Built-in logger is enabled by default app := mizu.New() // Includes Logger middleware // Access the logger in any handler c.Logger().Info("processing", "user_id", userID) // Configure logging behavior app.Use(mizu.Logger(mizu.LoggerOptions{ Mode: mizu.Dev, // Dev (text) or Prod (JSON) Color: true, // Colored output for terminals UserAgent: true, // Include User-Agent in logs })) ``` **Log modes:** * `mizu.Auto` - Colored text for terminals, JSON for non-terminals (default) * `mizu.Dev` - Human-readable text format * `mizu.Prod` - JSON lines for log collectors ## Error handling Handlers return errors directly. Mizu catches returned errors and recovered panics in one central place. ```go theme={null} // Handlers can return errors func getUser(c *mizu.Ctx) error { user, err := findUser(c.Param("id")) if err != nil { return err // Caught by error handler } return c.JSON(200, user) } // Set a global error handler app.ErrorHandler(func(c *mizu.Ctx, err error) { // Check for panics if perr, ok := err.(*mizu.PanicError); ok { c.Logger().Error("panic", "value", perr.Value) } c.JSON(500, map[string]string{"error": "internal error"}) }) ``` ## Server lifecycle Mizu handles graceful shutdown automatically. When your server receives SIGINT (Ctrl+C) or SIGTERM, it: 1. Stops accepting new connections 2. Waits for active requests to complete (up to timeout) 3. Shuts down cleanly ```go theme={null} app := mizu.New() // Configure shutdown timeout (default: 15 seconds) app.ShutdownTimeout = 30 * time.Second // Start methods app.Listen(":3000") // HTTP app.ListenTLS(":443", "cert.pem", "key.pem") // HTTPS app.Serve(listener) // Custom listener ``` **Health check endpoints** for load balancers and Kubernetes: ```go theme={null} // Liveness probe - always returns 200 http.Handle("/livez", app.LivezHandler()) // Readiness probe - returns 503 during shutdown http.Handle("/readyz", app.ReadyzHandler()) ``` ## Static files Serve files from disk or embedded filesystems. ```go theme={null} // Serve ./public folder at /assets app.Static("/assets/", http.Dir("public")) // Access: /assets/logo.png → ./public/logo.png // Serve embedded files (Go 1.16+) //go:embed public/* var publicFS embed.FS sub, _ := fs.Sub(publicFS, "public") app.Static("/assets/", http.FS(sub)) ``` ## Summary Mizu stays small and predictable. Everything is plain Go code with no hidden behavior. | Category | Key point | | -------------- | ------------------------------------------------------ | | **Routing** | Go 1.22 ServeMux patterns with `{param}` syntax | | **Request** | Explicit methods: `Param`, `Query`, `Form`, `BindJSON` | | **Response** | Type helpers: `Text`, `JSON`, `HTML`, `File`, `SSE` | | **Middleware** | Simple `func(Handler) Handler` signature | | **Logging** | Built-in with `slog`, configurable modes | | **Errors** | Return from handlers, catch in one place | | **Lifecycle** | Graceful shutdown with configurable timeout | # Roadmap Source: https://docs.go-mizu.dev/overview/roadmap The planned milestones for Mizu development. Mizu development follows a principle of incremental improvement. Each release adds focused functionality while keeping the core simple and stable. ### Current - Core Framework The foundation is in place: * Routing with Go 1.22 `ServeMux` patterns * Context wrapper with request/response helpers * Middleware system compatible with `net/http` * Structured logging with `slog` * Graceful shutdown with configurable timeouts * Server-Sent Events (SSE) and streaming responses * Static file serving * Error handling with panic recovery ### Planned Future development will focus on ecosystem packages that integrate with the core: **Templates and Rendering** * HTML templates with layouts and partials * Integration with Go's `html/template` **Testing Utilities** * Test helpers for handlers and middleware * Request/response mocking **Documentation** * More examples covering common patterns * Guides for deployment and production use ### Philosophy Mizu stays small by design. Features that require significant complexity will be separate packages rather than additions to the core. This keeps the framework predictable and easy to understand. If you need functionality beyond what Mizu provides, you can use any Go library that works with `net/http`. Mizu does not lock you into a specific ecosystem. # Use Cases Source: https://docs.go-mizu.dev/overview/use-cases What you can build with Mizu. Mizu is a good fit when you want a lightweight HTTP layer without framework lock-in. Here are common use cases with practical examples. ## REST APIs Build JSON APIs with typed handlers and central error handling. This is Mizu's sweet spot. ```go theme={null} package main import ( "github.com/go-mizu/mizu" ) type User struct { ID string `json:"id"` Name string `json:"name"` Email string `json:"email"` } func main() { app := mizu.New() // List all users app.Get("/users", func(c *mizu.Ctx) error { users := []User{{ID: "1", Name: "Alice"}} return c.JSON(200, users) }) // Get a single user app.Get("/users/{id}", func(c *mizu.Ctx) error { id := c.Param("id") user := User{ID: id, Name: "Alice"} return c.JSON(200, user) }) // Create a user app.Post("/users", func(c *mizu.Ctx) error { var input struct { Name string `json:"name"` Email string `json:"email"` } if err := c.BindJSON(&input, 1<<20); err != nil { return c.JSON(400, map[string]string{"error": err.Error()}) } user := User{ID: "123", Name: input.Name, Email: input.Email} return c.JSON(201, user) }) // Central error handling app.ErrorHandler(func(c *mizu.Ctx, err error) { c.Logger().Error("api error", "error", err) c.JSON(500, map[string]string{"error": "internal server error"}) }) app.Listen(":3000") } ``` ## Backend for SPAs and Mobile Serve JSON data for React, Vue, or mobile applications. Add CORS headers for cross-origin requests. ```go theme={null} func main() { app := mizu.New() // Simple CORS middleware app.Use(func(next mizu.Handler) mizu.Handler { return func(c *mizu.Ctx) error { c.Header().Set("Access-Control-Allow-Origin", "*") c.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE") c.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") // Handle preflight requests if c.Request().Method == "OPTIONS" { return c.NoContent() } return next(c) } }) // Your API endpoints app.Get("/api/data", func(c *mizu.Ctx) error { return c.JSON(200, map[string]any{ "items": []string{"a", "b", "c"}, }) }) app.Listen(":3000") } ``` ## Microservices Create small, focused services. Mizu's minimal footprint means fast startup times and low memory usage. ```go theme={null} func main() { app := mizu.New() // Health endpoints for container orchestrators (Kubernetes, ECS, etc.) http.Handle("/livez", app.LivezHandler()) // Liveness: is the process alive? http.Handle("/readyz", app.ReadyzHandler()) // Readiness: can it handle traffic? // Your service's API app.Get("/process", func(c *mizu.Ctx) error { input := c.Query("data") result := processData(input) return c.JSON(200, result) }) // Fast startup - suitable for serverless or scaled deployments app.Listen(":3000") } func processData(input string) map[string]string { return map[string]string{"processed": input} } ``` ## Prototypes and Internal Tools Go from idea to running server quickly. A complete Mizu app fits in a single file. ```go theme={null} package main import "github.com/go-mizu/mizu" func main() { app := mizu.New() app.Get("/", func(c *mizu.Ctx) error { return c.HTML(200, `

    Internal Dashboard

    `) }) app.Get("/status", func(c *mizu.Ctx) error { return c.JSON(200, map[string]string{ "database": "connected", "cache": "ready", }) }) app.Listen(":3000") } ``` ## Server-Side Rendering Serve HTML pages with Go's `html/template` package. Mizu handles routing and static files while you use standard library templates. ```go theme={null} package main import ( "html/template" "github.com/go-mizu/mizu" ) var tmpl = template.Must(template.New("page").Parse(` {{.Title}}

    {{.Title}}

    Hello, {{.Name}}!

    `)) func main() { app := mizu.New() app.Get("/", func(c *mizu.Ctx) error { name := c.Query("name") if name == "" { name = "World" } c.Header().Set("Content-Type", "text/html; charset=utf-8") return tmpl.Execute(c.Writer(), map[string]string{ "Title": "Welcome", "Name": name, }) }) // Serve static assets app.Static("/static/", http.Dir("./public")) app.Listen(":3000") } ``` ## Real-Time Updates with SSE Stream data to clients using Server-Sent Events. Useful for live dashboards, notifications, and progress updates. ```go theme={null} func main() { app := mizu.New() app.Get("/", func(c *mizu.Ctx) error { return c.HTML(200, `

    Live Updates

    `) }) app.Get("/events", func(c *mizu.Ctx) error { ch := make(chan any) // Send updates every second go func() { defer close(ch) for i := 1; i <= 10; i++ { ch <- map[string]any{ "count": i, "message": "Update " + fmt.Sprint(i), } time.Sleep(time.Second) } }() return c.SSE(ch) }) app.Listen(":3000") } ``` ## When Mizu is not the right choice | Need | Why Mizu may not fit | Alternative | | --------------------------------- | -------------------- | ----------------------------------------- | | **WebSocket support** | Not built in | Add gorilla/websocket or nhooyr/websocket | | **Full-stack framework** | No ORM, auth, admin | Consider Buffalo, Beego, or Gin | | **Convention over configuration** | Mizu is explicit | Consider Rails-like frameworks | | **Automatic binding** | No struct tag magic | Consider Echo or Fiber | Mizu stays focused on HTTP routing and request handling. For everything else, pick the Go library you prefer. # Why Mizu Source: https://docs.go-mizu.dev/overview/why The idea and purpose behind the Mizu framework. Mizu exists because web frameworks often add complexity that Go developers do not need. Go already has excellent HTTP primitives. Mizu builds on them with a thin layer that improves ergonomics without hiding what is happening underneath. ## The problem with most frameworks Many web frameworks introduce their own way of doing things: | Framework pattern | Problem for Go developers | | ------------------------------- | ----------------------------------- | | Magic routing annotations | Hard to trace which handler runs | | Dependency injection containers | Hidden object creation | | Custom context types | Must learn framework-specific APIs | | Code generation | Extra build steps, harder debugging | When something goes wrong, you spend time understanding the framework instead of fixing your code. ## Mizu's approach Mizu takes a different path: **enhance Go's standard library, don't replace it**. ### Stay close to the standard library Mizu uses Go 1.22's enhanced `ServeMux` for routing—the same router from the standard library, just with helper methods added. Your handlers receive a context that wraps `http.ResponseWriter` and `*http.Request`. You can access the underlying types anytime: ```go theme={null} func handler(c *mizu.Ctx) error { w := c.Writer() // http.ResponseWriter - the same type you know r := c.Request() // *http.Request - unchanged from net/http return c.Text(200, "ok") } ``` **Why this matters:** * Any `http.Handler` can be mounted directly * Any middleware that works with `net/http` works with Mizu * Stack traces show familiar Go types * Your existing knowledge transfers directly ### Return errors from handlers Standard Go handlers have this signature: ```go theme={null} func(w http.ResponseWriter, r *http.Request) ``` No return value means errors must be handled inline, leading to repeated patterns: ```go theme={null} // Standard library approach - repetitive error handling func createUser(w http.ResponseWriter, r *http.Request) { var input UserInput if err := json.NewDecoder(r.Body).Decode(&input); err != nil { http.Error(w, err.Error(), 400) return } if input.Name == "" { http.Error(w, "name required", 400) return } // finally, the actual logic } ``` Mizu handlers return an error: ```go theme={null} // Mizu approach - errors flow up func createUser(c *mizu.Ctx) error { var input UserInput if err := c.BindJSON(&input, 1<<20); err != nil { return err // handled centrally } if input.Name == "" { return fmt.Errorf("name required") } // focused on business logic return c.JSON(201, user) } ``` One central error handler catches all returned errors and recovered panics. Your route handlers stay focused on business logic. ### No reflection or magic Mizu does not scan struct tags to bind parameters. It does not generate code. It does not use dependency injection. You read data with explicit method calls and write responses with explicit helpers. ```go theme={null} id := c.Param("id") // path parameter - you control the key q := c.Query("search") // query string - explicit lookup form, _ := c.Form() // form data - standard url.Values name := form.Get("name") // exactly what you expect ``` **Why this matters:** * Debugging is straightforward—step through and see what happens * No surprises from automatic binding or conversion * Code is self-documenting—what you see is what runs * IDE navigation works perfectly ### Production defaults Mizu includes what you need for production without extra setup: | Feature | What it does | Why it matters | | ---------------------- | ------------------------------------------- | ----------------------------------------------- | | **Graceful shutdown** | Drains active connections on SIGINT/SIGTERM | Zero dropped requests during deploys | | **Structured logging** | Uses Go's `slog` package | JSON logs for production, readable logs for dev | | **Panic recovery** | Catches panics, logs stack traces | Server stays running after bugs | | **Health endpoints** | `LivezHandler()`, `ReadyzHandler()` | Works with Kubernetes and load balancers | You can configure shutdown timing and swap out the logger, but sensible defaults work out of the box. ### Small surface area The entire framework fits in a few files: * `app.go` - Server lifecycle and configuration * `router.go` - Routing and middleware * `context.go` - Request/response helpers * `logger.go` - Request logging middleware There are no plugins, no lifecycle hooks beyond middleware, and no framework-specific conventions to learn. If you understand Go's `net/http`, you understand Mizu. ## When to use Mizu **Mizu is a good fit when you want:** * A lightweight wrapper over Go's HTTP server * Explicit code without hidden behavior * Easy integration with existing `net/http` code * Production-ready defaults without external dependencies * To keep using Go idioms, not learn framework idioms **Mizu is NOT for you if:** * You want an opinionated full-stack framework with ORM, auth, and admin panels built in * You prefer convention over configuration * You want automatic parameter binding from struct tags * You need built-in WebSocket support (you'll add a library like gorilla/websocket) Mizu stays small so you can compose it with the libraries you prefer. Need authentication? Pick any Go auth library. Need a database? Use `database/sql` or any ORM. Mizu handles HTTP; you choose the rest. # Engine Source: https://docs.go-mizu.dev/view/engine Configure the view engine for development and production The view engine is the core of Mizu's template system. This guide covers all configuration options and how to use them effectively. ## Creating an Engine Create a view engine with `view.New()`: ```go theme={null} import "github.com/go-mizu/mizu/view" engine := view.New(view.Config{ Dir: "./views", Extension: ".html", DefaultLayout: "default", Development: true, }) ``` ## Configuration Options ### Dir The directory where your templates are stored. ```go theme={null} view.Config{ Dir: "./views", // Default: "views" } ``` The engine expects this structure inside the directory: ``` views/ ├── layouts/ # Layout templates │ └── default.html └── pages/ # Page templates ├── home.html └── about.html ``` ### FS An `fs.FS` filesystem to load templates from. Use this with `embed.FS` in production to bundle templates into your binary. ```go theme={null} import "embed" //go:embed views var viewsFS embed.FS engine := view.New(view.Config{ FS: viewsFS, // Use embedded filesystem }) ``` When `FS` is set, templates are loaded from the embedded filesystem instead of the local filesystem. The directory structure inside the embedded FS should match what you'd have on disk. ### Extension The file extension for template files. ```go theme={null} view.Config{ Extension: ".html", // Default: ".html" } ``` You can use other extensions: ```go theme={null} view.Config{ Extension: ".tmpl", // Now looks for .tmpl files } ``` ### DefaultLayout The layout template to use when rendering pages. This refers to a file in the `layouts/` directory. ```go theme={null} view.Config{ DefaultLayout: "default", // Uses layouts/default.html } ``` You can override this per-render with `view.Layout()` or disable it with `view.NoLayout()`. ### Delims Custom template delimiters. Useful if your templates contain `{{` and `}}` literally (common with Vue.js or Angular templates). ```go theme={null} view.Config{ Delims: [2]string{"<%", "%>"}, // Default: ["{{", "}}"] } ``` Now templates use `<% %>` instead: ```html theme={null}

    Hello, <%.Data.Name%>

    ``` ### Development Enables development mode features: * **Template reload** - Templates are re-read from disk on every request * **No caching** - Templates are never cached ```go theme={null} view.Config{ Development: true, // Default: false } ``` **Important:** Never enable Development mode in production. It significantly impacts performance. A common pattern is to use an environment variable: ```go theme={null} view.Config{ Development: os.Getenv("ENV") != "production", } ``` ### Funcs Custom template functions to add to all templates. ```go theme={null} import "html/template" import "strings" view.Config{ Funcs: template.FuncMap{ "uppercase": strings.ToUpper, "formatDate": func(t time.Time) string { return t.Format("Jan 2, 2006") }, }, } ``` Now you can use these in templates: ```html theme={null}

    {{uppercase .Data.Name}}

    Posted on {{formatDate .Data.CreatedAt}}

    ``` ## Built-in Functions The view engine provides these functions by default: | Function | Description | Example | | ----------- | ----------------- | ------------------------------------ | | `dict` | Create a map | `{{dict "key" "value"}}` | | `list` | Create a slice | `{{list 1 2 3}}` | | `upper` | Uppercase string | `{{upper .Data.Name}}` | | `lower` | Lowercase string | `{{lower .Data.Name}}` | | `trim` | Trim whitespace | `{{trim .Data.Text}}` | | `contains` | Check substring | `{{if contains .Data.Text "hello"}}` | | `replace` | Replace substring | `{{replace .Data.Text "old" "new"}}` | | `split` | Split string | `{{split .Data.Tags ","}}` | | `join` | Join slice | `{{join .Data.Items ", "}}` | | `hasPrefix` | Check prefix | `{{if hasPrefix .Data.URL "https"}}` | | `hasSuffix` | Check suffix | `{{if hasSuffix .Data.File ".go"}}` | ## Engine Methods ### New Creates a new view engine with the given configuration. ```go theme={null} engine := view.New(view.Config{ Dir: "./views", Development: true, }) ``` ### Load Loads and validates all templates at startup. Call this in production to fail fast if templates have errors. ```go theme={null} engine := view.New(config) if err := engine.Load(); err != nil { log.Fatal("template error:", err) } ``` This loads all files in `layouts/` and `pages/` directories. ### Clear Clears the template cache. Useful if you need to reload templates without restarting. ```go theme={null} engine.Clear() ``` ### Render Renders a page template. The first argument is an `io.Writer` (like `http.ResponseWriter`), followed by the template name, data, and optional render options. ```go theme={null} err := engine.Render(w, "home", view.Data{ "Title": "Home", "User": currentUser, }) ``` The template name refers to a file in `pages/` without the extension. ### Middleware Returns a Mizu middleware that adds the engine to every request context. ```go theme={null} app := mizu.New() app.Use(engine.Middleware()) ``` After adding this middleware, you can use `view.From(c)` and `view.Render(c, ...)` in handlers. ## Package Functions ### From Retrieves the engine from a request context. Use this in handlers after adding the middleware. ```go theme={null} func handler(c *mizu.Ctx) error { engine := view.From(c) if engine == nil { return errors.New("view engine not configured") } return engine.Render(c.Writer(), "page", data) } ``` ### Render (package-level) A convenience function that gets the engine from context and renders. This is the most common way to render templates. ```go theme={null} func handler(c *mizu.Ctx) error { return view.Render(c, "home", view.Data{ "Title": "Home", }) } ``` This is equivalent to: ```go theme={null} engine := view.From(c) c.Writer().Header().Set("Content-Type", "text/html; charset=utf-8") c.Writer().WriteHeader(200) return engine.Render(c.Writer(), "home", data) ``` ## Render Options When calling `Render()`, you can pass options to customize behavior. ### Layout Override the default layout for this render: ```go theme={null} view.Render(c, "dashboard", data, view.Layout("admin")) ``` This uses `layouts/admin.html` instead of the default layout. ### NoLayout Render without any layout (useful for partial responses): ```go theme={null} view.Render(c, "user-row", data, view.NoLayout()) ``` ## Template Data Structure When you render a template, your data is wrapped in a structure: ```go theme={null} type pageData struct { Page pageMeta // Template metadata Data any // Your data Content template.HTML // Rendered page (only in layouts) } type pageMeta struct { Name string // Template name (e.g., "home") Layout string // Layout name (e.g., "default") } ``` **In page templates:** ```html theme={null}

    {{.Data.Title}}

    Template: {{.Page.Name}}

    ``` **In layout templates:** ```html theme={null} {{.Data.Title}} {{.Content}} ``` ## Complete Example Here's a typical configuration for a production app: ```go theme={null} package main import ( "embed" "html/template" "os" "strings" "time" "github.com/go-mizu/mizu" "github.com/go-mizu/mizu/view" ) //go:embed views var viewsFS embed.FS func main() { isDev := os.Getenv("ENV") != "production" config := view.Config{ Extension: ".html", DefaultLayout: "default", Development: isDev, Funcs: template.FuncMap{ "formatDate": formatDate, "truncate": truncate, }, } if isDev { // Development: load from filesystem config.Dir = "./views" } else { // Production: load from embedded FS config.FS = viewsFS } engine := view.New(config) // Preload templates in production to fail fast if !isDev { if err := engine.Load(); err != nil { panic(err) } } app := mizu.New() app.Use(engine.Middleware()) app.Get("/", homeHandler) app.Get("/about", aboutHandler) app.Listen(":8080") } func formatDate(t time.Time) string { return t.Format("January 2, 2006") } func truncate(s string, length int) string { if len(s) <= length { return s } return s[:length] + "..." } func homeHandler(c *mizu.Ctx) error { return view.Render(c, "home", view.Data{ "Title": "Welcome", }) } func aboutHandler(c *mizu.Ctx) error { return view.Render(c, "about", view.Data{ "Title": "About Us", }) } ``` ## Error Handling ### Template Errors The engine returns an `Error` type that wraps template parsing and execution errors: ```go theme={null} err := engine.Render(w, "page", data) if err != nil { var viewErr *view.Error if errors.As(err, &viewErr) { log.Printf("Template error (%s %q): %v", viewErr.Kind, viewErr.Name, viewErr.Err) } } ``` The `Error` type has: * `Kind` - "page", "layout", or "template" * `Name` - Template name * `Err` - Underlying error (nil if not found) ### ErrNotFound Returned when a template file doesn't exist: ```go theme={null} if errors.Is(err, view.ErrNotFound) { // Template file not found } ``` ## Best Practices ### 1. Use Development Mode Locally Always enable development mode during development: ```go theme={null} Development: os.Getenv("ENV") != "production" ``` ### 2. Load Templates in Production Call `Load()` at startup to catch template errors early: ```go theme={null} if err := engine.Load(); err != nil { log.Fatal(err) } ``` ### 3. Embed Templates for Production Use `embed.FS` so templates are bundled in your binary: ```go theme={null} //go:embed views var viewsFS embed.FS ``` This eliminates the need to deploy template files alongside your binary. ### 4. Keep Custom Functions Pure Custom functions should be pure (no side effects): ```go theme={null} // Good: pure function "uppercase": strings.ToUpper, // Bad: has side effects "log": func(s string) string { log.Println(s) // Side effect! return s }, ``` ### 5. Use Consistent Data Structures Create struct types for complex template data: ```go theme={null} type PageData struct { Title string User *User Posts []Post } view.Render(c, "home", PageData{ Title: "Home", User: currentUser, Posts: posts, }) ``` Note: When using a struct instead of `view.Data` (which is `map[string]any`), access it directly in templates: ```html theme={null}

    {{.Data.Title}}

    {{.Data.Title}}

    ``` # Functions Source: https://docs.go-mizu.dev/view/functions Built-in helper functions and custom template functions The view engine provides built-in template functions that make your templates more expressive. This guide covers all available functions and how to add your own. ## Built-in Functions These functions are available in all templates. ### Data Structure Functions #### dict Creates a map from key-value pairs: ```html theme={null} {{$user := dict "Name" "Alice" "Age" 30 "Admin" true}}

    {{$user.Name}} is {{$user.Age}} years old

    ``` #### list Creates a slice from values: ```html theme={null} {{$fruits := list "apple" "banana" "cherry"}}
      {{range $fruits}}
    • {{.}}
    • {{end}}
    ``` ### String Functions #### upper Converts a string to uppercase: ```html theme={null} {{upper .Data.Status}} ``` #### lower Converts a string to lowercase: ```html theme={null}
    ``` #### trim Removes leading and trailing whitespace: ```html theme={null}

    {{trim .Data.Description}}

    ``` #### contains Checks if a string contains a substring: ```html theme={null} {{if contains .Data.Email "@company.com"}} Internal User {{end}} ``` #### replace Replaces occurrences of a substring: ```html theme={null} {{replace .Data.Content "\n" "
    " -1}} ``` The third argument is the maximum number of replacements (-1 means all). #### split Splits a string into a slice: ```html theme={null} {{$parts := split .Data.Tags ","}} {{range $parts}} {{trim .}} {{end}} ``` #### join Joins a slice into a string: ```html theme={null} ``` #### hasPrefix Checks if a string starts with a prefix: ```html theme={null} {{if hasPrefix .Data.URL "https"}} Secure {{end}} ``` #### hasSuffix Checks if a string ends with a suffix: ```html theme={null} {{if hasSuffix .Data.Filename ".pdf"}} PDF {{end}} ``` ### Summary Table | Function | Description | Example | | ----------- | ----------------- | --------------------------------------- | | `dict` | Create a map | `{{dict "key" "value"}}` | | `list` | Create a slice | `{{list 1 2 3}}` | | `upper` | Uppercase string | `{{upper .Data.Name}}` | | `lower` | Lowercase string | `{{lower .Data.Name}}` | | `trim` | Trim whitespace | `{{trim .Data.Text}}` | | `contains` | Check substring | `{{if contains .Data.Text "hello"}}` | | `replace` | Replace substring | `{{replace .Data.Text "old" "new" -1}}` | | `split` | Split string | `{{split .Data.Tags ","}}` | | `join` | Join slice | `{{join .Data.Items ", "}}` | | `hasPrefix` | Check prefix | `{{if hasPrefix .Data.URL "https"}}` | | `hasSuffix` | Check suffix | `{{if hasSuffix .Data.File ".go"}}` | ## Go's Standard Functions Go's `html/template` package provides these built-in functions: ### Comparison Functions | Function | Description | | -------- | --------------------- | | `eq` | Equal | | `ne` | Not equal | | `lt` | Less than | | `le` | Less than or equal | | `gt` | Greater than | | `ge` | Greater than or equal | ```html theme={null} {{if eq .Data.Status "active"}}Active{{end}} {{if gt .Data.Count 10}}Many items{{end}} {{if le .Data.Stock 5}}Low stock{{end}} ``` ### Logic Functions | Function | Description | | -------- | ----------- | | `and` | Logical AND | | `or` | Logical OR | | `not` | Logical NOT | ```html theme={null} {{if and .Data.LoggedIn .Data.IsAdmin}}Admin controls{{end}} {{if or .Data.Error .Data.Warning}}Alert{{end}} {{if not .Data.Disabled}}Enabled{{end}} ``` ### Other Standard Functions | Function | Description | | ---------- | -------------------------------------- | | `len` | Length of array, slice, map, or string | | `index` | Index into array, slice, or map | | `print` | Format with default formatting | | `printf` | Format with format string | | `println` | Format with newline | | `html` | HTML escape | | `js` | JavaScript escape | | `urlquery` | URL query escape | ```html theme={null}

    {{len .Data.Items}} items

    First: {{index .Data.Items 0}}

    {{printf "%.2f" .Data.Price}}

    ``` ## Adding Custom Functions Add your own functions via the `Funcs` option: ```go theme={null} import ( "html/template" "strings" "time" "github.com/go-mizu/mizu/view" ) engine := view.New(view.Config{ Dir: "./views", Funcs: template.FuncMap{ // Simple function "greet": func(name string) string { return "Hello, " + name + "!" }, // Function with multiple arguments "pluralize": func(count int, singular, plural string) string { if count == 1 { return singular } return plural }, // Date formatting "formatDate": func(t time.Time, layout string) string { return t.Format(layout) }, // Word count "wordCount": func(s string) int { return len(strings.Fields(s)) }, // Truncate with ellipsis "truncate": func(s string, length int) string { if len(s) <= length { return s } return s[:length] + "..." }, }, }) ``` ### Using Custom Functions ```html theme={null}

    {{greet .Data.Name}}

    {{.Data.Count}} {{pluralize .Data.Count "item" "items"}}

    Published: {{formatDate .Data.CreatedAt "January 2, 2006"}}

    {{wordCount .Data.Content}} words

    {{truncate .Data.Description 100}}

    ``` ## Common Custom Functions ### Date Formatting ```go theme={null} "formatDate": func(t time.Time, layout string) string { return t.Format(layout) }, "timeAgo": func(t time.Time) string { duration := time.Since(t) switch { case duration < time.Minute: return "just now" case duration < time.Hour: return fmt.Sprintf("%d minutes ago", int(duration.Minutes())) case duration < 24*time.Hour: return fmt.Sprintf("%d hours ago", int(duration.Hours())) default: return fmt.Sprintf("%d days ago", int(duration.Hours()/24)) } }, ``` ```html theme={null} {{timeAgo .Data.CreatedAt}} ``` ### Number Formatting ```go theme={null} "formatNumber": func(n int) string { // Simple thousands separator s := strconv.Itoa(n) if len(s) <= 3 { return s } var result []byte for i, c := range s { if i > 0 && (len(s)-i)%3 == 0 { result = append(result, ',') } result = append(result, byte(c)) } return string(result) }, "formatCurrency": func(amount float64) string { return fmt.Sprintf("$%.2f", amount) }, ``` ```html theme={null}

    {{formatNumber .Data.Views}} views

    Price: {{formatCurrency .Data.Price}}

    ``` ### URL Helpers ```go theme={null} "urlEncode": url.QueryEscape, "activeClass": func(currentPath, linkPath string) string { if currentPath == linkPath { return "active" } return "" }, ``` ```html theme={null} Search Home ``` ### Safe HTML (Use Carefully!) ```go theme={null} "safeHTML": func(s string) template.HTML { return template.HTML(s) }, ``` ```html theme={null} {{/* Only use with trusted content! */}} {{safeHTML .Data.TrustedContent}} ``` ### Default Values ```go theme={null} "default": func(defaultVal, val interface{}) interface{} { if val == nil || val == "" || val == 0 { return defaultVal } return val }, ``` ```html theme={null} ``` ## Best Practices ### 1. Keep Functions Pure Functions should not have side effects: ```go theme={null} // Good: pure function "double": func(n int) int { return n * 2 }, // Bad: has side effects "logAndReturn": func(s string) string { log.Println(s) // Side effect! return s }, ``` ### 2. Handle Edge Cases ```go theme={null} "truncate": func(s string, length int) string { if s == "" || length <= 0 { return "" } if len(s) <= length { return s } return s[:length] + "..." }, ``` ### 3. Use Pipelines for Readability ```html theme={null}

    {{.Data.Bio | truncate 200 | upper}}

    {{upper (truncate .Data.Bio 200)}}

    ``` ### 4. Document Your Functions ```go theme={null} Funcs: template.FuncMap{ // formatPhone formats a phone number as (XXX) XXX-XXXX // Example: "1234567890" → "(123) 456-7890" "formatPhone": formatPhone, // pluralize returns singular if count is 1, plural otherwise // Example: pluralize 1 "item" "items" → "item" "pluralize": pluralize, } ``` ### 5. Test Custom Functions ```go theme={null} func TestTruncate(t *testing.T) { tests := []struct { input string length int expected string }{ {"hello", 10, "hello"}, {"hello world", 5, "hello..."}, {"", 5, ""}, } for _, tt := range tests { result := truncate(tt.input, tt.length) if result != tt.expected { t.Errorf("truncate(%q, %d) = %q, want %q", tt.input, tt.length, result, tt.expected) } } } ``` ### 6. Be Careful with safeHTML Only use `safeHTML` with content you completely trust: ```html theme={null} {{safeHTML .Data.TrustedContent}} {{.Data.UserComment}} ``` ## Complete Example ```go theme={null} package main import ( "fmt" "html/template" "strings" "time" "github.com/go-mizu/mizu" "github.com/go-mizu/mizu/view" ) func main() { engine := view.New(view.Config{ Dir: "./views", DefaultLayout: "default", Funcs: template.FuncMap{ // String helpers "truncate": truncate, "slugify": slugify, // Date helpers "formatDate": formatDate, "timeAgo": timeAgo, // Number helpers "formatNumber": formatNumber, "formatCurrency": formatCurrency, // Logic helpers "default": defaultValue, "pluralize": pluralize, }, }) app := mizu.New() app.Use(engine.Middleware()) // ... routes app.Listen(":8080") } func truncate(s string, length int) string { if len(s) <= length { return s } return s[:length] + "..." } func slugify(s string) string { return strings.ToLower(strings.ReplaceAll(s, " ", "-")) } func formatDate(t time.Time, layout string) string { return t.Format(layout) } func timeAgo(t time.Time) string { d := time.Since(t) switch { case d < time.Minute: return "just now" case d < time.Hour: return fmt.Sprintf("%dm ago", int(d.Minutes())) case d < 24*time.Hour: return fmt.Sprintf("%dh ago", int(d.Hours())) default: return fmt.Sprintf("%dd ago", int(d.Hours()/24)) } } func formatNumber(n int) string { return fmt.Sprintf("%d", n) // Simplified } func formatCurrency(amount float64) string { return fmt.Sprintf("$%.2f", amount) } func defaultValue(defaultVal, val interface{}) interface{} { if val == nil || val == "" { return defaultVal } return val } func pluralize(count int, singular, plural string) string { if count == 1 { return singular } return plural } ``` # Layouts Source: https://docs.go-mizu.dev/view/layouts Create consistent page structures with layouts Layouts are the HTML shell that wraps your pages. They contain the common structure shared across pages: doctype, head, navigation, footer, and more. ## What is a Layout? A layout is a template that defines the outer structure of your pages. Instead of repeating the HTML boilerplate in every page, you define it once in a layout. **Without layouts - every page repeats this:** ```html theme={null} Page Title
    ...
    ``` **With layouts - define once, pages fill in the content:** ```html theme={null} {{.Data.Title}} {{.Content}}
    ...
    ``` ## Directory Structure Layouts live in the `layouts/` subdirectory: ``` views/ ├── layouts/ │ ├── default.html # Default layout │ ├── admin.html # Admin layout │ └── minimal.html # Minimal layout └── pages/ ├── home.html └── dashboard.html ``` ## Creating a Layout ### Basic Layout ```html theme={null} {{.Data.Title}}
    {{.Content}}

    © 2024 My Company

    ``` **Key point:** `{{.Content}}` is where the rendered page content is inserted. ### Page Template Pages simply provide content - no layout wrapper needed: ```html theme={null}

    Welcome, {{.Data.Name}}!

    This is the home page.

    ``` ## How It Works When you render a page: ```go theme={null} view.Render(c, "home", view.Data{ "Title": "Home", "Name": "Alice", }) ``` The engine: 1. Renders `pages/home.html` with your data 2. Puts the result in `.Content` 3. Renders `layouts/default.html` with `.Content` set ## Setting the Default Layout Configure the default layout in your engine: ```go theme={null} engine := view.New(view.Config{ Dir: "./views", DefaultLayout: "default", // Uses layouts/default.html }) ``` ## Multiple Layouts Create different layouts for different sections of your site. ### Admin Layout ```html theme={null} Admin - {{.Data.Title}}

    {{.Data.PageTitle}}

    {{.Content}}
    ``` ### Using a Different Layout Override the default layout when rendering: ```go theme={null} func dashboardHandler(c *mizu.Ctx) error { return view.Render(c, "admin/dashboard", view.Data{ "Title": "Dashboard", "PageTitle": "Admin Dashboard", "Stats": stats, }, view.Layout("admin")) } ``` ### Minimal Layout For simple pages that don't need full navigation: ```html theme={null} {{.Data.Title}} {{.Content}} ``` ```go theme={null} func loginHandler(c *mizu.Ctx) error { return view.Render(c, "login", view.Data{ "Title": "Login", }, view.Layout("minimal")) } ``` ## No Layout For AJAX responses, partial HTML, or API endpoints that return HTML fragments: ```go theme={null} // Returns just the page content, no layout view.Render(c, "user-row", data, view.NoLayout()) ``` This is useful for: * HTMX partial updates * AJAX content loading * Email templates (that define their own complete HTML) ## Layout Data Access Layouts have access to all the same data as pages: | Field | Description | | -------------- | -------------------------- | | `.Data` | Your data from the handler | | `.Page.Name` | Page template name | | `.Page.Layout` | Layout name | | `.Content` | Rendered page content | ```go theme={null} view.Render(c, "home", view.Data{ "Title": "Home Page", "CurrentUser": user, "Year": time.Now().Year(), }) ``` ```html theme={null} {{.Data.Title}}
    {{.Content}}

    © {{.Data.Year}} My Company

    ``` ## Common Patterns ### Dynamic Navigation Highlight the current page: ```html theme={null} ``` Or pass the current path: ```go theme={null} view.Data{ "CurrentPath": c.Path(), } ``` ```html theme={null} Home ``` ### Flash Messages Display flash messages from sessions: ```html theme={null} {{if .Data.Flash}}
    {{.Data.Flash.Message}}
    {{end}} ``` ### Page-Specific Body Classes ```html theme={null} ``` ```go theme={null} view.Data{ "BodyClass": "page-home dark-mode", } ``` ### Meta Tags ```html theme={null} {{.Data.Title}} {{if .Data.NoIndex}}{{end}} ``` ### Conditional Scripts ```html theme={null} {{.Content}} {{if .Data.NeedsChart}} {{end}} ``` ## Best Practices ### 1. Keep Layouts Focused Each layout should serve a clear purpose: * `default.html` - Main public pages * `admin.html` - Admin panel * `auth.html` - Login/signup pages * `email.html` - Email templates ### 2. Pass Common Data via Middleware Instead of passing the same data in every handler, use middleware: ```go theme={null} func commonDataMiddleware(next mizu.Handler) mizu.Handler { return func(c *mizu.Ctx) error { c.Locals("Year", time.Now().Year()) c.Locals("CurrentPath", c.Path()) return next(c) } } ``` ### 3. Use Meaningful Names ```go theme={null} // Good: clear purpose view.Layout("admin") view.Layout("email") // Avoid: unclear view.Layout("layout2") view.Layout("other") ``` ### 4. Don't Overcomplicate Start simple. Most sites only need 2-3 layouts: * Main layout for public pages * Admin layout for backend * Minimal layout for auth pages ### 5. Test All Layouts Make sure every layout works with your data: ```go theme={null} func TestLayouts(t *testing.T) { engine := view.New(config) layouts := []string{"default", "admin", "minimal"} for _, layout := range layouts { var buf bytes.Buffer err := engine.Render(&buf, "test-page", testData, view.Layout(layout)) if err != nil { t.Errorf("Layout %s failed: %v", layout, err) } } } ``` # Overview Source: https://docs.go-mizu.dev/view/live-overview Real-time WebSocket communication with topic-based pub/sub The `live` package provides low-latency real-time message delivery over WebSocket with topic-based publish/subscribe. It's designed as a transport layer for instant updates: chat, notifications, collaborative editing, real-time dashboards, and more. ## What is Real-time Communication? In traditional HTTP, the browser sends a request and waits for a response. For new data, you must make another request (polling). **Traditional HTTP:** ``` Client: "Give me messages" → Server: Returns messages Client: (waits...) Client: "Any new messages?" → Server: Returns messages ``` With WebSockets, the connection stays open. Either side can send messages anytime. **WebSocket:** ``` Client: "Connect" → Server: Connected! Server: "New message!" → Client: Receives instantly Server: "Another message!" → Client: Receives instantly Client: "I'm typing..." → Server: Receives instantly ``` ## Design Principles | Principle | Description | | --------------- | --------------------------------------- | | Transport-only | Moves messages, doesn't interpret state | | Best-effort | No durability or replay guarantees | | Topic-based | Scalable, simple pub/sub routing | | Opaque payloads | Your app defines message schemas | | Minimal surface | Few types, predictable behavior | ## Architecture ``` ┌─────────────────────┐ │ Live Server │ │ │ Clients │ ┌───────────────┐ │ │ │ │ Sessions │ │ │ WebSocket │ │ │ │ ├──────────────────────▶│ │ Session A │ │ │ │ │ Session B │ │ │ WebSocket │ │ Session C │ │ ├──────────────────────▶│ │ │ │ │ │ └───────────────┘ │ │ WebSocket │ │ │ ├──────────────────────▶│ │ │ │ │ ┌──────▼──────┐ │ │ │ Topics │ │ │ │ │ │ │ │ "chat:room1"│ │ │ │ "user:123" │ │ │ │ "global" │ │ │ │ │ │ │ └─────────────┘ │ │ │ └─────────────────────┘ ``` 1. **Clients connect** via WebSocket 2. **Sessions created** for each connection 3. **Server subscribes sessions** to topics 4. **Messages published** to topics 5. **All subscribers** receive the message ## Core Concepts ### Server The Server manages WebSocket connections and pub/sub routing. ```go theme={null} import "github.com/go-mizu/mizu/live" server := live.New(live.Options{ OnAuth: func(ctx context.Context, r *http.Request) (any, error) { // Authenticate connection return userID, nil }, }) ``` ### Session A Session represents one WebSocket connection. Each has: * Unique ID * Optional value from authentication * Send queue for outgoing messages ### Message Messages have a topic and data: ```go theme={null} type Message struct { Topic string `json:"topic,omitempty"` Data json.RawMessage `json:"data,omitempty"` } ``` ### Topics Topics are named channels. Sessions subscribe to topics they care about. When you publish to a topic, all subscribers receive the message. ```go theme={null} // Subscribe session to topic server.Subscribe(session, "chat:room1") // Publish to all subscribers server.Publish("chat:room1", []byte(`{"text":"Hello!"}`)) ``` ## Quick Example **Server (Go):** ```go theme={null} package main import ( "context" "encoding/json" "net/http" "github.com/go-mizu/mizu" "github.com/go-mizu/mizu/live" ) func main() { server := live.New(live.Options{ OnAuth: func(ctx context.Context, r *http.Request) (any, error) { userID := r.URL.Query().Get("user") if userID == "" { return nil, fmt.Errorf("user required") } return userID, nil }, OnMessage: func(ctx context.Context, s *live.Session, topic string, data []byte) { // Handle incoming messages if topic == "subscribe" { var req struct{ Topic string `json:"topic"` } json.Unmarshal(data, &req) server.Subscribe(s, req.Topic) } else if topic == "chat" { // Broadcast to all in chat server.Publish("chat:general", data) } }, OnClose: func(s *live.Session, err error) { log.Printf("Session %s closed: %v", s.ID(), err) }, }) app := mizu.New() app.Get("/ws", mizu.Compat(server.Handler())) app.Listen(":8080") } ``` **Client (JavaScript):** ```javascript theme={null} const ws = new WebSocket('ws://localhost:8080/ws?user=alice'); ws.onmessage = (event) => { const msg = JSON.parse(event.data); console.log(`[${msg.topic}]`, msg.data); }; // Subscribe to a topic ws.send(JSON.stringify({topic: 'subscribe', data: {topic: 'chat:general'}})); // Send a chat message ws.send(JSON.stringify({topic: 'chat', data: {text: 'Hello!'}})); ``` ## When to Use Live **Use Live when:** * You need instant updates (\< 100ms latency) * Multiple clients need to see the same data * Building interactive features (chat, gaming) * Pushing data to clients (notifications) **Consider alternatives when:** * Updates are infrequent (use polling or SSE) * You need message durability (use sync package) * Building a simple REST API ## What's Next? 1. **[Quick Start](/view/live-quick-start)** - Build your first live feature 2. **[Server](/view/live-server)** - All server options explained 3. **[Sessions](/view/live-sessions)** - Connection lifecycle 4. **[Pub/Sub](/view/live-pubsub)** - Topic patterns and publishing # Pub/Sub Source: https://docs.go-mizu.dev/view/live-pubsub Topic-based publish/subscribe for real-time messaging The publish/subscribe pattern is at the heart of the live package. Sessions subscribe to topics, and when you publish to a topic, all subscribers receive the message. ## How Pub/Sub Works ``` ┌─────────────────────────┐ │ Server │ │ │ Publish to │ Topic: "chat:room1" │ Delivered to "chat:room1" ────▶│ ┌─────────────────┐ │────▶ Session A │ │ Subscribers: │ │────▶ Session B │ │ - Session A │ │ │ │ - Session B │ │ Session C (not subscribed) │ │ - Session D │ │────▶ Session D │ └─────────────────┘ │ │ │ └─────────────────────────┘ ``` ## Subscribing Subscribe sessions to topics via server methods: ```go theme={null} // In OnMessage handler OnMessage: func(ctx context.Context, s *live.Session, topic string, data []byte) { if topic == "subscribe" { var req struct{ Topic string `json:"topic"` } json.Unmarshal(data, &req) server.Subscribe(s, req.Topic) } if topic == "unsubscribe" { var req struct{ Topic string `json:"topic"` } json.Unmarshal(data, &req) server.Unsubscribe(s, req.Topic) } } ``` ## Publishing ### To All Subscribers ```go theme={null} server.Publish("chat:room1", []byte(`{"text":"Hello everyone!"}`)) ``` ### With JSON Data ```go theme={null} data, _ := json.Marshal(map[string]any{ "type": "message", "user": userID, "text": "Hello!", }) server.Publish("chat:room1", data) ``` ### To a Single Session ```go theme={null} s.Send(live.Message{ Topic: "notification", Data: []byte(`{"title":"Hello"}`), }) ``` ## Topic Naming Patterns Use consistent, hierarchical names: ### User Topics ``` user:{userID} # Personal messages user:{userID}:notifications # User notifications ``` ### Room/Channel Topics ``` chat:general # General chat chat:room:{roomID} # Specific room ``` ### Event Topics ``` events:user:created # User events events:order:updated # Order events ``` ### Resource Topics ``` item:{itemID} # Updates for specific item team:{teamID} # Team broadcasts ``` ## Common Patterns ### Chat Room ```go theme={null} OnMessage: func(ctx context.Context, s *live.Session, topic string, data []byte) { userID := s.Value().(string) switch topic { case "join_room": var req struct{ Room string `json:"room"` } json.Unmarshal(data, &req) server.Subscribe(s, "room:"+req.Room) // Announce join msg, _ := json.Marshal(map[string]string{"type": "join", "user": userID}) server.Publish("room:"+req.Room, msg) case "chat": var req struct { Room string `json:"room"` Text string `json:"text"` } json.Unmarshal(data, &req) msg, _ := json.Marshal(map[string]string{ "type": "message", "user": userID, "text": req.Text, }) server.Publish("room:"+req.Room, msg) } } ``` ### Notifications ```go theme={null} // Send notification from anywhere in your app func NotifyUser(userID string, notification Notification) { data, _ := json.Marshal(notification) server.Publish("user:"+userID+":notifications", data) } // Usage NotifyUser("user123", Notification{ Title: "Order Shipped", Message: "Your order #456 has shipped!", }) ``` ### Real-time Updates ```go theme={null} // When data changes, notify watchers func UpdateItem(item Item) error { // Save to database if err := db.Save(item); err != nil { return err } // Notify all watchers data, _ := json.Marshal(map[string]any{ "type": "updated", "item": item, }) server.Publish("item:"+item.ID, data) return nil } ``` ### Presence ```go theme={null} OnMessage: func(ctx context.Context, s *live.Session, topic string, data []byte) { userID := s.Value().(string) if topic == "join_room" { var req struct{ Room string `json:"room"` } json.Unmarshal(data, &req) // Subscribe to room presence and messages server.Subscribe(s, "room:"+req.Room+":presence") server.Subscribe(s, "room:"+req.Room+":messages") // Announce presence msg, _ := json.Marshal(map[string]string{"type": "join", "user": userID}) server.Publish("room:"+req.Room+":presence", msg) } }, OnClose: func(s *live.Session, err error) { userID := s.Value().(string) // Note: You'd need to track which rooms the user was in // to announce departure } ``` ## Publishing from HTTP Handlers You can publish from any part of your application: ```go theme={null} // API endpoint triggers notification app.Post("/api/items", func(c *mizu.Ctx) error { var item Item c.BodyJSON(&item) // Save db.Save(&item) // Notify via WebSocket data, _ := json.Marshal(map[string]any{ "type": "new_item", "item": item, }) server.Publish("team:"+item.TeamID, data) return c.JSON(item) }) // Background job publishes updates func processJob(job Job) { // ... do work ... progress, _ := json.Marshal(map[string]any{ "type": "progress", "job_id": job.ID, "progress": job.Progress, }) server.Publish("job:"+job.ID, progress) } ``` ## Message Format Messages sent to clients have this format: ```json theme={null} { "topic": "chat:room1", "data": {"type": "message", "user": "alice", "text": "Hello!"} } ``` Client-side parsing: ```javascript theme={null} ws.onmessage = (event) => { const msg = JSON.parse(event.data); console.log(`Topic: ${msg.topic}`); console.log(`Data:`, msg.data); }; ``` ## Performance Tips ### Topic Granularity ```go theme={null} // Too broad - everyone gets everything server.Subscribe(s, "all_updates") // Too narrow - many small topics server.Subscribe(s, "user:123:item:456:field:name") // Just right - meaningful groups server.Subscribe(s, "user:"+userID) server.Subscribe(s, "team:"+teamID) ``` ### Message Size ```go theme={null} // Bad: send large objects server.Publish(topic, entireLargeObject) // Good: send just what changed server.Publish(topic, []byte(`{"id":"123","status":"updated"}`)) ``` ### Fan-out Be aware of high fan-out: ```go theme={null} // 10,000 subscribers = 10,000 messages server.Publish("global", message) // Consider if all really need this update ``` ## Subscription Validation Always validate subscriptions: ```go theme={null} OnMessage: func(ctx context.Context, s *live.Session, topic string, data []byte) { if topic == "subscribe" { var req struct{ Topic string `json:"topic"` } json.Unmarshal(data, &req) userID := s.Value().(string) // Validate permission if !canSubscribe(userID, req.Topic) { s.Send(live.Message{ Topic: "error", Data: []byte(`{"message":"not authorized"}`), }) return } server.Subscribe(s, req.Topic) } } func canSubscribe(userID, topic string) bool { // Check topic patterns if strings.HasPrefix(topic, "admin:") { return isAdmin(userID) } if strings.HasPrefix(topic, "team:") { teamID := strings.Split(strings.TrimPrefix(topic, "team:"), ":")[0] return isTeamMember(userID, teamID) } if strings.HasPrefix(topic, "user:") { targetUser := strings.Split(strings.TrimPrefix(topic, "user:"), ":")[0] return targetUser == userID // Can only subscribe to own topics } return true } ``` ## Best Practices ### 1. Use Namespaced Topics ```go theme={null} // Good: clear hierarchy "chat:room:123" "user:456:notifications" // Avoid: ambiguous "room123" "notifications" ``` ### 2. Document Conventions ```go theme={null} // Topic conventions: // - user:{id} Direct messages to user // - user:{id}:notifs User notifications // - team:{id} Team broadcasts // - chat:{roomId} Chat room messages // - presence:{roomId} Room presence updates ``` ### 3. Consistent Message Format ```go theme={null} type Message struct { Type string `json:"type"` Data any `json:"data,omitempty"` } func publish(topic string, msgType string, data any) { msg := Message{Type: msgType, Data: data} bytes, _ := json.Marshal(msg) server.Publish(topic, bytes) } // Usage publish("chat:room1", "message", ChatMessage{User: "alice", Text: "Hi"}) publish("chat:room1", "typing", TypingIndicator{User: "bob"}) ``` # Quick Start Source: https://docs.go-mizu.dev/view/live-quick-start Build your first real-time feature with WebSockets This guide walks you through building a real-time notification system. Users receive instant notifications via WebSocket. ## Prerequisites * Go 1.22 or later * Basic understanding of Go and JavaScript ## Step 1: Create Your Project ```bash theme={null} mkdir live-demo && cd live-demo go mod init live-demo go get github.com/go-mizu/mizu mkdir public ``` ## Step 2: Create the Server Create `main.go`: ```go theme={null} package main import ( "context" "encoding/json" "fmt" "log" "net/http" "github.com/go-mizu/mizu" "github.com/go-mizu/mizu/live" ) var server *live.Server func main() { // Create the live server server = live.New(live.Options{ // Authenticate connections OnAuth: func(ctx context.Context, r *http.Request) (any, error) { userID := r.URL.Query().Get("user") if userID == "" { return nil, fmt.Errorf("user parameter required") } return userID, nil }, // Handle incoming messages OnMessage: func(ctx context.Context, s *live.Session, topic string, data []byte) { userID := s.Value().(string) log.Printf("Message from %s: topic=%s", userID, topic) switch topic { case "subscribe": // Client wants to subscribe to a topic var req struct{ Topic string `json:"topic"` } json.Unmarshal(data, &req) server.Subscribe(s, req.Topic) log.Printf("User %s subscribed to %s", userID, req.Topic) case "broadcast": // Broadcast to all on global topic server.Publish("global", data) } }, // Handle disconnections OnClose: func(s *live.Session, err error) { userID := s.Value().(string) log.Printf("User %s disconnected: %v", userID, err) }, }) // Create Mizu app app := mizu.New() // WebSocket endpoint app.Get("/ws", mizu.Compat(server.Handler())) // API to send notifications app.Post("/api/notify", notifyHandler) app.Post("/api/broadcast", broadcastHandler) // Serve static files app.Static("/", "./public") log.Println("Server running on http://localhost:8080") app.Listen(":8080") } func notifyHandler(c *mizu.Ctx) error { var req struct { UserID string `json:"user_id"` Title string `json:"title"` Message string `json:"message"` } if err := c.BodyJSON(&req); err != nil { return c.Status(400).JSON(map[string]string{"error": "invalid request"}) } // Publish to user's personal topic data, _ := json.Marshal(map[string]string{ "title": req.Title, "message": req.Message, }) server.Publish("user:"+req.UserID, data) return c.JSON(map[string]string{"status": "sent"}) } func broadcastHandler(c *mizu.Ctx) error { var req struct { Title string `json:"title"` Message string `json:"message"` } if err := c.BodyJSON(&req); err != nil { return c.Status(400).JSON(map[string]string{"error": "invalid request"}) } // Publish to global topic data, _ := json.Marshal(map[string]string{ "title": req.Title, "message": req.Message, }) server.Publish("global", data) return c.JSON(map[string]string{"status": "broadcast sent"}) } ``` ## Step 3: Create the Frontend Create `public/index.html`: ```html theme={null} Live Notifications

    Live Notifications

    Disconnected

    Notifications

    Send Test

    ``` ## Step 4: Run and Test ```bash theme={null} go run main.go ``` Open `http://localhost:8080` in your browser. 1. Enter a user ID (e.g., "alice") and click Connect 2. Open another tab, connect as "bob" 3. Send a notification from Alice to Bob 4. Try the broadcast feature ## Understanding the Code ### Authentication ```go theme={null} OnAuth: func(ctx context.Context, r *http.Request) (any, error) { userID := r.URL.Query().Get("user") if userID == "" { return nil, fmt.Errorf("user required") } return userID, nil // Value stored in session } ``` `OnAuth` returns: * `(value, nil)` - Accept connection, store value in session * `(nil, error)` - Reject connection ### Message Handling ```go theme={null} OnMessage: func(ctx context.Context, s *live.Session, topic string, data []byte) { // topic: extracted from incoming message // data: the message payload } ``` Messages from clients are JSON with `topic` and `data` fields: ```json theme={null} {"topic": "chat", "data": {"text": "Hello"}} ``` ### Subscribe and Publish ```go theme={null} // Subscribe session to topic server.Subscribe(s, "user:"+userID) // Publish to all subscribers server.Publish("user:"+targetID, data) ``` ## Next Steps * **[Server](/view/live-server)** - All configuration options * **[Sessions](/view/live-sessions)** - Session lifecycle * **[Pub/Sub](/view/live-pubsub)** - Topic patterns # Server Source: https://docs.go-mizu.dev/view/live-server Configure the live server for WebSocket connections The Live server manages WebSocket connections, handles message routing, and provides hooks for authentication and lifecycle events. ## Creating a Server ```go theme={null} import "github.com/go-mizu/mizu/live" server := live.New(live.Options{ // Options here }) ``` ## Configuration Options ### OnAuth Called when a client attempts to connect. Return a value to store in the session, or an error to reject. ```go theme={null} live.Options{ OnAuth: func(ctx context.Context, r *http.Request) (any, error) { // Option 1: Query parameter userID := r.URL.Query().Get("user") if userID == "" { return nil, errors.New("user required") } return userID, nil // Option 2: JWT token token := r.Header.Get("Authorization") claims, err := validateJWT(token) if err != nil { return nil, err } return claims.UserID, nil // Option 3: Cookie session cookie, err := r.Cookie("session") if err != nil { return nil, err } return getSessionUser(cookie.Value) }, } ``` If `OnAuth` is nil, all connections are accepted. ### OnMessage Called when a client sends a message. The message is parsed and topic/data are extracted. ```go theme={null} live.Options{ OnMessage: func(ctx context.Context, s *live.Session, topic string, data []byte) { userID := s.Value().(string) log.Printf("Message from %s: %s", userID, topic) switch topic { case "subscribe": var req struct{ Topic string `json:"topic"` } json.Unmarshal(data, &req) server.Subscribe(s, req.Topic) case "chat": server.Publish("chat:general", data) } }, } ``` Client messages must be JSON with `topic` and `data`: ```json theme={null} {"topic": "chat", "data": {"text": "Hello"}} ``` ### OnClose Called when a connection closes. ```go theme={null} live.Options{ OnClose: func(s *live.Session, err error) { userID := s.Value().(string) if err != nil { log.Printf("User %s disconnected with error: %v", userID, err) } else { log.Printf("User %s disconnected cleanly", userID) } // Clean up removeFromOnlineList(userID) }, } ``` ### QueueSize Per-session send buffer size. When full, the session is closed. ```go theme={null} live.Options{ QueueSize: 256, // Default: 256 } ``` ### ReadLimit Maximum incoming message size in bytes. ```go theme={null} live.Options{ ReadLimit: 4 * 1024 * 1024, // Default: 4MB } ``` ### Origins List of allowed WebSocket origins. ```go theme={null} live.Options{ Origins: []string{ "http://localhost:8080", "https://myapp.com", }, } ``` Use `"*"` to allow all origins (not recommended for production). ### CheckOrigin Custom origin validation (takes precedence over Origins). ```go theme={null} live.Options{ CheckOrigin: func(r *http.Request) bool { origin := r.Header.Get("Origin") return strings.HasSuffix(origin, ".mycompany.com") }, } ``` ### IDGenerator Custom session ID generator. ```go theme={null} live.Options{ IDGenerator: func() string { return uuid.New().String() }, } ``` ## Server Methods ### Handler Returns an HTTP handler for WebSocket upgrades. ```go theme={null} app.Get("/ws", mizu.Compat(server.Handler())) // Or with standard library http.Handle("/ws", server.Handler()) ``` ### Publish Sends data to all sessions subscribed to a topic. ```go theme={null} server.Publish("chat:room1", []byte(`{"text":"Hello!"}`)) // With JSON data, _ := json.Marshal(message) server.Publish(topic, data) ``` ### Subscribe Subscribes a session to a topic. ```go theme={null} server.Subscribe(session, "chat:room1") server.Subscribe(session, "user:"+userID) ``` ### Unsubscribe Removes a session from a topic. ```go theme={null} server.Unsubscribe(session, "chat:room1") ``` ## Complete Example ```go theme={null} package main import ( "context" "encoding/json" "errors" "log" "net/http" "github.com/go-mizu/mizu" "github.com/go-mizu/mizu/live" ) var server *live.Server func main() { server = live.New(live.Options{ // Authentication OnAuth: func(ctx context.Context, r *http.Request) (any, error) { token := r.URL.Query().Get("token") userID, err := validateToken(token) if err != nil { return nil, err } return userID, nil }, // Message handling OnMessage: func(ctx context.Context, s *live.Session, topic string, data []byte) { handleMessage(s, topic, data) }, // Cleanup on disconnect OnClose: func(s *live.Session, err error) { userID := s.Value().(string) log.Printf("User %s disconnected", userID) server.Publish("presence", []byte(`{"type":"leave","user":"`+userID+`"}`)) }, // Limits QueueSize: 512, ReadLimit: 64 * 1024, // Origins Origins: []string{"https://myapp.com"}, }) app := mizu.New() app.Get("/ws", mizu.Compat(server.Handler())) app.Listen(":8080") } func handleMessage(s *live.Session, topic string, data []byte) { userID := s.Value().(string) switch topic { case "subscribe": var req struct{ Topic string `json:"topic"` } json.Unmarshal(data, &req) // Validate subscription if canSubscribe(userID, req.Topic) { server.Subscribe(s, req.Topic) } case "unsubscribe": var req struct{ Topic string `json:"topic"` } json.Unmarshal(data, &req) server.Unsubscribe(s, req.Topic) case "chat": // Broadcast chat message msg, _ := json.Marshal(map[string]any{ "user": userID, "data": json.RawMessage(data), }) server.Publish("chat:general", msg) } } ``` ## Mounting the Handler ### With Mizu ```go theme={null} app := mizu.New() app.Get("/ws", mizu.Compat(server.Handler())) ``` ### With Standard Library ```go theme={null} http.Handle("/ws", server.Handler()) http.ListenAndServe(":8080", nil) ``` ## Best Practices ### 1. Always Authenticate ```go theme={null} OnAuth: func(ctx context.Context, r *http.Request) (any, error) { // Never skip authentication in production token := r.Header.Get("Authorization") if token == "" { return nil, errors.New("unauthorized") } return validateToken(token) }, ``` ### 2. Set Reasonable Limits ```go theme={null} live.Options{ QueueSize: 256, // Prevent memory issues ReadLimit: 64 * 1024, // Limit message size } ``` ### 3. Handle Close Gracefully ```go theme={null} OnClose: func(s *live.Session, err error) { // Clean up resources userID := s.Value().(string) removeFromRooms(userID) notifyOthers(userID, "offline") }, ``` ### 4. Validate Subscriptions ```go theme={null} OnMessage: func(ctx context.Context, s *live.Session, topic string, data []byte) { if topic == "subscribe" { var req struct{ Topic string `json:"topic"` } json.Unmarshal(data, &req) // Check permission before subscribing if !canSubscribe(s.Value().(string), req.Topic) { s.Send(live.Message{Topic: "error", Data: []byte(`"not allowed"`)}) return } server.Subscribe(s, req.Topic) } }, ``` # Sessions Source: https://docs.go-mizu.dev/view/live-sessions Managing WebSocket connections and session lifecycle A Session represents a single WebSocket connection. Each connected client has its own session with a unique ID and associated value from authentication. ## Session Lifecycle ``` Client connects (HTTP upgrade request) │ ▼ ┌─────────────┐ │ OnAuth │ ──── Error ────▶ Connection rejected (401) └─────────────┘ │ Returns value │ ▼ ┌─────────────┐ │ Session │ ◀──── Session created with ID and value │ Created │ └─────────────┘ │ ▼ ┌─────────────┐ │ Active │ ◀──── Send/Receive messages └─────────────┘ OnMessage called for each message │ Client disconnects or server closes or queue full │ ▼ ┌─────────────┐ │ OnClose │ ──── Session destroyed └─────────────┘ ``` ## Session Methods ### ID Returns the session's unique identifier. ```go theme={null} OnMessage: func(ctx context.Context, s *live.Session, topic string, data []byte) { sessionID := s.ID() log.Printf("Message from session: %s", sessionID) } ``` ### Value Returns the value stored during authentication. ```go theme={null} OnAuth: func(ctx context.Context, r *http.Request) (any, error) { // Store user info return UserInfo{ID: "123", Name: "Alice"}, nil } OnMessage: func(ctx context.Context, s *live.Session, topic string, data []byte) { // Retrieve user info user := s.Value().(UserInfo) log.Printf("Message from user: %s", user.Name) } ``` ### Send Queues a message for delivery to the client. Non-blocking. ```go theme={null} // Send a message err := s.Send(live.Message{ Topic: "notification", Data: []byte(`{"title":"Hello"}`), }) if err == live.ErrSessionClosed { log.Println("Session already closed") } else if err == live.ErrQueueFull { log.Println("Queue full, session closed") } ``` ### Close Closes the connection gracefully. ```go theme={null} // Kick a user s.Close() ``` ### IsClosed Checks if the session is closed. ```go theme={null} if s.IsClosed() { return // Don't send to closed session } ``` ### CloseError Returns the error that caused the session to close. ```go theme={null} OnClose: func(s *live.Session, err error) { closeErr := s.CloseError() if closeErr == live.ErrQueueFull { log.Println("Client couldn't keep up") } } ``` ## Subscription Management Subscriptions are managed through the server, not the session: ```go theme={null} // Subscribe a session to a topic server.Subscribe(s, "chat:room1") // Unsubscribe from a topic server.Unsubscribe(s, "chat:room1") ``` When a session closes, it's automatically unsubscribed from all topics. ## Common Patterns ### User Authentication ```go theme={null} type UserInfo struct { ID string Name string Role string } server := live.New(live.Options{ OnAuth: func(ctx context.Context, r *http.Request) (any, error) { token := r.Header.Get("Authorization") user, err := validateToken(token) if err != nil { return nil, err } return user, nil }, OnMessage: func(ctx context.Context, s *live.Session, topic string, data []byte) { user := s.Value().(UserInfo) // Check permissions if topic == "admin" && user.Role != "admin" { s.Send(live.Message{Topic: "error", Data: []byte(`"unauthorized"`)}) return } // Handle message... }, }) ``` ### Auto-Subscribe on Connect Since there's no `OnConnect` hook, use `OnMessage` with a special "init" topic: ```go theme={null} OnMessage: func(ctx context.Context, s *live.Session, topic string, data []byte) { user := s.Value().(UserInfo) switch topic { case "init": // Subscribe to default topics server.Subscribe(s, "user:"+user.ID) server.Subscribe(s, "global") // Send welcome message s.Send(live.Message{ Topic: "welcome", Data: []byte(`{"status":"connected"}`), }) case "subscribe": // Handle explicit subscription requests var req struct{ Topic string `json:"topic"` } json.Unmarshal(data, &req) server.Subscribe(s, req.Topic) } } ``` Client sends init on connect: ```javascript theme={null} ws.onopen = () => { ws.send(JSON.stringify({topic: 'init', data: {}})); }; ``` ### Presence Tracking ```go theme={null} var onlineUsers sync.Map server := live.New(live.Options{ OnAuth: func(ctx context.Context, r *http.Request) (any, error) { userID := r.URL.Query().Get("user") return userID, nil }, OnMessage: func(ctx context.Context, s *live.Session, topic string, data []byte) { userID := s.Value().(string) if topic == "init" { // Mark user online onlineUsers.Store(userID, s.ID()) // Subscribe to presence server.Subscribe(s, "presence") // Notify others server.Publish("presence", []byte(`{"type":"join","user":"`+userID+`"}`)) } }, OnClose: func(s *live.Session, err error) { userID := s.Value().(string) // Mark offline onlineUsers.Delete(userID) // Notify others server.Publish("presence", []byte(`{"type":"leave","user":"`+userID+`"}`)) }, }) ``` ### Direct Messaging ```go theme={null} // Store sessions by user ID var userSessions sync.Map // userID -> *live.Session OnMessage: func(ctx context.Context, s *live.Session, topic string, data []byte) { userID := s.Value().(string) switch topic { case "init": userSessions.Store(userID, s) case "dm": var req struct { To string `json:"to"` Text string `json:"text"` } json.Unmarshal(data, &req) // Find recipient if recipient, ok := userSessions.Load(req.To); ok { sess := recipient.(*live.Session) msg, _ := json.Marshal(map[string]string{ "from": userID, "text": req.Text, }) sess.Send(live.Message{Topic: "dm", Data: msg}) } } }, OnClose: func(s *live.Session, err error) { userID := s.Value().(string) userSessions.Delete(userID) }, ``` ### Room Management ```go theme={null} OnMessage: func(ctx context.Context, s *live.Session, topic string, data []byte) { userID := s.Value().(string) switch topic { case "join_room": var req struct{ Room string `json:"room"` } json.Unmarshal(data, &req) // Subscribe to room server.Subscribe(s, "room:"+req.Room) // Notify room msg, _ := json.Marshal(map[string]string{"type": "join", "user": userID}) server.Publish("room:"+req.Room, msg) case "leave_room": var req struct{ Room string `json:"room"` } json.Unmarshal(data, &req) // Notify room msg, _ := json.Marshal(map[string]string{"type": "leave", "user": userID}) server.Publish("room:"+req.Room, msg) // Unsubscribe server.Unsubscribe(s, "room:"+req.Room) case "room_message": var req struct { Room string `json:"room"` Text string `json:"text"` } json.Unmarshal(data, &req) msg, _ := json.Marshal(map[string]string{ "type": "message", "user": userID, "text": req.Text, }) server.Publish("room:"+req.Room, msg) } } ``` ## Errors | Error | Description | | ------------------ | -------------------------------------- | | `ErrSessionClosed` | Session is already closed | | `ErrQueueFull` | Send queue is full, session was closed | ## Best Practices ### 1. Store Minimal Data in Value ```go theme={null} // Good: just the user ID return userID, nil // Okay: small struct return UserInfo{ID: "123", Role: "admin"}, nil // Avoid: large objects return entireUserObject, nil // Bad ``` ### 2. Handle Close Gracefully ```go theme={null} OnClose: func(s *live.Session, err error) { userID := s.Value().(string) // Clean up all resources onlineUsers.Delete(userID) userSessions.Delete(userID) // Notify others if needed server.Publish("presence", []byte(`{"type":"leave","user":"`+userID+`"}`)) } ``` ### 3. Check Before Sending ```go theme={null} if !s.IsClosed() { s.Send(message) } ``` ### 4. Handle Reconnection Clients will reconnect. Design your protocol to handle it: ```javascript theme={null} // Client-side reconnection function connect() { ws = new WebSocket(url); ws.onopen = () => { // Re-initialize on every connect ws.send(JSON.stringify({topic: 'init', data: {}})); }; ws.onclose = () => setTimeout(connect, 1000); } ``` # Overview Source: https://docs.go-mizu.dev/view/overview Server-side HTML templating with Go's html/template The `view` package provides server-side HTML rendering using Go's built-in `html/template` engine. It adds a thin layer on top for layout support, custom functions, and development-friendly features like hot reloading. ## What is Server-Side Rendering? When a user visits your website, the server generates the complete HTML page and sends it to their browser. This is called **server-side rendering (SSR)**. ``` User Request Server Browser | | | | GET /users | | |--------------->| | | | | | | 1. Fetch data | | | 2. Render template | | | 3. Return HTML | | | | | ... | | |<---------------| | | | | | | Display page | | |------------------->| ``` **Benefits of SSR:** * Fast initial page load (browser displays content immediately) * SEO-friendly (search engines can read your content) * Works without JavaScript * Simple mental model (one language, one place) ## What the View Package Provides | Feature | Description | | ---------------- | --------------------------------------------------- | | Template Engine | Wraps Go's `html/template` with convenience methods | | Layouts | Wrap page content in a consistent structure | | Custom Functions | Add your own template helpers | | Hot Reload | Templates reload automatically during development | | Embedded Files | Bundle templates in your binary for production | ## How It Works The view package follows a simple flow: ``` Engine ┌─────────────────────────────────────────────────────────┐ | | | Config Templates Output | | ┌─────────┐ ┌─────────────┐ ┌─────────────┐ | | | Dir | | pages/ | | | | | | FS |--->| layouts/ |---->| ... | | | | Funcs | | | | | | | | Layout | └─────────────┘ └─────────────┘ | | └─────────┘ | | | └─────────────────────────────────────────────────────────┘ ``` 1. **Configure** - Tell the engine where templates are and how to process them 2. **Load** - Engine parses and caches your template files 3. **Render** - Pass data to a template, get HTML back ## Directory Structure The view package expects templates organized in subdirectories: ``` views/ ├── layouts/ │ └── default.html # Layout templates └── pages/ ├── home.html # Page templates └── about.html ``` * **layouts/** - Templates that wrap around page content (HTML shell, navigation, footer) * **pages/** - Individual page templates that define content ## Quick Example Here's a minimal example showing the view package in action: **Directory structure:** ``` myapp/ ├── main.go └── views/ ├── layouts/ │ └── default.html └── pages/ └── home.html ``` **views/layouts/default.html:** ```html theme={null} {{.Data.Title}} {{.Content}} ``` **views/pages/home.html:** ```html theme={null}

    Welcome, {{.Data.Name}}!

    You have {{.Data.Count}} messages.

    ``` **main.go:** ```go theme={null} package main import ( "github.com/go-mizu/mizu" "github.com/go-mizu/mizu/view" ) func main() { // Create the template engine engine := view.New(view.Config{ Dir: "./views", Development: true, }) // Create the app app := mizu.New() // Add engine to app (makes it available in handlers) app.Use(engine.Middleware()) // Define a route app.Get("/", func(c *mizu.Ctx) error { return view.Render(c, "home", view.Data{ "Title": "My App", "Name": "Alice", "Count": 5, }) }) app.Listen(":8080") } ``` When you visit `http://localhost:8080`, you'll see: ```html theme={null} My App

    Welcome, Alice!

    You have 5 messages.

    ``` ## Template Data When you render a template, your data is wrapped in a structure that provides additional context: ```go theme={null} // What you pass: view.Render(c, "home", view.Data{ "Title": "My App", "User": currentUser, }) // What templates receive: // .Page.Name - Template name ("home") // .Page.Layout - Layout name ("default") // .Data - Your data (access .Data.Title, .Data.User) // .Content - Rendered page content (only in layouts) ``` **In page templates:** ```html theme={null}

    {{.Data.Title}}

    Welcome, {{.Data.User.Name}}

    ``` **In layout templates:** ```html theme={null} {{.Data.Title}} {{.Content}} ``` ## When to Use View **Use the view package when:** * Building traditional server-rendered websites * Creating admin dashboards * Building email templates * Generating any HTML from Go **Consider alternatives when:** * Building single-page applications (SPAs) with React/Vue/etc. * Building APIs that return JSON * Using a separate frontend framework ## What's Next? 1. **[Quick Start](/view/quick-start)** - Build your first page step by step 2. **[Engine](/view/engine)** - All configuration options explained 3. **[Templates](/view/templates)** - Go template syntax guide 4. **[Layouts](/view/layouts)** - Page structure and inheritance 5. **[Functions](/view/functions)** - Built-in and custom helpers 6. **[Production](/view/production)** - Deployment best practices # Production Source: https://docs.go-mizu.dev/view/production Deploy your views in production with embedded templates and caching This guide covers everything you need to deploy your Mizu views in production: embedding templates, caching, performance optimization, and best practices. ## Embedding Templates In production, embed templates into your binary. This eliminates file system dependencies and ensures templates can't be accidentally modified. ### Using embed.FS Go's `embed` package lets you include files in your binary: ```go theme={null} package main import ( "embed" "github.com/go-mizu/mizu" "github.com/go-mizu/mizu/view" ) //go:embed views var viewsFS embed.FS func main() { engine := view.New(view.Config{ FS: viewsFS, // Use embedded filesystem Extension: ".html", DefaultLayout: "default", }) app := mizu.New() app.Use(engine.Middleware()) // ... routes ... app.Listen(":8080") } ``` ### Directory Structure for Embedding The `//go:embed` directive embeds a directory relative to the source file: ``` myapp/ ├── main.go # Contains //go:embed views ├── views/ │ ├── layouts/ │ │ └── default.html │ └── pages/ │ ├── home.html │ └── about.html └── go.mod ``` After building, the binary contains all templates. ### Conditional Embedding Use environment variables to switch between development and production: ```go theme={null} package main import ( "embed" "os" "github.com/go-mizu/mizu" "github.com/go-mizu/mizu/view" ) //go:embed views var viewsFS embed.FS func main() { isDev := os.Getenv("ENV") != "production" config := view.Config{ Extension: ".html", DefaultLayout: "default", Development: isDev, } if isDev { // Development: filesystem for hot reload config.Dir = "./views" } else { // Production: embedded with caching config.FS = viewsFS } engine := view.New(config) // ... rest of application } ``` ## Preloading Templates In production, preload all templates at startup to: 1. **Fail fast** - Catch template errors before serving requests 2. **Warm cache** - Avoid first-request latency 3. **Validate** - Ensure all templates parse correctly ```go theme={null} engine := view.New(view.Config{ FS: viewsFS, DefaultLayout: "default", }) // Preload and validate all templates if err := engine.Load(); err != nil { log.Fatal("Template error:", err) } app := mizu.New() app.Use(engine.Middleware()) ``` ### Preload Errors `Load()` catches errors like: * Missing template files * Template syntax errors * Invalid template references ```go theme={null} err := engine.Load() if err != nil { // err contains details about the failure log.Fatal(err) } ``` ## Performance Optimization ### Template Caching In production mode (`Development: false`), templates are cached: * **Parsed once** - Templates are parsed at first use or Load() * **Reused** - Same parsed template serves all requests * **No disk I/O** - Templates read from memory ```go theme={null} view.Config{ Development: false, // Enable caching (default) } ``` ### Pre-compute in Handlers Move complex logic to Go: ```go theme={null} // Good: compute once in handler func handler(c *mizu.Ctx) error { posts := fetchPosts() // Pre-compute derived data featured := filterFeatured(posts) byCategory := groupByCategory(posts) return view.Render(c, "posts", view.Data{ "Posts": posts, "Featured": featured, "ByCategory": byCategory, }) } ``` ```html theme={null} {{range .Data.Featured}}
    {{.Title}}
    {{end}} ``` ### Minimize Template Complexity Complex templates are slower. Keep them focused: ```html theme={null} {{if .Data.A}} {{if .Data.B}} {{if .Data.C}} ... {{end}} {{end}} {{end}} {{if .Data.ShowSpecialContent}} ... {{end}} ``` ## Error Handling ### Custom Error Pages Create error page templates: ```html theme={null}

    404

    The page you're looking for doesn't exist.

    Go Home
    ``` ```html theme={null}

    500

    Something went wrong on our end.

    Please try again later.

    ``` ### Error Handler Set up a global error handler: ```go theme={null} app := mizu.New() app.ErrorHandler = func(c *mizu.Ctx, err error) { code := 500 page := "500" // Check for HTTP errors var httpErr *mizu.Error if errors.As(err, &httpErr) { code = httpErr.Code if code == 404 { page = "404" } } // Log server errors if code >= 500 { log.Printf("Server error: %v", err) } // Render error page c.Status(code) if renderErr := view.Render(c, page, view.Data{ "Title": "Error", "Error": err, }); renderErr != nil { // Fallback if error page fails c.Text("An error occurred") } } ``` ### Template Error Handling Handle template rendering errors gracefully: ```go theme={null} func handler(c *mizu.Ctx) error { err := view.Render(c, "page", data) if err != nil { // Log the error log.Printf("Template error: %v", err) // Return generic error return c.Status(500).Text("Internal error") } return nil } ``` ## Security ### Content Security Policy Set appropriate headers: ```go theme={null} app.Use(func(next mizu.Handler) mizu.Handler { return func(c *mizu.Ctx) error { c.Set("Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'") return next(c) } }) ``` ### HTML Escaping Go templates escape HTML by default. Only bypass escaping for trusted content: ```html theme={null} {{.Data.UserComment}} {{.Data.TrustedHTML}} ``` ```go theme={null} import "html/template" view.Data{ "TrustedHTML": template.HTML(trustedContent), } ``` ### CSRF Tokens Include CSRF tokens in forms: ```html theme={null}
    ``` ## Monitoring ### Template Metrics Track template rendering performance: ```go theme={null} func handler(c *mizu.Ctx) error { start := time.Now() err := view.Render(c, "page", data) duration := time.Since(start) metrics.RecordTemplateRender("page", duration) return err } ``` ### Health Checks Verify templates work in health checks: ```go theme={null} app.Get("/health", func(c *mizu.Ctx) error { // Try rendering a simple template engine := view.From(c) var buf bytes.Buffer err := engine.Render(&buf, "health-check", nil, view.NoLayout()) if err != nil { return c.Status(500).JSON(map[string]string{ "status": "unhealthy", "error": err.Error(), }) } return c.JSON(map[string]string{ "status": "healthy", }) }) ``` ## Deployment Checklist ### Before Deploying * [ ] Set `Development: false` * [ ] Use `embed.FS` for templates * [ ] Call `Load()` at startup * [ ] Create error page templates * [ ] Set up error handler * [ ] Test all pages render correctly ### Environment Variables ```bash theme={null} # Production ENV=production PORT=8080 # Development ENV=development PORT=8080 ``` ### Docker Example ```dockerfile theme={null} FROM golang:1.22-alpine AS builder WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -o server . FROM alpine:latest WORKDIR /app COPY --from=builder /app/server . # Templates are embedded - no need to copy views/ ENV ENV=production EXPOSE 8080 CMD ["./server"] ``` ## Complete Production Setup ```go theme={null} package main import ( "embed" "errors" "log" "os" "github.com/go-mizu/mizu" "github.com/go-mizu/mizu/view" ) //go:embed views var viewsFS embed.FS func main() { // Configuration port := os.Getenv("PORT") if port == "" { port = "8080" } isDev := os.Getenv("ENV") != "production" // Create view engine config := view.Config{ Extension: ".html", DefaultLayout: "default", Development: isDev, } if isDev { config.Dir = "./views" } else { config.FS = viewsFS } engine := view.New(config) // Preload in production if !isDev { if err := engine.Load(); err != nil { log.Fatal("Template error:", err) } log.Println("Templates loaded successfully") } // Create app app := mizu.New() app.Use(engine.Middleware()) // Error handler app.ErrorHandler = func(c *mizu.Ctx, err error) { code := 500 page := "500" var httpErr *mizu.Error if errors.As(err, &httpErr) { code = httpErr.Code if code == 404 { page = "404" } } if code >= 500 { log.Printf("Error: %v", err) } c.Status(code) view.Render(c, page, view.Data{"Title": "Error"}) } // Routes app.Get("/", homeHandler) app.Get("/about", aboutHandler) // Start server log.Printf("Server starting on :%s (dev=%v)", port, isDev) if err := app.Listen(":" + port); err != nil { log.Fatal(err) } } func homeHandler(c *mizu.Ctx) error { return view.Render(c, "home", view.Data{ "Title": "Welcome", }) } func aboutHandler(c *mizu.Ctx) error { return view.Render(c, "about", view.Data{ "Title": "About Us", }) } ``` ## What's Next? Now that your views are ready for production, explore: * **[Sync Overview](/view/sync-overview)** - Client-side state synchronization * **[Live Overview](/view/live-overview)** - Real-time WebSocket communication # Quick Start Source: https://docs.go-mizu.dev/view/quick-start Create your first dynamic web page with Mizu's view engine in 5 minutes In this guide, you'll create a simple web page with a layout and dynamic content. By the end, you'll understand the core concepts of Mizu's view system. ## Prerequisites * Go 1.22 or later installed * Basic understanding of Go and HTML * A text editor ## Step 1: Create Your Project First, create a new directory and initialize a Go module: ```bash theme={null} mkdir myapp && cd myapp go mod init myapp go get github.com/go-mizu/mizu ``` ## Step 2: Create the Directory Structure The view package expects templates in a specific structure: ```bash theme={null} mkdir -p views/layouts views/pages ``` Your project should look like this: ``` myapp/ ├── go.mod ├── go.sum ├── main.go (we'll create this) └── views/ ├── layouts/ │ └── default.html └── pages/ ├── home.html └── about.html ``` ## Step 3: Create the Layout A layout is the HTML shell that wraps your pages. Create `views/layouts/default.html`: ```html theme={null} {{.Data.Title}}
    {{.Content}}
    Built with Mizu
    ``` **What's happening here?** * `{{.Data.Title}}` - Accesses the "Title" field from data you pass * `{{.Content}}` - This is where the page content gets inserted * The layout provides the common structure (doctype, head, nav, footer) ## Step 4: Create Pages Pages are templates that provide the main content. Create `views/pages/home.html`: ```html theme={null}

    Hello, {{.Data.Name}}!

    Welcome to our website. We're glad you're here.

    What We Offer

      {{range .Data.Features}}
    • {{.}}
    • {{end}}
    ``` **What's happening here?** * `{{.Data.Name}}` - Accesses data passed from your handler * `{{range .Data.Features}}...{{end}}` - Loops through a list * `{{.}}` - Inside range, this is the current item * Page content is automatically inserted into the layout's `{{.Content}}` Create `views/pages/about.html`: ```html theme={null}

    About Us

    We are a team passionate about building great web applications with Go.

    Our Mission

    To make server-side rendering in Go simple, fast, and enjoyable.

    ``` ## Step 5: Create the Go Application Now create `main.go`: ```go theme={null} package main import ( "github.com/go-mizu/mizu" "github.com/go-mizu/mizu/view" ) func main() { // Step 1: Create the view engine engine := view.New(view.Config{ Dir: "./views", // Where templates are stored Extension: ".html", // File extension to look for Development: true, // Enable hot reload }) // Step 2: Create the Mizu app app := mizu.New() // Step 3: Add the view middleware // This makes the engine available to all handlers app.Use(engine.Middleware()) // Step 4: Define routes app.Get("/", homeHandler) app.Get("/about", aboutHandler) // Step 5: Start the server app.Listen(":8080") } func homeHandler(c *mizu.Ctx) error { return view.Render(c, "home", view.Data{ "Title": "Home - My App", "Name": "Friend", "Features": []string{ "Fast server-side rendering", "Layout support", "Development hot reload", "Production caching", }, }) } func aboutHandler(c *mizu.Ctx) error { return view.Render(c, "about", view.Data{ "Title": "About - My App", }) } ``` **What's happening here?** 1. **Create engine** - `view.New()` creates a view engine with your configuration 2. **Add middleware** - `app.Use(engine.Middleware())` stores the engine in every request context 3. **Render pages** - `view.Render(c, "home", data)` renders the "home" page with data ## Step 6: Run Your Application Start the server: ```bash theme={null} go run main.go ``` Now open your browser to `http://localhost:8080`. You should see: * The home page with your name and features list * A styled button * Navigation to the About page Try editing `views/pages/home.html` and refreshing your browser - the changes appear instantly because Development mode is enabled! ## Understanding the Data Flow Here's what happens when someone visits your site: ``` 1. Browser requests http://localhost:8080/ | v 2. Mizu routes to homeHandler | v 3. Handler calls view.Render(c, "home", data) | v 4. View engine finds views/pages/home.html | v 5. Engine also loads views/layouts/default.html | v 6. Page is rendered, output goes into layout's {{.Content}} | v 7. Final HTML sent to browser ``` ## Understanding Template Data Templates receive a wrapper structure, not just your data directly: ```go theme={null} // In your handler: view.Render(c, "home", view.Data{ "Title": "Home", "User": user, }) ``` ```html theme={null}

    {{.Data.Title}}

    {{.Data.User.Name}}

    ``` ## Common Patterns ### Using a Different Layout Override the default layout for specific pages: ```go theme={null} view.Render(c, "dashboard", data, view.Layout("admin")) ``` This uses `views/layouts/admin.html` instead of `default.html`. ### Rendering Without a Layout For partial HTML responses (AJAX, htmx, etc.): ```go theme={null} view.Render(c, "user-row", data, view.NoLayout()) ``` ### Using the Engine Directly If you need the engine instance: ```go theme={null} func handler(c *mizu.Ctx) error { engine := view.From(c) return engine.Render(c.Writer(), "home", data) } ``` ## Next Steps You've learned the basics! Here's what to explore next: * **[Engine](/view/engine)** - All configuration options explained * **[Templates](/view/templates)** - Deep dive into Go template syntax * **[Layouts](/view/layouts)** - Advanced layout patterns * **[Functions](/view/functions)** - Built-in and custom template functions * **[Production](/view/production)** - Embedding templates and caching for production ## Troubleshooting ### "template not found" Error Check that: 1. Your `views` directory exists with `layouts/` and `pages/` subdirectories 2. The file extension matches your Config (default is `.html`) 3. The name in `Render()` matches the filename in `pages/` (without extension) ### Changes Not Appearing Make sure `Development: true` is set in your view config. In production mode (the default), templates are cached. ### "layout not found" Error Make sure the layout file exists at `views/layouts/default.html` (or your custom `DefaultLayout` value). # Client Source: https://docs.go-mizu.dev/view/sync-client Client-side sync runtime for offline-first applications The sync client is a Go runtime for building offline-first applications. It manages local state, queues mutations, and synchronizes with the server. This guide covers client configuration and usage. The client runtime is designed for Go applications (mobile, desktop, CLI). For browser applications, you'll typically implement a JavaScript client that uses the same sync protocol. ## Creating a Client ```go theme={null} import "github.com/go-mizu/mizu/view/sync" client := sync.New(sync.Options{ BaseURL: "https://api.example.com/_sync", Scope: "user:123", }) ``` ## Configuration Options ### BaseURL (Required) The sync server endpoint: ```go theme={null} sync.Options{ BaseURL: "https://api.example.com/_sync", } ``` ### Scope (Required) The data partition to sync: ```go theme={null} sync.Options{ Scope: "user:123", // This client syncs user 123's data } ``` ### HTTP Custom HTTP client for requests: ```go theme={null} sync.Options{ HTTP: &http.Client{ Timeout: 30 * time.Second, Transport: &http.Transport{ // Custom transport settings }, }, } ``` ### Persistence Save state between app restarts: ```go theme={null} type FilePersistence struct { path string } func (p *FilePersistence) Save(cursor uint64, clientID string, queue []sync.Mutation, store map[string]map[string][]byte) error { data := map[string]any{ "cursor": cursor, "clientID": clientID, "queue": queue, "store": store, } bytes, _ := json.Marshal(data) return os.WriteFile(p.path, bytes, 0644) } func (p *FilePersistence) Load() (uint64, string, []sync.Mutation, map[string]map[string][]byte, error) { bytes, err := os.ReadFile(p.path) if err != nil { return 0, "", nil, nil, nil // Fresh start } var data map[string]any json.Unmarshal(bytes, &data) // Extract and return values... } // Usage sync.Options{ Persistence: &FilePersistence{path: "sync-state.json"}, } ``` ### Callbacks React to sync events: ```go theme={null} sync.Options{ OnError: func(err error) { log.Printf("Sync error: %v", err) }, OnSync: func(cursor uint64) { log.Printf("Synced to cursor %d", cursor) }, OnOnline: func() { log.Println("Back online!") }, OnOffline: func() { log.Println("Gone offline") }, } ``` ### Intervals Control sync timing: ```go theme={null} sync.Options{ PushInterval: 1 * time.Second, // Push mutations every second PullInterval: 30 * time.Second, // Poll for changes every 30s } ``` ## Client Lifecycle ### Starting the Client ```go theme={null} ctx := context.Background() client := sync.New(opts) if err := client.Start(ctx); err != nil { log.Fatal(err) } ``` `Start` does: 1. Loads persisted state (if configured) 2. Performs initial sync (snapshot or pull) 3. Starts background push/pull loops ### Stopping the Client ```go theme={null} client.Stop() ``` `Stop` does: 1. Cancels background goroutines 2. Saves state (if persistence configured) ### Manual Sync Force an immediate sync: ```go theme={null} if err := client.Sync(); err != nil { log.Printf("Sync failed: %v", err) } ``` ## Mutations ### Queueing Mutations Use `Mutate` to queue changes: ```go theme={null} // Queue a mutation client.Mutate("todo.create", map[string]any{ "id": "todo-123", "title": "Buy milk", }) // The mutation is: // 1. Added to the queue // 2. Pushed to server in background // 3. Removed from queue on success ``` ### Mutation Flow ``` client.Mutate() │ ▼ ┌─────────────┐ │ Local Queue │ ◀──── Stored for offline support └─────────────┘ │ ▼ (background) ┌─────────────┐ │ Push to │ │ Server │ └─────────────┘ │ ▼ ┌─────────────┐ │ On success: │ │ Remove from │ │ queue │ └─────────────┘ ``` ## Status ### Online Status ```go theme={null} if client.IsOnline() { fmt.Println("Connected to server") } else { fmt.Println("Working offline") } ``` ### Current Cursor ```go theme={null} cursor := client.Cursor() fmt.Printf("Synced to cursor %d\n", cursor) ``` ## Live Integration For real-time updates, integrate with the live package: ```go theme={null} // When live receives sync notification liveClient.OnMessage(func(msg []byte) { var data struct { Cursor uint64 `json:"cursor"` } json.Unmarshal(msg, &data) // Notify sync client client.NotifyLive(data.Cursor) }) ``` This triggers an immediate pull instead of waiting for the next poll interval. ## Complete Example ```go theme={null} package main import ( "context" "fmt" "log" "os" "os/signal" "syscall" "time" "github.com/go-mizu/mizu/view/sync" ) func main() { // Create client client := sync.New(sync.Options{ BaseURL: "http://localhost:8080/_sync", Scope: "user:demo", PushInterval: time.Second, PullInterval: 5 * time.Second, OnError: func(err error) { log.Printf("Error: %v", err) }, OnSync: func(cursor uint64) { log.Printf("Synced to %d", cursor) }, OnOnline: func() { log.Println("Online") }, OnOffline: func() { log.Println("Offline") }, }) // Start sync ctx, cancel := context.WithCancel(context.Background()) if err := client.Start(ctx); err != nil { log.Printf("Initial sync failed: %v", err) } // Create a todo client.Mutate("todo.create", map[string]any{ "id": fmt.Sprintf("todo-%d", time.Now().UnixNano()), "title": "Test todo", }) // Wait for shutdown quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit cancel() client.Stop() log.Println("Shutdown complete") } ``` ## Error Handling ### Sync Errors ```go theme={null} sync.Options{ OnError: func(err error) { switch { case errors.Is(err, sync.ErrNotStarted): log.Println("Client not started") case errors.Is(err, context.Canceled): log.Println("Operation canceled") default: log.Printf("Sync error: %v", err) } }, } ``` ### Start Errors ```go theme={null} err := client.Start(ctx) if err != nil { // Initial sync failed // Client is still running, will retry log.Printf("Initial sync failed: %v", err) } ``` ### Double Start ```go theme={null} err := client.Start(ctx) if errors.Is(err, sync.ErrAlreadyStarted) { log.Println("Client already running") } ``` ## Best Practices ### 1. Always Handle Errors ```go theme={null} sync.Options{ OnError: func(err error) { // Log for debugging log.Printf("Sync error: %v", err) // Update UI showSyncError(err) }, } ``` ### 2. Use Persistence For a good offline experience, persist state: ```go theme={null} sync.Options{ Persistence: &MyPersistence{}, } ``` ### 3. Start Early Start the client as early as possible: ```go theme={null} func main() { client := sync.New(opts) client.Start(ctx) // Start sync immediately // Initialize rest of app... } ``` ### 4. Handle Offline Gracefully ```go theme={null} func addTodo(client *sync.Client, title string) { // This works offline! client.Mutate("todo.create", map[string]any{ "id": generateID(), "title": title, }) // Update UI immediately (optimistic) updateTodoList() if !client.IsOnline() { showMessage("Saved offline - will sync when online") } } ``` ### 5. Integrate with Live For real-time updates: ```go theme={null} // Subscribe to sync notifications liveSession.Subscribe("sync:" + scope) // Handle notifications liveSession.OnMessage(func(msg []byte) { var notification struct { Cursor uint64 `json:"cursor"` } if json.Unmarshal(msg, ¬ification) == nil { client.NotifyLive(notification.Cursor) } }) ``` # Collections Source: https://docs.go-mizu.dev/view/sync-collections Type-safe entity management with Collections and Entities Collections provide type-safe management of synchronized entities. They wrap the sync client's store with a convenient API for creating, reading, updating, and deleting records. ## Overview ```go theme={null} // Define your type type Todo struct { ID string `json:"id"` Title string `json:"title"` Completed bool `json:"completed"` CreatedAt time.Time `json:"created_at"` } // Create a collection client := sync.New(opts) todos := sync.NewCollection[Todo](client, "todo") // Use it todo := todos.Create("abc", Todo{Title: "Buy milk"}) all := todos.All() found := todos.Find(func(t Todo) bool { return t.Completed }) ``` ## Creating a Collection ```go theme={null} import "github.com/go-mizu/mizu/view/sync" // Type parameter is your entity type // Second argument is the entity name (matches server) todos := sync.NewCollection[Todo](client, "todo") users := sync.NewCollection[User](client, "user") settings := sync.NewCollection[Settings](client, "settings") ``` The entity name should match what your server-side Mutator uses. ## Collection Methods ### Create Creates a new entity: ```go theme={null} todo := todos.Create("todo-123", Todo{ Title: "Buy groceries", Completed: false, }) ``` This: 1. Stores the entity locally (immediate) 2. Queues a mutation (background sync) 3. Returns an Entity reference ### Get Gets an entity reference by ID: ```go theme={null} todo := todos.Get("todo-123") ``` This returns an Entity even if it doesn't exist yet. Use `Exists()` to check. ### All Returns all entities in the collection (reactive): ```go theme={null} allTodos := todos.All() for _, entity := range allTodos { todo := entity.Get() fmt.Printf("%s: %s\n", todo.ID, todo.Title) } ``` ### Count Returns the number of entities (reactive): ```go theme={null} count := todos.Count() fmt.Printf("%d todos\n", count) ``` ### Find Finds entities matching a predicate: ```go theme={null} completed := todos.Find(func(t Todo) bool { return t.Completed }) urgent := todos.Find(func(t Todo) bool { return strings.Contains(t.Title, "URGENT") }) ``` ### Has Checks if an entity exists: ```go theme={null} if todos.Has("todo-123") { fmt.Println("Todo exists") } ``` ## Entity Methods An Entity is a reference to a single record. ### ID Returns the entity's unique identifier: ```go theme={null} entity := todos.Get("todo-123") fmt.Println(entity.ID()) // "todo-123" ``` ### Get Returns the current value (reactive): ```go theme={null} entity := todos.Get("todo-123") todo := entity.Get() fmt.Println(todo.Title) fmt.Println(todo.Completed) ``` ### Set Updates the entity value: ```go theme={null} entity := todos.Get("todo-123") // Get current value todo := entity.Get() // Modify todo.Title = "Updated title" todo.Completed = true // Save entity.Set(todo) ``` This: 1. Updates local store (immediate) 2. Queues an update mutation (background sync) ### Delete Removes the entity: ```go theme={null} entity := todos.Get("todo-123") entity.Delete() ``` This: 1. Removes from local store (immediate) 2. Queues a delete mutation (background sync) ### Exists Checks if the entity exists: ```go theme={null} entity := todos.Get("maybe-exists") if entity.Exists() { todo := entity.Get() fmt.Println(todo.Title) } else { fmt.Println("Not found") } ``` ## Reactive Usage Collections and entities are reactive. Use them with Computed and Effect: ### Computed Example ```go theme={null} todos := sync.NewCollection[Todo](client, "todo") // Computed value updates when todos change completedCount := sync.NewComputed(func() int { count := 0 for _, entity := range todos.All() { if entity.Get().Completed { count++ } } return count }) // Or using Find completedTodos := sync.NewComputed(func() []*sync.Entity[Todo] { return todos.Find(func(t Todo) bool { return t.Completed }) }) ``` ### Effect Example ```go theme={null} // Log when todos change sync.NewEffect(func() { all := todos.All() fmt.Printf("Todos updated: %d items\n", len(all)) }) // Update UI when specific todo changes sync.NewEffect(func() { entity := todos.Get("important-todo") if entity.Exists() { todo := entity.Get() updateTodoUI(todo) } }) ``` ## Complete Example: Todo App ```go theme={null} package main import ( "context" "fmt" "time" "github.com/go-mizu/mizu/view/sync" ) type Todo struct { ID string `json:"id"` Title string `json:"title"` Completed bool `json:"completed"` CreatedAt time.Time `json:"created_at"` } func main() { // Create client and collection client := sync.New(sync.Options{ BaseURL: "http://localhost:8080/_sync", Scope: "user:demo", }) todos := sync.NewCollection[Todo](client, "todo") // Start sync ctx := context.Background() client.Start(ctx) // Reactive stats stats := sync.NewComputed(func() string { all := todos.All() completed := 0 for _, e := range all { if e.Get().Completed { completed++ } } return fmt.Sprintf("%d/%d completed", completed, len(all)) }) // Log changes sync.NewEffect(func() { fmt.Println("Stats:", stats.Get()) }) // Create todos todos.Create("1", Todo{Title: "Learn Go", CreatedAt: time.Now()}) todos.Create("2", Todo{Title: "Build app", CreatedAt: time.Now()}) todos.Create("3", Todo{Title: "Deploy", CreatedAt: time.Now()}) // Complete one entity := todos.Get("1") todo := entity.Get() todo.Completed = true entity.Set(todo) // Find incomplete incomplete := todos.Find(func(t Todo) bool { return !t.Completed }) fmt.Printf("\nIncomplete todos:\n") for _, e := range incomplete { fmt.Printf("- %s\n", e.Get().Title) } // Delete one todos.Get("3").Delete() // Keep running... select {} } ``` ## Patterns ### CRUD Operations ```go theme={null} // Create entity := collection.Create(id, value) // Read entity := collection.Get(id) if entity.Exists() { value := entity.Get() } // Update entity := collection.Get(id) value := entity.Get() value.Field = newValue entity.Set(value) // Delete collection.Get(id).Delete() ``` ### List with Filter ```go theme={null} filter := sync.NewSignal("") filteredTodos := sync.NewComputed(func() []*sync.Entity[Todo] { f := strings.ToLower(filter.Get()) if f == "" { return todos.All() } return todos.Find(func(t Todo) bool { return strings.Contains(strings.ToLower(t.Title), f) }) }) ``` ### Sorted List ```go theme={null} sortBy := sync.NewSignal("created_at") sortedTodos := sync.NewComputed(func() []*sync.Entity[Todo] { all := todos.All() // Convert to slice for sorting items := make([]Todo, len(all)) for i, e := range all { items[i] = e.Get() } // Sort switch sortBy.Get() { case "title": sort.Slice(items, func(i, j int) bool { return items[i].Title < items[j].Title }) case "created_at": sort.Slice(items, func(i, j int) bool { return items[i].CreatedAt.Before(items[j].CreatedAt) }) } // Convert back to entities result := make([]*sync.Entity[Todo], len(items)) for i, item := range items { result[i] = todos.Get(item.ID) } return result }) ``` ### Pagination ```go theme={null} page := sync.NewSignal(0) pageSize := 10 paginatedTodos := sync.NewComputed(func() []*sync.Entity[Todo] { all := todos.All() start := page.Get() * pageSize end := start + pageSize if start >= len(all) { return nil } if end > len(all) { end = len(all) } return all[start:end] }) totalPages := sync.NewComputed(func() int { return (todos.Count() + pageSize - 1) / pageSize }) ``` ### Multiple Collections ```go theme={null} projects := sync.NewCollection[Project](client, "project") tasks := sync.NewCollection[Task](client, "task") // Get tasks for a project projectTasks := func(projectID string) []*sync.Entity[Task] { return tasks.Find(func(t Task) bool { return t.ProjectID == projectID }) } // Reactive task count per project projectTaskCount := sync.NewComputed(func() map[string]int { counts := make(map[string]int) for _, entity := range tasks.All() { task := entity.Get() counts[task.ProjectID]++ } return counts }) ``` ## Best Practices ### 1. Use Meaningful IDs ```go theme={null} // Good: meaningful, unique ID todos.Create("user-123-todo-456", todo) // Bad: just a random string todos.Create("xyz", todo) ``` ### 2. Keep Entities Small ```go theme={null} // Good: focused entity type Todo struct { ID string Title string Completed bool } // Bad: entity with embedded large data type Todo struct { ID string Title string Attachments [][]byte // Large data } ``` ### 3. Check Existence ```go theme={null} entity := collection.Get(id) // Always check before accessing if entity.Exists() { value := entity.Get() // Use value } ``` ### 4. Use Computed for Derived Data ```go theme={null} // Good: computed derives from collection taskCount := sync.NewComputed(func() int { return tasks.Count() }) // Bad: manually tracking taskCount := sync.NewSignal(0) sync.NewEffect(func() { taskCount.Set(len(tasks.All())) // Manual sync }) ``` ### 5. Batch Related Operations ```go theme={null} // If creating multiple related entities, // they'll be queued and synced together project := projects.Create(projectID, Project{Name: "New Project"}) tasks.Create(task1ID, Task{ProjectID: projectID, Title: "Task 1"}) tasks.Create(task2ID, Task{ProjectID: projectID, Title: "Task 2"}) // All mutations sync in order ``` # Integration Source: https://docs.go-mizu.dev/view/sync-integration Combine sync with live for real-time state synchronization By default, sync clients poll the server for changes. By integrating with the `live` package, you can push notifications to clients for instant updates. ## Why Integrate? | Without Live | With Live | | ---------------------- | ---------------------------- | | Clients poll every 30s | Clients sync instantly | | Higher latency | Sub-second latency | | More bandwidth | Efficient push notifications | | Simpler setup | Requires WebSocket | ## Architecture ``` ┌─────────────────────────────────────────────────────────────────┐ │ SERVER │ │ │ │ ┌────────────────┐ ┌────────────────┐ │ │ │ Sync Engine │───notify──▶│ Live Server │ │ │ │ │ │ │ │ │ │ Push/Pull/ │ │ Publish to │ │ │ │ Snapshot │ │ sync topics │ │ │ └────────────────┘ └───────┬────────┘ │ │ │ │ │ WebSocket│ │ └────────────────────────────────────────│────────────────────────┘ │ ┌──────────┴──────────┐ │ │ ▼ ▼ ┌────────────────┐ ┌────────────────┐ │ Client A │ │ Client B │ │ │ │ │ │ On notify: │ │ On notify: │ │ pull changes │ │ pull changes │ └────────────────┘ └────────────────┘ ``` ## Server Setup ### Step 1: Create Both Servers ```go theme={null} package main import ( "context" "encoding/json" "fmt" "net/http" "github.com/go-mizu/mizu" "github.com/go-mizu/mizu/live" "github.com/go-mizu/mizu/sync" "github.com/go-mizu/mizu/sync/memory" ) func main() { // Create Live server liveServer := live.New(live.Options{ OnAuth: func(ctx context.Context, r *http.Request) (any, error) { scope := r.URL.Query().Get("scope") if scope == "" { return nil, fmt.Errorf("scope required") } return scope, nil }, OnMessage: func(ctx context.Context, s *live.Session, topic string, data []byte) { // Handle subscribe requests if topic == "subscribe" { scope := s.Value().(string) liveServer.Subscribe(s, "sync:"+scope) } }, }) // Create Sync engine changeLog := memory.NewLog() syncEngine := sync.New(sync.Options{ Log: changeLog, Dedupe: memory.NewDedupe(), Apply: applyMutation, }) // Mount on app app := mizu.New() // Sync endpoints app.Post("/sync/push", pushHandler(syncEngine, liveServer)) app.Get("/sync/pull", pullHandler(syncEngine)) app.Get("/sync/snapshot", snapshotHandler(syncEngine)) // WebSocket endpoint app.Get("/ws", mizu.Compat(liveServer.Handler())) app.Listen(":8080") } ``` ### Step 2: Notify on Push ```go theme={null} func pushHandler(engine *sync.Engine, live *live.Server) mizu.Handler { return func(c *mizu.Ctx) error { var mutations []sync.Mutation if err := c.BodyJSON(&mutations); err != nil { return c.Status(400).JSON(map[string]string{"error": "invalid"}) } results, err := engine.Push(c.Context(), mutations) if err != nil { return c.Status(500).JSON(map[string]string{"error": err.Error()}) } // Notify clients via Live for _, r := range results { if r.OK && len(r.Changes) > 0 { for _, change := range r.Changes { // Publish to scope's sync topic msg, _ := json.Marshal(map[string]uint64{"cursor": r.Cursor}) live.Publish("sync:"+change.Scope, msg) } } } return c.JSON(results) } } ``` ## Client Setup (JavaScript) ```javascript theme={null} class SyncClient { constructor(baseURL, scope) { this.baseURL = baseURL; this.scope = scope; this.cursor = 0; this.data = {}; this.ws = null; } // Connect to Live for instant notifications connect() { this.ws = new WebSocket(`ws://${location.host}/ws?scope=${this.scope}`); this.ws.onopen = () => { console.log('Live connected'); // Subscribe to sync notifications this.ws.send(JSON.stringify({topic: 'subscribe', data: {}})); }; this.ws.onmessage = (event) => { const msg = JSON.parse(event.data); if (msg.topic === 'sync:' + this.scope) { const data = JSON.parse(msg.data); if (data.cursor > this.cursor) { this.pull(); // Immediately sync! } } }; this.ws.onclose = () => { console.log('Disconnected, reconnecting...'); setTimeout(() => this.connect(), 1000); }; } // Pull changes from server async pull() { const response = await fetch( `${this.baseURL}/sync/pull?scope=${this.scope}&cursor=${this.cursor}` ); const result = await response.json(); for (const change of result.changes) { this.applyChange(change); this.cursor = Math.max(this.cursor, change.cursor); } if (result.hasMore) { await this.pull(); } } // Push mutation to server async push(name, args) { const mutation = { id: crypto.randomUUID(), scope: this.scope, name: name, args: args }; const response = await fetch(`${this.baseURL}/sync/push`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify([mutation]) }); return response.json(); } applyChange(change) { const data = JSON.parse(change.data); // Update local state based on change console.log('Applied change:', data); } } // Usage const client = new SyncClient('http://localhost:8080', 'user:123'); client.connect(); await client.pull(); // Initial sync ``` ## Complete Example ### Server ```go theme={null} package main import ( "context" "encoding/json" "fmt" "log" "net/http" gosync "sync" "github.com/go-mizu/mizu" "github.com/go-mizu/mizu/live" "github.com/go-mizu/mizu/sync" "github.com/go-mizu/mizu/sync/memory" ) type Todo struct { ID string `json:"id"` Title string `json:"title"` Completed bool `json:"completed"` } var ( todos = make(map[string]Todo) todosMu gosync.RWMutex ) func main() { // Live server liveServer := live.New(live.Options{ OnAuth: func(ctx context.Context, r *http.Request) (any, error) { scope := r.URL.Query().Get("scope") if scope == "" { return nil, fmt.Errorf("scope required") } return scope, nil }, OnMessage: func(ctx context.Context, s *live.Session, topic string, data []byte) { if topic == "subscribe" { scope := s.Value().(string) liveServer.Subscribe(s, "sync:"+scope) } }, OnClose: func(s *live.Session, err error) { log.Printf("Client disconnected: %v", err) }, }) // Sync engine changeLog := memory.NewLog() syncEngine := sync.New(sync.Options{ Log: changeLog, Dedupe: memory.NewDedupe(), Apply: applyMutation, Snapshot: func(ctx context.Context, scope string) (json.RawMessage, uint64, error) { todosMu.RLock() defer todosMu.RUnlock() data, _ := json.Marshal(todos) cursor, _ := changeLog.Cursor(ctx, scope) return data, cursor, nil }, }) // App app := mizu.New() app.Post("/sync/push", func(c *mizu.Ctx) error { var mutations []sync.Mutation c.BodyJSON(&mutations) results, _ := syncEngine.Push(c.Context(), mutations) // Notify via Live for _, r := range results { if r.OK { for _, ch := range r.Changes { msg, _ := json.Marshal(map[string]uint64{"cursor": r.Cursor}) liveServer.Publish("sync:"+ch.Scope, msg) } } } return c.JSON(results) }) app.Get("/sync/pull", func(c *mizu.Ctx) error { scope := c.Query("scope", sync.DefaultScope) cursor := uint64(c.QueryInt("cursor", 0)) changes, hasMore, _ := syncEngine.Pull(c.Context(), scope, cursor, 100) return c.JSON(map[string]any{"changes": changes, "hasMore": hasMore}) }) app.Get("/sync/snapshot", func(c *mizu.Ctx) error { scope := c.Query("scope", sync.DefaultScope) data, cursor, _ := syncEngine.Snapshot(c.Context(), scope) return c.JSON(map[string]any{"data": json.RawMessage(data), "cursor": cursor}) }) app.Get("/ws", mizu.Compat(liveServer.Handler())) log.Println("Server on :8080") app.Listen(":8080") } func applyMutation(ctx context.Context, m sync.Mutation) ([]sync.Change, error) { scope := m.Scope if scope == "" { scope = sync.DefaultScope } switch m.Name { case "todo.create": var args struct { ID string `json:"id"` Title string `json:"title"` } json.Unmarshal(m.Args, &args) todo := Todo{ID: args.ID, Title: args.Title} todosMu.Lock() todos[args.ID] = todo todosMu.Unlock() data, _ := json.Marshal(map[string]any{"op": "create", "todo": todo}) return []sync.Change{{Scope: scope, Data: data}}, nil default: return nil, sync.ErrInvalidMutation } } ``` ### Client HTML ```html theme={null} Real-time Sync Demo

    Todos

      ``` ## Best Practices ### 1. Use Live as Accelerator, Not Requirement Sync should work without Live via polling: ```javascript theme={null} // Fallback polling when WebSocket fails setInterval(() => { if (!ws || ws.readyState !== WebSocket.OPEN) { pull(); } }, 30000); ``` ### 2. Match Scopes to Topics ```go theme={null} // Sync scope scope := "user:" + userID // Live topic liveServer.Publish("sync:" + scope, msg) ``` ### 3. Deduplicate Client-Side ```javascript theme={null} ws.onmessage = (e) => { const {cursor} = JSON.parse(e.data); if (cursor > this.cursor) { this.pull(); // Only pull if new data } }; ``` # Overview Source: https://docs.go-mizu.dev/view/sync-overview Client-side reactive state management with offline-first synchronization The `sync` package provides a client-side runtime for building offline-first applications. It combines reactive state primitives (Signals, Computed values, Effects) with synchronized collections that stay in sync with a server. ## What is Offline-First? In a traditional web application, the server is the source of truth. When you're offline, the app doesn't work. ``` Traditional App: ┌─────────┐ Request ┌─────────┐ │ Client │ ────────────> │ Server │ │ │ <──────────── │ │ └─────────┘ Response └─────────┘ If server is unreachable = App broken ``` In an **offline-first** application, the client keeps a local copy of the data. Changes are queued locally and synced when connectivity is available. ``` Offline-First App: ┌─────────────────────────────────────┐ │ Client │ │ ┌─────────────┐ ┌──────────────┐ │ │ │ Local State │ │ Mutation │ │ │ │ │ │ Queue │ │ │ └─────────────┘ └──────────────┘ │ │ │ │ │ │ │ │ │ └─────────│────────────────│──────────┘ │ Background │ ▼ Sync ▼ ┌─────────┐ │ Server │ └─────────┘ If server is unreachable = App still works! ``` **Benefits:** * App works without internet * Instant UI updates (no loading spinners) * Better user experience on slow connections * Resilience to network failures ## What the Sync Package Provides ### Reactive Primitives | Type | Description | | ------------- | ------------------------------------------------ | | `Signal[T]` | A reactive value container | | `Computed[T]` | A derived value that updates automatically | | `Effect` | A side effect that runs when dependencies change | These provide fine-grained reactivity similar to SolidJS or Vue's Composition API. ### Synchronized State | Type | Description | | --------------- | --------------------------------------------------------- | | `Client` | Manages server sync, mutation queue, and connection state | | `Collection[T]` | A reactive set of entities that syncs with the server | | `Entity[T]` | A single synchronized record | ### The Sync Protocol The client and server communicate using a simple protocol: ``` ┌────────────┐ ┌────────────┐ │ Client │ │ Server │ └────────────┘ └────────────┘ │ │ │──── Push(mutations) ─────────────>│ │ │ │<─── Pull(cursor) ─────────────────│ │ Returns changes since cursor │ │ │ │<─── Snapshot(scope) ──────────────│ │ Returns full state │ │ │ ``` * **Push**: Client sends queued mutations to server * **Pull**: Client requests changes since last sync point (cursor) * **Snapshot**: Client requests full state (initial load or resync) ## Reactive Primitives ### Signal A Signal holds a value and notifies subscribers when it changes: ```go theme={null} import "github.com/go-mizu/mizu/view/sync" // Create a signal with initial value count := sync.NewSignal(0) // Read the value fmt.Println(count.Get()) // 0 // Update the value count.Set(5) fmt.Println(count.Get()) // 5 // Update with a function count.Update(func(v int) int { return v + 1 }) fmt.Println(count.Get()) // 6 ``` ### Computed A Computed value derives from other signals and updates automatically: ```go theme={null} count := sync.NewSignal(10) price := sync.NewSignal(5.0) // Automatically tracks dependencies total := sync.NewComputed(func() float64 { return float64(count.Get()) * price.Get() }) fmt.Println(total.Get()) // 50.0 count.Set(20) fmt.Println(total.Get()) // 100.0 (auto-updated!) ``` ### Effect An Effect runs a function whenever its dependencies change: ```go theme={null} count := sync.NewSignal(0) effect := sync.NewEffect(func() { fmt.Printf("Count is now: %d\n", count.Get()) }) // Prints: "Count is now: 0" count.Set(1) // Prints: "Count is now: 1" count.Set(2) // Prints: "Count is now: 2" // Stop the effect effect.Stop() ``` ## Collections and Entities Collections manage groups of entities that sync with a server: ```go theme={null} // Define your data type type Todo struct { Title string `json:"title"` Completed bool `json:"completed"` } // Create a client client := sync.New(sync.Options{ BaseURL: "http://localhost:8080/_sync", Scope: "user:123", }) // Create a collection todos := sync.NewCollection[Todo](client, "todo") // Create an entity todo := todos.Create("todo-1", Todo{ Title: "Buy groceries", Completed: false, }) // Read (reactive - tracks dependencies) fmt.Println(todo.Get().Title) // Update todo.Set(Todo{ Title: "Buy groceries", Completed: true, }) // Delete todo.Delete() // Query the collection all := todos.All() // All entities (reactive) count := todos.Count() // Entity count (reactive) found := todos.Find(func(t Todo) bool { return !t.Completed }) ``` ## Quick Example Here's a complete example showing the sync system: ```go theme={null} package main import ( "context" "fmt" "time" "github.com/go-mizu/mizu/view/sync" ) type Todo struct { Title string `json:"title"` Completed bool `json:"completed"` } func main() { // Create client client := sync.New(sync.Options{ BaseURL: "http://localhost:8080/_sync", Scope: "user:demo", OnSync: func(cursor uint64) { fmt.Printf("Synced to cursor %d\n", cursor) }, OnOnline: func() { fmt.Println("Connected!") }, OnOffline: func() { fmt.Println("Offline - changes will sync later") }, }) // Start sync ctx := context.Background() client.Start(ctx) defer client.Stop() // Create a collection todos := sync.NewCollection[Todo](client, "todo") // Create a todo (works offline!) todo := todos.Create("todo-1", Todo{ Title: "Learn sync", }) // Create an effect to react to changes sync.NewEffect(func() { t := todo.Get() status := "pending" if t.Completed { status = "done" } fmt.Printf("Todo: %s [%s]\n", t.Title, status) }) // Update the todo todo.Set(Todo{ Title: "Learn sync", Completed: true, }) // Wait a bit to see sync happen time.Sleep(2 * time.Second) } ``` ## Architecture ``` ┌─────────────────────────────────────────────────────┐ │ Client │ │ │ │ ┌───────────────┐ ┌──────────────────────────┐ │ │ │ Reactive │ │ Sync Engine │ │ │ │ Layer │ │ │ │ │ │ │ │ ┌────────────────────┐ │ │ │ │ Signal[T] │ │ │ Mutation Queue │ │ │ │ │ Computed[T] │ │ │ (offline-safe) │ │ │ │ │ Effect │ │ └────────────────────┘ │ │ │ │ │ │ │ │ │ └───────────────┘ │ ┌────────────────────┐ │ │ │ │ │ │ Store │ │ │ │ ▼ │ │ (local state) │ │ │ │ ┌───────────────┐ │ └────────────────────┘ │ │ │ │ Collections │ │ │ │ │ │ │◄───┤ ┌────────────────────┐ │ │ │ │ Entity[T] │ │ │ Cursor │ │ │ │ │ │ │ │ (sync position) │ │ │ │ └───────────────┘ │ └────────────────────┘ │ │ │ │ │ │ │ └──────────────────────────┘ │ │ │ │ └──────────────────────────────────│──────────────────┘ │ ┌──────────────┴──────────────┐ │ │ ▼ ▼ Push/Pull Live Updates (HTTP) (WebSocket) │ │ └──────────────┬──────────────┘ │ ▼ ┌─────────────┐ │ Server │ └─────────────┘ ``` ## When to Use Sync **Use sync when:** * Building offline-capable applications * You need instant UI updates without loading states * Multiple clients need to see synchronized data * You want fine-grained reactivity in Go **Consider alternatives when:** * Building server-side only applications * You don't need offline support * Simple request/response patterns are sufficient ## What's Next? 1. **[Quick Start](/view/sync-quick-start)** - Build your first synced app 2. **[Client](/view/sync-client)** - Client configuration and lifecycle 3. **[Reactive](/view/sync-reactive)** - Deep dive into Signal, Computed, Effect 4. **[Collections](/view/sync-collections)** - Working with synchronized data 5. **[Integration](/view/sync-integration)** - Combining sync with live for real-time updates # Quick Start Source: https://docs.go-mizu.dev/view/sync-quick-start Build your first offline-first app with sync in 10 minutes This guide walks you through creating a simple offline-first application using the sync package. You'll learn how to set up a client, create collections, and use reactive primitives. ## Prerequisites * Go 1.22 or later * Basic understanding of Go ## Step 1: Create Your Project ```bash theme={null} mkdir sync-demo && cd sync-demo go mod init sync-demo go get github.com/go-mizu/mizu ``` ## Step 2: Define Your Data Type Create `main.go`: ```go theme={null} package main import ( "context" "fmt" "time" "github.com/go-mizu/mizu/view/sync" ) // Todo is our synchronized data type type Todo struct { Title string `json:"title"` Completed bool `json:"completed"` } ``` ## Step 3: Create and Configure the Client ```go theme={null} func main() { // Create the sync client client := sync.New(sync.Options{ BaseURL: "http://localhost:8080/_sync", // Your sync server Scope: "user:demo", // Data partition // Optional callbacks OnSync: func(cursor uint64) { fmt.Printf("Synced to cursor %d\n", cursor) }, OnOnline: func() { fmt.Println("Connected to server") }, OnOffline: func() { fmt.Println("Working offline") }, OnError: func(err error) { fmt.Printf("Error: %v\n", err) }, }) // ... continue below } ``` ## Step 4: Start the Client ```go theme={null} // Start sync in the background ctx := context.Background() if err := client.Start(ctx); err != nil { fmt.Printf("Initial sync failed: %v\n", err) // Client continues working offline } defer client.Stop() ``` ## Step 5: Create a Collection ```go theme={null} // Create a typed collection todos := sync.NewCollection[Todo](client, "todo") ``` ## Step 6: Use the Collection ```go theme={null} // Create a todo (works even offline!) todo := todos.Create("todo-1", Todo{ Title: "Learn sync", Completed: false, }) // Read the value fmt.Printf("Created: %s\n", todo.Get().Title) // Update it todo.Set(Todo{ Title: "Learn sync", Completed: true, }) // List all todos fmt.Printf("\nAll todos (%d):\n", todos.Count()) for _, entity := range todos.All() { t := entity.Get() status := "[ ]" if t.Completed { status = "[x]" } fmt.Printf(" %s %s\n", status, t.Title) } ``` ## Step 7: Add Reactivity ```go theme={null} // React to changes automatically sync.NewEffect(func() { count := todos.Count() fmt.Printf("\n>>> Todo count changed: %d\n", count) }) // Create more todos todos.Create("todo-2", Todo{Title: "Build app"}) todos.Create("todo-3", Todo{Title: "Deploy"}) // Delete one todos.Get("todo-2").Delete() ``` ## Complete Example ```go theme={null} package main import ( "context" "fmt" "time" "github.com/go-mizu/mizu/view/sync" ) type Todo struct { Title string `json:"title"` Completed bool `json:"completed"` } func main() { // Create client client := sync.New(sync.Options{ BaseURL: "http://localhost:8080/_sync", Scope: "user:demo", OnSync: func(cursor uint64) { fmt.Printf("Synced to cursor %d\n", cursor) }, OnOnline: func() { fmt.Println("Connected!") }, OnOffline: func() { fmt.Println("Offline - changes will sync later") }, }) // Start sync ctx := context.Background() client.Start(ctx) defer client.Stop() // Create collection todos := sync.NewCollection[Todo](client, "todo") // Log changes reactively sync.NewEffect(func() { fmt.Printf("Todo count: %d\n", todos.Count()) }) // Create some todos todos.Create("1", Todo{Title: "Learn Go"}) todos.Create("2", Todo{Title: "Build sync app"}) todos.Create("3", Todo{Title: "Deploy"}) // Mark one complete entity := todos.Get("1") todo := entity.Get() todo.Completed = true entity.Set(todo) // Find incomplete incomplete := todos.Find(func(t Todo) bool { return !t.Completed }) fmt.Printf("\nIncomplete todos:\n") for _, e := range incomplete { fmt.Printf(" - %s\n", e.Get().Title) } // Keep running to see sync happen time.Sleep(3 * time.Second) } ``` ## Running the Example If you have a sync server running: ```bash theme={null} go run main.go ``` Output: ``` Connected! Synced to cursor 0 Todo count: 0 Todo count: 1 Todo count: 2 Todo count: 3 Incomplete todos: - Build sync app - Deploy Synced to cursor 3 ``` ## What Just Happened? ``` 1. Client connects to server │ ▼ 2. Initial sync (pulls any existing data) │ ▼ 3. Collections created (bound to client) │ ▼ 4. Effects set up (react to changes) │ ▼ 5. Local changes made (immediate) │ ▼ 6. Mutations queued (background) │ ▼ 7. Push to server (when online) │ ▼ 8. OnSync callback (confirms sync) ``` **Key points:** * Changes are **immediate** locally * Sync happens in the **background** * Works **offline** - mutations queue until connected * **Reactive** - effects run automatically when data changes ## Understanding the Parts ### Client The client manages: * Connection to sync server * Mutation queue (for offline support) * Pulling changes from server * Online/offline state ### Collection A collection is a reactive set of entities: * Type-safe with generics * Syncs with server automatically * Supports CRUD operations * Reactive (triggers effects) ### Entity An entity is a single record: * Has a unique ID * Reactive Get/Set * Can be deleted * Tracks existence ### Reactive Primitives * **Signal**: A value that notifies when changed * **Computed**: A derived value that auto-updates * **Effect**: A function that runs when dependencies change ## Next Steps * **[Client](/view/sync-client)** - Configuration options and lifecycle * **[Reactive](/view/sync-reactive)** - Deep dive into signals and effects * **[Collections](/view/sync-collections)** - Working with data * **[Integration](/view/sync-integration)** - Real-time updates with Live # Reactive Source: https://docs.go-mizu.dev/view/sync-reactive Signal, Computed, and Effect for reactive state management The sync package includes a reactive state system inspired by SolidJS and Svelte. It provides **Signals** for state, **Computed** for derived values, and **Effects** for side effects. This enables automatic updates when data changes. ## Core Concepts ### Why Reactive State? Without reactivity: ```go theme={null} // Manual updates everywhere count := 0 count++ updateUI(count) // Don't forget this! updateOtherThing() // And this! ``` With reactivity: ```go theme={null} // Automatic propagation count := sync.NewSignal(0) count.Set(1) // All dependents update automatically ``` ## Signal A **Signal** is a reactive value container. When the value changes, all dependents are notified. ### Creating Signals ```go theme={null} import "github.com/go-mizu/mizu/view/sync" // Signal with initial value count := sync.NewSignal(0) name := sync.NewSignal("Alice") items := sync.NewSignal([]string{"a", "b", "c"}) ``` ### Reading Values ```go theme={null} value := count.Get() // Returns 0 ``` When `Get()` is called inside a Computed or Effect, it automatically registers as a dependency. ### Setting Values ```go theme={null} count.Set(5) // All dependents are notified ``` ### Updating Values ```go theme={null} // Update based on current value count.Update(func(current int) int { return current + 1 }) ``` ### Full Example ```go theme={null} count := sync.NewSignal(0) // Read fmt.Println(count.Get()) // 0 // Set count.Set(10) fmt.Println(count.Get()) // 10 // Update count.Update(func(n int) int { return n * 2 }) fmt.Println(count.Get()) // 20 ``` ## Computed A **Computed** is a derived value that automatically recomputes when its dependencies change. ### Creating Computed Values ```go theme={null} count := sync.NewSignal(5) // This automatically tracks count as a dependency doubled := sync.NewComputed(func() int { return count.Get() * 2 }) ``` ### Reading Computed Values ```go theme={null} fmt.Println(doubled.Get()) // 10 count.Set(7) fmt.Println(doubled.Get()) // 14 (automatically recomputed) ``` ### Lazy Evaluation Computed values are: 1. **Lazy** - Only computed when accessed 2. **Cached** - Not recomputed if dependencies haven't changed ```go theme={null} computeCount := 0 result := sync.NewComputed(func() int { computeCount++ return count.Get() * 2 }) // Not computed yet (lazy) fmt.Println(computeCount) // 0 result.Get() fmt.Println(computeCount) // 1 result.Get() // Uses cache fmt.Println(computeCount) // 1 (not recomputed) count.Set(10) // Marks as dirty result.Get() // Now recomputes fmt.Println(computeCount) // 2 ``` ### Chained Computed Computed values can depend on other computed values: ```go theme={null} count := sync.NewSignal(5) doubled := sync.NewComputed(func() int { return count.Get() * 2 }) quadrupled := sync.NewComputed(func() int { return doubled.Get() * 2 }) fmt.Println(quadrupled.Get()) // 20 count.Set(3) fmt.Println(quadrupled.Get()) // 12 ``` ## Effect An **Effect** runs a function whenever its dependencies change. Use it for side effects like logging, API calls, or UI updates. ### Creating Effects ```go theme={null} count := sync.NewSignal(0) effect := sync.NewEffect(func() { fmt.Printf("Count is now: %d\n", count.Get()) }) // Prints immediately: "Count is now: 0" count.Set(5) // Prints: "Count is now: 5" count.Set(10) // Prints: "Count is now: 10" ``` ### Stopping Effects ```go theme={null} effect := sync.NewEffect(func() { // ... }) // Later, stop the effect effect.Stop() // Changes no longer trigger the effect count.Set(100) // Nothing printed ``` ### Effect Use Cases **Logging:** ```go theme={null} sync.NewEffect(func() { log.Printf("User changed: %+v", user.Get()) }) ``` **UI Updates:** ```go theme={null} sync.NewEffect(func() { updateTodoList(todos.All()) }) ``` **Persistence:** ```go theme={null} sync.NewEffect(func() { settings := settingsSignal.Get() saveToStorage(settings) }) ``` ## Practical Examples ### Counter ```go theme={null} count := sync.NewSignal(0) increment := func() { count.Update(func(n int) int { return n + 1 }) } decrement := func() { count.Update(func(n int) int { return n - 1 }) } // Display effect sync.NewEffect(func() { fmt.Printf("Counter: %d\n", count.Get()) }) increment() // Counter: 1 increment() // Counter: 2 decrement() // Counter: 1 ``` ### Form Validation ```go theme={null} email := sync.NewSignal("") password := sync.NewSignal("") emailValid := sync.NewComputed(func() bool { e := email.Get() return strings.Contains(e, "@") && len(e) > 3 }) passwordValid := sync.NewComputed(func() bool { return len(password.Get()) >= 8 }) formValid := sync.NewComputed(func() bool { return emailValid.Get() && passwordValid.Get() }) // UI effect sync.NewEffect(func() { if formValid.Get() { enableSubmitButton() } else { disableSubmitButton() } }) ``` ### Filtered List ```go theme={null} items := sync.NewSignal([]Item{...}) filter := sync.NewSignal("") filteredItems := sync.NewComputed(func() []Item { f := strings.ToLower(filter.Get()) if f == "" { return items.Get() } var result []Item for _, item := range items.Get() { if strings.Contains(strings.ToLower(item.Name), f) { result = append(result, item) } } return result }) // Update filter filter.Set("search term") // filteredItems.Get() now returns only matching items ``` ### Stats Dashboard ```go theme={null} sales := sync.NewSignal([]Sale{...}) totalRevenue := sync.NewComputed(func() float64 { var total float64 for _, s := range sales.Get() { total += s.Amount } return total }) averageSale := sync.NewComputed(func() float64 { all := sales.Get() if len(all) == 0 { return 0 } return totalRevenue.Get() / float64(len(all)) }) saleCount := sync.NewComputed(func() int { return len(sales.Get()) }) ``` ## Integration with Collections Collections are reactive: ```go theme={null} client := sync.New(opts) todos := sync.NewCollection[Todo](client, "todo") // This is reactive! sync.NewEffect(func() { all := todos.All() // Re-runs when todos change fmt.Printf("Todo count: %d\n", len(all)) }) // Also reactive todoCount := sync.NewComputed(func() int { return todos.Count() }) ``` ## Thread Safety All reactive primitives are thread-safe: ```go theme={null} count := sync.NewSignal(0) // Safe to call from multiple goroutines go func() { count.Set(1) }() go func() { count.Set(2) }() go func() { fmt.Println(count.Get()) }() ``` ## Best Practices ### 1. Keep Computations Pure ```go theme={null} // Good: pure computation doubled := sync.NewComputed(func() int { return count.Get() * 2 }) // Bad: side effects in computed bad := sync.NewComputed(func() int { log.Println("Computing...") // Side effect! return count.Get() * 2 }) ``` Use Effects for side effects. ### 2. Avoid Deep Nesting ```go theme={null} // Harder to follow result := sync.NewComputed(func() int { return sync.NewComputed(func() int { // Don't nest like this return count.Get() * 2 }).Get() }) // Better: flat structure doubled := sync.NewComputed(func() int { return count.Get() * 2 }) result := sync.NewComputed(func() int { return doubled.Get() }) ``` ### 3. Stop Effects When Done ```go theme={null} effect := sync.NewEffect(func() { ... }) defer effect.Stop() // Clean up ``` ### 4. Use Computed for Derived State ```go theme={null} // Bad: manual syncing items := sync.NewSignal([]Item{...}) count := sync.NewSignal(0) sync.NewEffect(func() { count.Set(len(items.Get())) // Manually keeping in sync }) // Good: computed items := sync.NewSignal([]Item{...}) count := sync.NewComputed(func() int { return len(items.Get()) // Automatically derived }) ``` ### 5. Break Down Complex Computations ```go theme={null} // Hard to read result := sync.NewComputed(func() Report { items := items.Get() filtered := filterItems(items, filter.Get()) sorted := sortItems(filtered, sortBy.Get()) paginated := paginateItems(sorted, page.Get(), pageSize.Get()) return buildReport(paginated) }) // Better: intermediate computations filteredItems := sync.NewComputed(func() []Item { return filterItems(items.Get(), filter.Get()) }) sortedItems := sync.NewComputed(func() []Item { return sortItems(filteredItems.Get(), sortBy.Get()) }) paginatedItems := sync.NewComputed(func() []Item { return paginateItems(sortedItems.Get(), page.Get(), pageSize.Get()) }) report := sync.NewComputed(func() Report { return buildReport(paginatedItems.Get()) }) ``` # Templates Source: https://docs.go-mizu.dev/view/templates Understanding Go template syntax and data access This guide covers everything you need to know about writing templates in Mizu's view system. We'll explore Go's template syntax, how data flows through your templates, and common patterns. ## Template Basics Mizu uses Go's standard `html/template` package. Templates are HTML files with special markers (`{{` and `}}`) for dynamic content. ```html theme={null}

      Hello, {{.Data.Name}}!

      Welcome to our site.

      ``` ## Accessing Data In Mizu templates, your data is wrapped in a structure. Access your data using `.Data`: ```go theme={null} // In your handler: view.Render(c, "home", view.Data{ "Title": "Welcome", "User": user, "Items": items, }) ``` ```html theme={null}

      {{.Data.Title}}

      Hello, {{.Data.User.Name}}

      ``` ### The Template Data Structure Templates receive this structure: | Field | Description | Available In | | -------------- | ----------------------------- | ----------------- | | `.Page.Name` | Template name (e.g., "home") | Pages and Layouts | | `.Page.Layout` | Layout name (e.g., "default") | Pages and Layouts | | `.Data` | Your data from the handler | Pages and Layouts | | `.Content` | Rendered page content | Layouts only | ## Go Template Syntax ### Outputting Values Use `.Data.` to access your data: ```html theme={null}

      {{.Data.Title}}

      Author: {{.Data.Post.Author.Name}}

      Setting: {{.Data.Settings.theme}}

      ``` ### Conditions Use `if` to show content conditionally: ```html theme={null} {{if .Data.LoggedIn}}

      Welcome back!

      {{else}}

      Please log in.

      {{end}} {{if .Data.Error}}
      {{.Data.Error}}
      {{end}} ``` ### Comparison Operators Go templates provide comparison functions: | Function | Description | Example | | -------- | --------------------- | --------------------------------- | | `eq` | Equal | `{{if eq .Data.Status "active"}}` | | `ne` | Not equal | `{{if ne .Data.Count 0}}` | | `lt` | Less than | `{{if lt .Data.Price 100}}` | | `le` | Less than or equal | `{{if le .Data.Stock 5}}` | | `gt` | Greater than | `{{if gt .Data.Price 100}}` | | `ge` | Greater than or equal | `{{if ge .Data.Age 18}}` | ```html theme={null} {{if eq .Data.Status "active"}}Active{{end}} {{if ne .Data.Count 0}}Has items{{end}} {{if gt .Data.Price 100}}Premium{{end}} {{if le .Data.Stock 5}}Low stock{{end}} ``` ### Boolean Logic Combine conditions with `and`, `or`, and `not`: ```html theme={null} {{if and .Data.LoggedIn .Data.IsAdmin}} Admin controls {{end}} {{if or .Data.Error .Data.Warning}}
      Check messages
      {{end}} {{if not .Data.Disabled}} {{end}} ``` ### Loops Use `range` to iterate over slices, arrays, or maps: ```html theme={null}
        {{range .Data.Items}}
      • {{.}}
      • {{end}}
        {{range $index, $item := .Data.Items}}
      • {{$index}}: {{$item}}
      • {{end}}
      {{range .Data.Items}}
    • {{.}}
    • {{else}}
    • No items found.
    • {{end}} ``` ### Looping Over Structs When ranging over a slice of structs: ```html theme={null} {{range .Data.Users}}

      {{.Name}}

      {{.Email}}

      {{end}} ``` ### Looping Over Maps ```html theme={null} {{range $key, $value := .Data.Settings}}

      {{$key}}: {{$value}}

      {{end}} ``` ### Variables Assign values to variables with `:=`: ```html theme={null} {{$name := .Data.User.Name}}

      Hello, {{$name}}!

      {{$fullName := printf "%s %s" .Data.FirstName .Data.LastName}}

      {{$fullName}}

      ``` ### The Root Context (\$) Inside a `range` loop, `.` refers to the current item. Use `$` to access the root context: ```html theme={null} {{range .Data.Users}}

      {{.Name}}

      {{if $.Data.ShowEmail}}

      {{.Email}}

      {{end}} {{end}} ``` `$` always refers to the original data passed to the template. ### Pipelines Chain operations with `|`: ```html theme={null}

      {{.Data.Name | upper}}

      {{.Data.Title | lower | trim}}

      ``` ### Comments Template comments are stripped from output: ```html theme={null} {{/* This comment won't appear in the HTML */}}

      Visible content

      ``` ### Whitespace Control Use `-` to trim whitespace: ```html theme={null} {{- .Data.Name -}} {{- .Data.Name}} {{.Data.Name -}} ``` This is useful for keeping your HTML clean: ```html theme={null}
        {{- range .Data.Items}}
      • {{.}}
      • {{- end}}
      ``` ## Layouts and Pages ### Layout Structure Layouts use `{{.Content}}` to include the rendered page: ```html theme={null} {{.Data.Title}}
      {{.Content}}
      Copyright 2024
      ``` ### Page Structure Pages are simple - just write HTML with template expressions: ```html theme={null}

      Welcome, {{.Data.Name}}!

      This is the home page.

      ``` The page content is rendered first, then inserted into the layout's `{{.Content}}`. ## Common Patterns ### Conditional Classes ```html theme={null}
      ...
      ``` ### Dynamic Attributes ```html theme={null} ``` ### Safe HTML Output By default, Go templates escape HTML to prevent XSS attacks: ```html theme={null}
      {{.Data.Content}}
      ``` To output raw HTML (use only with trusted content!): ```go theme={null} import "html/template" view.Data{ "Content": template.HTML("bold"), } ``` ```html theme={null}
      {{.Data.Content}}
      ``` ### Empty/Nil Checks ```html theme={null} {{if .Data.Description}}

      {{.Data.Description}}

      {{end}} {{if .Data.Items}}
        {{range .Data.Items}}
      • {{.}}
      • {{end}}
      {{else}}

      No items found.

      {{end}} ``` ### Building Links ```html theme={null} View Profile Next Page ``` ### Formatting Numbers and Dates Use custom functions or `printf`: ```html theme={null}

      Price: ${{printf "%.2f" .Data.Price}}

      Count: {{printf "%d" .Data.Count}}

      ``` For dates, either format in Go or use a custom function: ```go theme={null} view.Data{ "FormattedDate": time.Now().Format("January 2, 2006"), } ``` Or add a custom function: ```go theme={null} view.Config{ Funcs: template.FuncMap{ "formatDate": func(t time.Time) string { return t.Format("Jan 2, 2006") }, }, } ``` ```html theme={null}

      Published: {{formatDate .Data.PublishedAt}}

      ``` ## Debugging Templates ### Print Values Use `printf` with `%#v` to see the Go syntax representation: ```html theme={null}
      {{printf "%#v" .Data}}
      ``` ### Check Types ```html theme={null}

      Type: {{printf "%T" .Data.Items}}

      ``` ### Enable Development Mode For detailed error messages with line numbers: ```go theme={null} view.New(view.Config{ Development: true, }) ``` ## Best Practices ### 1. Keep Templates Simple Put complex logic in Go handlers, not templates: ```go theme={null} // Good: logic in handler isVIP := user.TotalPurchases > 1000 && user.MemberSince.Before(cutoff) view.Data{"IsVIP": isVIP} ``` ```html theme={null} {{if .Data.IsVIP}}VIP Member{{end}} ``` ### 2. Use Meaningful Variable Names ```html theme={null} {{range $user := .Data.Users}}

      {{$user.Name}}

      {{end}} ``` ### 3. Document Expected Data Add comments at the top of templates: ```html theme={null} {{/* Expected data: - Title: string - User: {Name, Email} - Posts: []Post */}} ``` ### 4. Escape User Input Always let Go's default escaping protect against XSS. Only use `template.HTML` for content you trust completely. ### 5. Use Whitespace Trimming Keep generated HTML clean: ```html theme={null}
        {{- range .Data.Items}}
      • {{.}}
      • {{- end}}
      ```