Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.go-mizu.dev/llms.txt

Use this file to discover all available pages before exploring further.

Integrating your frontend with Mizu’s backend is straightforward. This guide covers patterns, best practices, and common scenarios.

Basic API Call

Frontend

async function fetchUsers() {
  const response = await fetch('/api/users')
  const users = await response.json()
  return users
}

Backend

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

const users = await fetch('/api/users')
  .then(r => r.json())
app.Get("/api/users", handleGetUsers)

POST

await fetch('/api/users', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'Alice', email: 'alice@example.com' })
})
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

// 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

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

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

// Store token
localStorage.setItem('auth_token', token)

// Send with requests
const response = await fetch('/api/protected', {
  headers: {
    'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
  }
})
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:
import "github.com/go-mizu/mizu/middlewares/jwt"

app.Use(jwt.WithOptions(jwt.Options{
    Secret:      []byte("your-secret"),
    TokenLookup: "header:Authorization",
}))

HTTP-Only Cookies

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:
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:
// shared/types.ts
export interface User {
  id: number
  name: string
  email: string
}

export interface CreateUserRequest {
  name: string
  email: string
}
Frontend:
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:
class API {
  private baseURL: string

  constructor(baseURL = '/api') {
    this.baseURL = baseURL
  }

  private async request<T>(
    endpoint: string,
    options?: RequestInit
  ): Promise<T> {
    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<T>(endpoint: string) {
    return this.request<T>(endpoint)
  }

  post<T>(endpoint: string, data: any) {
    return this.request<T>(endpoint, {
      method: 'POST',
      body: JSON.stringify(data),
    })
  }

  put<T>(endpoint: string, data: any) {
    return this.request<T>(endpoint, {
      method: 'PUT',
      body: JSON.stringify(data),
    })
  }

  delete<T>(endpoint: string) {
    return this.request<T>(endpoint, {
      method: 'DELETE',
    })
  }
}

export const api = new API()
Use:
import { api } from './api'

const users = await api.get<User[]>('/users')
const user = await api.post<User>('/users', { name: 'Alice', email: 'alice@example.com' })

Loading States

function Users() {
  const [users, setUsers] = useState<User[]>([])
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)

  useEffect(() => {
    api.get<User[]>('/users')
      .then(setUsers)
      .catch(err => setError(err.message))
      .finally(() => setLoading(false))
  }, [])

  if (loading) return <div>Loading...</div>
  if (error) return <div>Error: {error}</div>

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  )
}

Pagination

Backend

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

const [page, setPage] = useState(1)
const limit = 20

const { data, total } = await api.get(`/users?page=${page}&limit=${limit}`)

File Upload

Frontend

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

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

SSR vs SPA

Architecture decisions

Security

API security best practices

CORS Middleware

CORS configuration

JWT Middleware

JWT authentication