Skip to main content
This guide shows how to create a Mizu app from scratch. You’ll build a working API server that handles multiple endpoints, parses JSON, and organizes routes into groups.

Prerequisites

  • Go 1.22+ - Check with go version
  • A code editor - VS Code with the Go extension works well
  • Basic Go knowledge - Functions, structs, and packages

Step 1: Create a project

Open your terminal and create a new directory:
mkdir myapi
cd myapi
go mod init myapi
The go mod init command creates a go.mod file. This tracks your project’s dependencies.

Step 2: Install Mizu

Add Mizu as a dependency:
go get github.com/go-mizu/mizu@latest
You’ll see Mizu added to your go.mod file.

Step 3: Write your first handler

Create main.go:
package main

import "github.com/go-mizu/mizu"

func main() {
    // Create a new Mizu app
    // This sets up a router with default request logging
    app := mizu.New()

    // Register a handler for GET /
    // When someone visits this URL, the handler runs
    app.Get("/", func(c *mizu.Ctx) error {
        // c is the context - it has the request and response helpers
        // Text(200, "...") sends a plain text response with status 200
        return c.Text(200, "Hello, Mizu!")
    })

    // Start the server on port 3000
    // This runs until you press Ctrl+C
    app.Listen(":3000")
}
Run it:
go run main.go
Open http://localhost:3000. You’ll see “Hello, Mizu!” in the browser. The terminal shows request logs. Press Ctrl+C to stop the server.

Step 4: Add JSON responses

Most APIs return JSON. Update main.go:
package main

import "github.com/go-mizu/mizu"

// Define a struct for your data
type User struct {
    ID    string `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

func main() {
    app := mizu.New()

    // Return JSON data
    app.Get("/users", func(c *mizu.Ctx) error {
        users := []User{
            {ID: "1", Name: "Alice", Email: "[email protected]"},
            {ID: "2", Name: "Bob", Email: "[email protected]"},
        }
        // JSON(200, data) encodes the struct and sets Content-Type header
        return c.JSON(200, users)
    })

    // Path parameter: {id} captures that part of the URL
    app.Get("/users/{id}", func(c *mizu.Ctx) error {
        // c.Param("id") gets the captured value
        id := c.Param("id")
        user := User{ID: id, Name: "Alice", Email: "[email protected]"}
        return c.JSON(200, user)
    })

    app.Listen(":3000")
}
Try these URLs:
  • http://localhost:3000/users → Returns a JSON array of users
  • http://localhost:3000/users/42 → Returns a single user with ID “42”

Step 5: Handle POST requests

APIs need to accept data too. Add a POST endpoint:
package main

import "github.com/go-mizu/mizu"

type User struct {
    ID    string `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

// Input struct for creating users
type CreateUserInput struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

func main() {
    app := mizu.New()

    app.Get("/users", listUsers)
    app.Get("/users/{id}", getUser)
    app.Post("/users", createUser)

    app.Listen(":3000")
}

func listUsers(c *mizu.Ctx) error {
    users := []User{{ID: "1", Name: "Alice"}}
    return c.JSON(200, users)
}

func getUser(c *mizu.Ctx) error {
    id := c.Param("id")
    return c.JSON(200, User{ID: id, Name: "Alice"})
}

func createUser(c *mizu.Ctx) error {
    var input CreateUserInput

    // BindJSON parses the request body into the struct
    // 1<<20 is the max size (1MB) - prevents huge payloads
    if err := c.BindJSON(&input, 1<<20); err != nil {
        // Return 400 Bad Request if parsing fails
        return c.JSON(400, map[string]string{"error": err.Error()})
    }

    // Validate the input
    if input.Name == "" {
        return c.JSON(400, map[string]string{"error": "name is required"})
    }

    // In a real app, you'd save to a database here
    user := User{
        ID:    "123", // Would be generated
        Name:  input.Name,
        Email: input.Email,
    }

    // Return 201 Created with the new user
    return c.JSON(201, user)
}
Test with curl:
curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"name": "Charlie", "email": "[email protected]"}'

Step 6: Organize with route groups

As your API grows, group related routes together:
func main() {
    app := mizu.New()

    // Group all /api/v1 routes
    app.Group("/api/v1", func(r *mizu.Router) {
        // These become /api/v1/users, /api/v1/users/{id}, etc.
        r.Get("/users", listUsers)
        r.Get("/users/{id}", getUser)
        r.Post("/users", createUser)

        // Nested groups work too
        r.Group("/admin", func(admin *mizu.Router) {
            admin.Get("/stats", getStats)
        })
    })

    // Root endpoint
    app.Get("/", func(c *mizu.Ctx) error {
        return c.JSON(200, map[string]string{
            "message": "API v1 available at /api/v1",
        })
    })

    app.Listen(":3000")
}

func getStats(c *mizu.Ctx) error {
    return c.JSON(200, map[string]int{"users": 100})
}
Routes are now:
  • GET / → API info
  • GET /api/v1/users → List users
  • GET /api/v1/users/{id} → Get user
  • POST /api/v1/users → Create user
  • GET /api/v1/admin/stats → Admin stats

Step 7: Add error handling

Centralize how errors are reported:
func main() {
    app := mizu.New()

    // Set a global error handler
    app.ErrorHandler(func(c *mizu.Ctx, err error) {
        // Log the error
        c.Logger().Error("request failed", "error", err)

        // Check if it's a panic (recovered automatically)
        if perr, ok := err.(*mizu.PanicError); ok {
            c.Logger().Error("panic", "value", perr.Value)
        }

        // Send a consistent error response
        c.JSON(500, map[string]string{"error": "internal server error"})
    })

    app.Get("/error", func(c *mizu.Ctx) error {
        // This error flows to the error handler
        return fmt.Errorf("something went wrong")
    })

    app.Listen(":3000")
}

Project structure

For larger projects, organize your code:
myapi/
├── main.go           # Entry point
├── handlers/
│   ├── users.go      # User handlers
│   └── admin.go      # Admin handlers
├── models/
│   └── user.go       # Data structures
└── go.mod
Example handlers/users.go:
package handlers

import "github.com/go-mizu/mizu"

func ListUsers(c *mizu.Ctx) error {
    // ...
}

func GetUser(c *mizu.Ctx) error {
    // ...
}
Then in main.go:
import "myapi/handlers"

func main() {
    app := mizu.New()
    app.Get("/users", handlers.ListUsers)
    app.Get("/users/{id}", handlers.GetUser)
    app.Listen(":3000")
}

What you learned

  • Create a Mizu app with mizu.New()
  • Define handlers that return errors
  • Return JSON with c.JSON(code, data)
  • Capture path parameters with {param} and c.Param()
  • Parse JSON bodies with c.BindJSON()
  • Organize routes with Group()
  • Handle errors centrally with ErrorHandler()

Next steps