Skip to content

Guide

Responses and Rendering

Send JSON, text, HTML, templates, files, downloads, streams, and server-sent events, with the right status and headers.

Response helpers on *zinc.Context write the body, set Content-Type, and return an error you pass straight back. Set the status and headers first, then call exactly one body helper.

return c.
Status(zinc.StatusCreated).
SetHeader(zinc.HeaderLocation, "/users/42").
JSON(user)
To send Use
JSON c.JSON(v), or c.JSONPretty(v, " ")
XML, YAML, TOML c.XML(v), c.YAML(v), c.TOML(v)
Text or HTML c.String(s), c.HTML(s)
Nothing c.NoContent() (204)
Pre-encoded bytes c.JSONBlob, c.XMLBlob, c.HTMLBlob, c.Blob
A template c.Render(name, data)
A file c.File(path), c.FileFS(name, fsys)
A download c.Download(path, filename), c.Inline(path)
A stream c.Stream(contentType, reader)
An event stream c.SSE(event)
A redirect c.Redirect(code, url)

The status defaults to 200 OK.

c.Status(zinc.StatusAccepted)
c.SetHeader("Cache-Control", "no-store")
c.AppendHeader("Vary", "Accept-Language")
c.Location("/jobs/81")

These methods return the context, so they chain into the body helper. Once a body helper runs, the response is committed. Changing headers afterwards has no effect.

return c.JSON(zinc.Map{"ok": true})
return c.XML(invoice)
return c.YAML(config)

zinc.Map is shorthand for map[string]any. For bytes that are already encoded, the blob helpers write them unchanged:

return c.JSONBlob(zinc.StatusOK, cachedJSON)
return c.Blob(zinc.StatusOK, "application/vnd.api+json", payload)

Configure a renderer once, then render by name:

views := template.Must(template.ParseGlob("views/*.html"))
cfg := zinc.DefaultConfig
cfg.Renderer = zinc.NewHTMLTemplateRenderer(views)
app := zinc.NewWithConfig(cfg)
app.Get("/dashboard", func(c *zinc.Context) error {
return c.Render("dashboard.html", zinc.Map{"Title": "Overview"})
})

Templates covers text/template, custom engines, and Templ.

return c.File("./public/report.pdf") // served with a detected content type
return c.FileFS("report.pdf", embeddedFiles) // from any fs.FS
return c.Download("./exports/users.csv", "users-2026-09.csv") // "Save as" with a filename
return c.Inline("./public/report.pdf") // display in the browser

c.Stream copies from any io.Reader without buffering the whole body:

return c.Stream("text/csv", exportReader)

For server-sent events, write one event at a time and flush after each:

app.Get("/events", func(c *zinc.Context) error {
for msg := range updates(c.Context()) {
if err := c.SSE(zinc.SSEvent{Event: "update", Data: msg}); err != nil {
return err
}
if f, ok := c.Writer().(http.Flusher); ok {
f.Flush()
}
}
return nil
})

The loop ends when the client disconnects and c.Context() is cancelled. See the Server-Sent Events recipe for a complete program.

Branch on what the client accepts:

switch c.Accepts("application/json", "text/html") {
case "application/json":
return c.JSON(user)
case "text/html":
return c.Render("user.html", user)
default:
return zinc.ErrNotAcceptable
}

Accepts honours quality values and wildcards in the Accept header, and returns the first offer when the header is missing. When each type has a ready-made body, Negotiate picks and sends it in one call:

return c.Negotiate(zinc.StatusOK, zinc.Map{
"application/json": zinc.Map{"ok": true},
"text/plain": "ok",
})
c.SetCookie(&http.Cookie{Name: "theme", Value: "dark", Path: "/"})
return c.Redirect(zinc.StatusSeeOther, "/dashboard")

Cookies covers reading, clearing, and secure defaults.