Responses

The second type parameter of a handler is the response body, serialized exactly as returned.

type ItemOut struct {
    ID    string `json:"id" doc:"The item's identifier"`
    Name  string `json:"name" doc:"The item's display name"`
    Owner string `json:"owner,omitzero" doc:"The user the item belongs to"`
}

r.Get("/items/{item_id}", func(ctx *muzak.Context, in ItemParams) (ItemOut, error) {
    return ItemOut{ID: in.ID, Name: "Foo"}, nil
})

There is no wrapper type, no response_model= and no filtering pass at run time. The handler's return type is the response model, which means a field you did not declare cannot leak: it is not part of the type, and the compiler is what tells you, not a bug report.

That is why response models are kept separate from storage types:

// Item is the store's own type, deliberately separate from the response
// models. A field added here does not become visible to clients until a
// handler puts it on a response type.
type Item struct {
    ID   string
    Name string
}

The body is encoded fully into a buffer before anything is written, so a failure part way through encoding still produces a clean error response instead of a truncated body. Content-Type: application/json; charset=utf-8 and Content-Length are set for you.

Status codes

A status that never changes for a route is part of the route.

r.Post("/items/", handlers.CreateItem, muzak.Status(http.StatusCreated))

A status that depends on what happened is set in the handler.

func CreateItem(ctx *muzak.Context, in schemas.ItemCreateIn) (schemas.ItemOut, error) {
    store := muzak.From[*core.ItemStore](ctx)

    if err := store.Create(core.Item{ID: in.ID, Name: in.Name}); err != nil {
        return schemas.ItemOut{}, asHTTPError(err)
    }
    if in.Async {
        ctx.SetStatus(http.StatusAccepted)
    }
    return schemas.ItemOut{ID: in.ID, Name: in.Name}, nil
}

The two are orthogonal. You never have to choose between returning a value and setting a status, and you never declare something dynamic as if it were fixed. When both are used the imperative call wins.

Details worth knowing:

  • Without either, the status is 200.
  • ctx.Status() reports what will be written, which is the route's declared default until SetStatus changes it.
  • Codes outside 100 to 599 are clamped to 500.
  • Calling SetStatus after the body has started writing has no effect and is logged at warn level, because the status line has by then been sent.
  • 204 and 304 write no body at all, whatever the handler returned.
func Feed(ctx *muzak.Context, in schemas.FeedIn) (schemas.FeedOut, error) {
    if !in.IfModifiedSince.IsZero() && !feedUpdatedAt.After(in.IfModifiedSince.Time) {
        ctx.SetStatus(http.StatusNotModified)
        return schemas.FeedOut{}, nil
    }
    // ...
}

Headers

ctx.SetHeader("Last-Modified", feedUpdatedAt.UTC().Format(http.TimeFormat))
ctx.SetHeader("Vary", "Save-Data, X-Tag, Cookie")
ctx.AddHeader("Vary", "Accept-Encoding")   // appends rather than replacing

SetHeader replaces any value already set; AddHeader appends, which is what repeated headers such as Set-Cookie and Vary require. Headers must be set before the handler returns: once the response has begun, net/http ignores further changes.

Cookies

session := uuid.NewV4().String()
ctx.SetCookie(&http.Cookie{
    Name:  "session_id",
    Value: session,
    Path:  "/",
    // HttpOnly keeps the session out of reach of scripts, and SameSite keeps
    // it off cross-site requests. Secure belongs here too once this is served
    // over TLS, which is why it is named rather than omitted.
    HttpOnly: true,
    SameSite: http.SameSiteLaxMode,
    Secure:   false,
    MaxAge:   3600,
})

Muzak does not modify the cookie. Setting Secure, HttpOnly and SameSite appropriately is the caller's job, which is covered in Cookies.

Responses with no body

muzak.Empty is also usable as an output type. The generated document then describes no response content at all, which is the honest description of a route that answers with nothing.

r.Delete("/items/{item_id}", func(ctx *muzak.Context, in ItemParams) (muzak.Empty, error) {
    store := muzak.From[*core.ItemStore](ctx)
    if _, err := store.Get(in.ID); err != nil {
        return muzak.Empty{}, muzak.NotFound("Item not found")
    }
    return muzak.Empty{}, nil
}, muzak.Status(http.StatusNoContent))

Pair it with muzak.Status(http.StatusNoContent). At 200 an Empty value still encodes as {}, since that is what it serializes to; at 204 nothing is written.

HTML

A handler returning muzak.HTML bypasses JSON encoding entirely. The string is written verbatim under text/html, and the generated document describes the response as text/html rather than as a JSON schema.

func LoginForm(ctx *muzak.Context, _ muzak.Empty) (muzak.HTML, error) {
    return muzak.HTML(`<body>
<form action="/login/" method="post">
<label>Username <input name="username" autocomplete="username"></label>
<label>Password <input name="password" type="password" autocomplete="current-password"></label>
<input type="submit" value="Sign in">
</form>
</body>`), nil
}

Everything else about the route is unchanged, so its status, headers and errors work exactly as they do for a JSON route. The value is written as given: Muzak does not escape it, because it cannot tell markup the handler meant from text it did not. Interpolating anything a client supplied is the handler's job to escape, with html/template or html.EscapeString.

Documenting other outcomes

A handler that reports a failure through an error does not describe that outcome in its return type, so declare it at registration.

r.Get("/items/{item_id}", handlers.ReadItem,
    muzak.WithResponseDoc(http.StatusNotFound, "The item does not exist"))

WithResponseDoc is recorded in the OpenAPI document and has no effect at runtime. Applied to a router, it documents the response for every route beneath it, which is how app.Include(admin, muzak.WithResponseDoc(418, "I'm a teapot")) reads. The body is described as Muzak's error envelope, because that is what a returned error produces.

When the outcome carries a body of its own instead, name the model:

r.Post("/users/", handlers.CreateUser,
    muzak.Status(http.StatusCreated),
    muzak.WithResponseModel[schemas.ErrorOut](http.StatusBadRequest, "The request was malformed"))

One operation can carry a different schema at every status code that way. Response Models covers it in full, including inheritance, Empty and HTML models, and what reaches the document.

Writing the response yourself

For a file download or any other body Muzak should not encode, take the writer.

func Download(ctx *muzak.Context, in FileParams) (muzak.Empty, error) {
    file, err := os.Open(filepath.Join(storageDir, in.Name))
    if err != nil {
        return muzak.Empty{}, muzak.NotFound("no such file")
    }
    defer file.Close()

    stat, err := file.Stat()
    if err != nil {
        return muzak.Empty{}, err
    }

    ctx.SetHeader("Content-Disposition", `attachment; filename="report.pdf"`)
    http.ServeContent(ctx.ResponseWriter(), ctx.Request(), stat.Name(), stat.ModTime(), file)
    return muzak.Empty{}, nil
}

Writing to ctx.ResponseWriter() bypasses Muzak's response encoding: the returned value is not serialized and SetStatus stops having an effect, so return the zero Out value with a nil error afterwards. The writer supports http.ResponseController, so flushing and hijacking work as usual.

For streaming a sequence of events rather than one body, reach for Server-Sent Events instead, which keeps the typed contract and the generated documentation.

Where to go next

Dependencies covers how services and resolved values reach a handler, Error Handling covers what happens when a handler returns an error, and JSON covers the encoding in detail.

Open source under MIT / Apache-2.0 · sustained by the people who ship on it.Built on net/http, and nothing else.