Skip to main content
Every handler in Mizu uses a *mizu.Ctx to send responses. The context provides helper methods for common response types like JSON, HTML, and files, while still giving you access to the underlying http.ResponseWriter when needed.

Response basics

Each response has three parts:
  1. Status code - HTTP status like 200, 404, or 500
  2. Headers - Metadata like Content-Type and Cache-Control
  3. Body - The actual data sent to the client
Mizu’s response helpers handle all three. You typically just call one method and return.

Text responses

Send plain text with c.Text():
func handler(c *mizu.Ctx) error {
    return c.Text(200, "Hello, world!")
}
This sets:
  • Status: 200
  • Content-Type: text/plain; charset=utf-8
  • Body: “Hello, world!”
If the string isn’t valid UTF-8, it’s sent as application/octet-stream.

JSON responses

Send structured data with c.JSON():
func handler(c *mizu.Ctx) error {
    user := User{ID: "123", Name: "Alice"}
    return c.JSON(200, user)
}

// Works with maps too
func status(c *mizu.Ctx) error {
    return c.JSON(200, map[string]any{
        "status": "ok",
        "count":  42,
    })
}
This sets:
  • Status: 200
  • Content-Type: application/json; charset=utf-8
  • Body: JSON-encoded data

HTML responses

Send HTML content with c.HTML():
func handler(c *mizu.Ctx) error {
    return c.HTML(200, "<h1>Welcome</h1><p>Hello from Mizu!</p>")
}
For templates, use Go’s html/template:
var tmpl = template.Must(template.New("page").Parse(`
    <h1>{{.Title}}</h1>
    <p>{{.Message}}</p>
`))

func handler(c *mizu.Ctx) error {
    c.Header().Set("Content-Type", "text/html; charset=utf-8")
    return tmpl.Execute(c.Writer(), map[string]string{
        "Title":   "Welcome",
        "Message": "Hello!",
    })
}

Setting status codes

Each response method takes a status code as the first argument:
return c.JSON(201, createdUser)  // 201 Created
return c.Text(400, "bad input")  // 400 Bad Request
return c.JSON(500, errorBody)    // 500 Internal Server Error
If you pass 0, Mizu uses the previously set status (default 200):
c.Status(202)
return c.Text(0, "Accepted")  // Uses status 202
Check the current status:
code := c.StatusCode()

Working with headers

Set headers before writing the body:
func handler(c *mizu.Ctx) error {
    // Set a header
    c.Header().Set("Cache-Control", "max-age=3600")

    // Set only if not already present
    c.HeaderIfNone("X-Request-Id", "abc123")

    return c.JSON(200, data)
}
Once you write the body (via c.JSON(), c.Text(), etc.), headers are sent and can’t be changed.

Redirects

Send the client to a different URL:
func handler(c *mizu.Ctx) error {
    return c.Redirect(302, "/login")
}
CodeMeaningWhen to use
301Moved PermanentlyURL changed forever
302FoundTemporary redirect (default if you pass 0)
303See OtherRedirect after POST
307Temporary RedirectPreserve method
308Permanent RedirectPreserve method

Empty responses

Return no body with status 204:
func deleteUser(c *mizu.Ctx) error {
    // ... delete the user
    return c.NoContent()  // 204 No Content
}

Serving files

Serve a file from disk:
func handler(c *mizu.Ctx) error {
    // File(statusCode, filePath)
    return c.File(200, "./public/logo.png")
}
Force the browser to download (adds Content-Disposition header):
func handler(c *mizu.Ctx) error {
    // Download(statusCode, filePath, downloadName)
    return c.Download(200, "./reports/data.csv", "report-2024.csv")
}
Both methods:
  • Auto-detect Content-Type from the file extension
  • Support range requests (for video seeking, resumable downloads)
  • Handle If-Modified-Since caching

Streaming responses

Send data gradually as it becomes available:
func handler(c *mizu.Ctx) error {
    return c.Stream(func(w io.Writer) error {
        for i := 0; i < 10; i++ {
            fmt.Fprintf(w, "chunk %d\n", i)
            time.Sleep(100 * time.Millisecond)
        }
        return nil
    })
}

Server-Sent Events (SSE)

Send real-time updates to the client:
func events(c *mizu.Ctx) error {
    ch := make(chan any)

    // Send updates from a goroutine
    go func() {
        defer close(ch)  // Closing the channel ends the stream
        for i := 0; i < 10; i++ {
            ch <- map[string]int{"count": i}
            time.Sleep(time.Second)
        }
    }()

    return c.SSE(ch)
}
SSE sets these headers automatically:
  • Content-Type: text/event-stream
  • Cache-Control: no-cache
  • Connection: keep-alive
Client-side JavaScript:
const events = new EventSource('/events');
events.onmessage = (e) => {
    const data = JSON.parse(e.data);
    console.log('Received:', data);
};

Raw bytes

Send arbitrary bytes with a custom content type:
func handler(c *mizu.Ctx) error {
    data := []byte{0x00, 0x01, 0x02}
    return c.Bytes(200, data, "application/octet-stream")
}

Low-level control

For advanced use cases, access the underlying response features:
// Flush buffered data to the client
c.Flush()

// Hijack the connection (for WebSocket upgrades)
conn, rw, err := c.Hijack()
if err != nil {
    return err
}
defer conn.Close()

// Set write deadline
c.SetWriteDeadline(time.Now().Add(5 * time.Second))

// Enable full duplex (HTTP/2)
c.EnableFullDuplex()

Method reference

MethodSignaturePurpose
Text(code int, s string) errorPlain text
JSON(code int, v any) errorJSON data
HTML(code int, s string) errorHTML content
File(code int, path string) errorServe file
Download(code int, path, name string) errorForce download
Bytes(code int, b []byte, ct string) errorRaw bytes
Stream(fn func(io.Writer) error) errorStreaming
SSE(ch <-chan any) errorServer-Sent Events
Redirect(code int, url string) errorRedirect
NoContent() error204 No Content

Next steps