Documentation / Routing & responses

Routing & responses

A URL is a useful interface. Give it a Go handler, then decide whether the browser needs a whole page or a small fragment.

Pages and fragments

Every route is a Handler: a function that takes a *hyperchi.Context, answers through it, and returns an error. c.View renders a page template: the whole page for a browser, and only the template for an htmx partial request. For a small piece of HTML, return c.HTML, or render a fragment template with c.Fragment.

HyperChi builds on chi. Existing net/http middleware fits naturally into the application, and app.Handle mounts an existing net/http handler. New endpoints are context handlers, with request helpers, rendering, JSON responses, and returned-error handling.

A button with a round trip

Register this route before starting the server. It needs no extra imports:

app.Get("/hello", func(c *hyperchi.Context) error {
    return c.HTML("<p>Hello from the server.</p>")
})

Load htmx 4.0.0 and add a button to your template. The browser requests /hello and puts the HTML response inside #greeting.

<script src="https://cdn.jsdelivr.net/npm/htmx.org@4.0.0/dist/htmx.min.js"></script>

<button hx-get="/hello" hx-target="#greeting">
  Say hello
</button>
<div id="greeting" aria-live="polite"></div>

For user-provided content, prefer a Go HTML template so values are escaped automatically. Never concatenate unescaped input into an HTML response.

Methods and patterns

Get, Post, Put, Patch, Delete, Head, and Options each take a pattern and a handler. Patterns are chi patterns: {name} captures one path segment, {name:regexp} restricts it, and a trailing * captures the rest of the path. A request that matches no pattern, or fails a regexp, goes through the same error pipeline as any other miss. Every Get route also answers HEAD with the same status and headers and no body, so uptime checks and link previews work; register app.Head for the same pattern when HEAD needs its own answer.

app.Get("/items", listItems)
app.Post("/items", createItem)
app.Get("/items/{id:[0-9]+}", showItem)
app.Put("/items/{id}", updateItem)
app.Patch("/items/{id}", updateItem)
app.Delete("/items/{id}", deleteItem)
app.Get("/files/*", serveFile)

app.Resource("/posts", controller) registers the seven conventional HTML routes for a struct with Index, New, Create, Show, Edit, Update, and Delete handlers. app.RESTful("/api/posts", controller) does the same for a JSON API and forces JSON errors.

Reading the request

Route parameters come from c.Param. c.ParamInt converts one to an int, and a value that is not a number comes back as a 400 error you can return as it is.

func showItem(c *hyperchi.Context) error {
    id, err := c.ParamInt("id")
    if err != nil {
        return err // 400: id must be an integer
    }
    found, err := store.Find(c.Ctx(), id)
    if err != nil {
        return hyperchi.NotFound("No such item.")
    }
    return c.View("item", hyperchi.H{"item": found})
}

Query strings have three typed readers, so no handler needs strconv:

func searchHandler(c *hyperchi.Context) error {
    q := c.Query("q")                        // first value, "" when absent
    page := c.QueryInt("page", 1)            // 1 when absent or not a number
    sort := c.QueryDefault("sort", "newest") // fallback when absent or empty
    tags := c.Queries("tag")                 // every ?tag=... value
    return c.View("search", hyperchi.H{"q": q, "page": page, "sort": sort, "tags": tags})
}

c.FormValue, c.Header, c.Cookie, c.Method, c.Path, and c.Ctx cover the rest of the common reads. To decode and validate a whole form or JSON body into a struct, use hyperchi.Bind[T](c); the forms page covers it.

Groups and middleware

Middleware is plain func(http.Handler) http.Handler, so any chi or net/http middleware works. app.Use applies it to every route, and it must run before the first route is registered (chi panics otherwise; app.UseGlobal has no such limit). Pass middleware after the handler to scope it to one route. app.Group gives a prefix its own middleware, and app.Mount attaches any http.Handler, such as another chi router.

app.Use(app.Middleware.Recovery())

app.Group("/admin", func(r *hyperchi.HyperChi) {
    r.Use(requireAdmin)
    r.Get("/", listItems)
    r.Post("/users", createItem)
})

app.Get("/reports", listItems, requireLogin, app.Middleware.NoCache())

api := chi.NewRouter()
api.Get("/ping", func(w http.ResponseWriter, r *http.Request) {})
app.Mount("/api/v2", api)

The group's r is a copy of the app, so every service (database, sessions, templates, content) works inside it.

Choosing a response

Every response method returns an error, so a handler ends in one line: return c.View(...). A handler answers once; a second response method writes nothing and returns an error.

Method Sends
c.View(name, data) The page inside its layout for a browser or boosted navigation; the bare template for an htmx partial request. The default for page routes.
c.Page(name, data) The page inside its layout, whatever sent the request.
c.Fragment(name, data) The template alone, never a layout. For endpoints that only answer htmx swaps.
c.HTML(markup) Trusted template.HTML your code built. Never pass visitor input.
c.JSON(v), c.Text(s) application/json or text/plain.
c.Blob(type, bytes) Any other content type, such as CSV or Markdown.
c.Redirect(url) 303 See Other, or an HX-Redirect header for htmx so the browser navigates.
c.NoContent() 204, which htmx does not swap.
c.Empty() Headers only, with the status you set.

c.Status, c.SetHeader, and c.Layout chain in front of any of these:

return c.Status(http.StatusCreated).Fragment("item-row", item{})
return c.SetHeader("Cache-Control", "no-store").JSON(hyperchi.H{"ok": true})
return c.Layout("admin").Page("dashboard", data) // wraps in layout-admin

Rendering finishes in a buffer before any header is sent, so a template error never leaves half a page on the wire. The templates page explains layouts and fragments in full.

Status codes and errors

Return an error to choose the status. hyperchi.NotFound, BadRequest, Forbidden, Conflict, and Unprocessable each build an *HTTPError; hyperchi.Errorf(code, ...) takes any code. Any other error becomes a 500 whose text is logged and never shown to the visitor. The app renders the error for whoever asked: JSON for an API client, a small alert for htmx, and an error page for a browser. htmx 4 swaps every status except 204 and 304, so a 422 form fragment lands in place. Error handling covers app.OnError and hyperchi.JSONErrors.

Steering htmx from the server

c.HX() reads the htmx request headers (Target, Source, CurrentURL, Boosted) and sets response headers. Setters chain and end in a response method.

return c.HX().
    Trigger("item-saved").
    Retarget("#list").
    Reswap("beforeend").
    Fragment("item-row", item{})
Setter Header Effect
Trigger(event, detail...) HX-Trigger Fires a browser event after the swap; call it again to fire several.
Retarget(selector) HX-Retarget Swaps into a different element.
Reswap(style) HX-Reswap Changes the swap style, such as outerHTML.
Reselect(selector) HX-Reselect Swaps only part of the response.
PushURL(url), ReplaceURL(url) HX-Push-Url, HX-Replace-Url Updates browser history.
Location(url) HX-Location Navigates without a full reload.
Refresh() HX-Refresh Reloads the page.

A trigger can carry data: c.HX().Trigger("toast", hyperchi.H{"message": "Saved"}). When only the headers matter, finish with Empty() or NoContent().

When you already have net/http code

Application routes use Handler. Code that already speaks net/http reaches the router through three named doors, so the rest of the app keeps the context API:

app.Handle(http.MethodGet, "/metrics", metrics)  // mount an http.Handler
app.Get("/legacy", hyperchi.WrapHTTP(legacy))    // an http.Handler as a Handler
mux.Handle("/inner", app.ToHTTP(listItems))      // a Handler inside another mux

app.Router() returns the chi router for anything else. Do not write new endpoints with http.ResponseWriter signatures: they skip htmx negotiation, the error pipeline, and validation.

Next steps

Use the Context API reference for the complete surface, or try a real request in the playground. Pages and fragments both depend on templates, so read that page next if layouts are unfamiliar.