Respond with different formats
Goal: return the right response shape (JSON, HTML, plain text, YAML, a redirect, an empty body) from a handler, and — when a single endpoint must serve more than one format — pick the shape based on the request’s Content-Type/Accept header.
This assumes a working controller — see Add a controller. All helpers live in the format module (loco_rs::controller::format, re-exported as format from the prelude). Keep handlers returning Result<impl IntoResponse> (or Result<Response>) so you can freely swap which format::* call you return.
1. Simple responses
use loco_rs::prelude::*;
async fn as_json() -> Result<Response> { format::json(serde_json::json!({ "hello": "world" }))}
async fn as_text() -> Result<Response> { format::text("hello, world")}
async fn as_html() -> Result<Response> { format::html("<h1>hello</h1>")}
async fn as_yaml() -> Result<Response> { format::yaml("openapi: 3.1.0\ninfo:\n title: my api\n") // sets Content-Type: application/yaml}
async fn nothing() -> Result<Response> { format::empty() // 200, empty body}
async fn empty_object() -> Result<Response> { format::empty_json() // 200, body: {}}
async fn go_elsewhere() -> Result<Response> { format::redirect("/dashboard") // axum::response::Redirect::to(..)}format::view/format::template render HTML from a Tera view or an inline template string — see Render server-side views.
2. When you need more than a one-liner: format::render()
format::render() returns a RenderBuilder you chain, terminating with one of .json(..), .html(..), .text(..), .empty(), .view(..), .template(..), .redirect(..), or .redirect_with_header_key(..):
async fn get_one(State(ctx): State<AppContext>) -> Result<Response> { format::render() .etag("some-etag-value")? .header("X-Custom", "1") .json(load_item(&ctx).await?)}Available builder methods:
| Method | Purpose |
|---|---|
.status(code) | set the response status (defaults to 200) |
.header(key, value) | add a single response header |
.etag(value) | set the ETag header (errors on non-visible-ASCII input) |
.cookies(&[Cookie, ..]) | add one Set-Cookie header per cookie |
.response() | escape hatch: hand back the raw axum::http::response::Builder |
.redirect(to) | 303 See Other with Location: <to> |
.redirect_with_header_key(key, to) | same, but with a custom header instead of Location — e.g. HX-Redirect for HTMX |
async fn htmx_redirect() -> Result<Response> { format::render().redirect_with_header_key("HX-Redirect", "/notes")}3. Content negotiation: respond differently per client
Use the RespondTo extractor (or its wrapper Format(pub RespondTo)) to detect the request’s format from Content-Type (checked first) or, failing that, Accept:
use loco_rs::prelude::*;
pub async fn get_one( respond_to: RespondTo, Path(id): Path<i64>, State(ctx): State<AppContext>,) -> Result<Response> { let item = load_item(&ctx, id).await?; match respond_to { RespondTo::Html => format::html(&format!("<html><body>{:?}</body></html>", item.title)), _ => format::json(item), }}RespondTo variants: None (neither header present/parseable), Html, Json, Xml, Other(String) (any other MIME type, preserved verbatim). Both RespondTo and Format implement FromRequestParts, so you can extract either one directly as a handler parameter — Format(respond_to) if you prefer the wrapped form.
4. Combine format negotiation with error handling
A common pattern: run your fallible logic first, then match on both the Result and the format in one place, so error rendering stays consistent per-format:
pub async fn get_one( respond_to: RespondTo, Path(id): Path<i64>, State(ctx): State<AppContext>,) -> Result<Response> { let res = load_item(&ctx, id).await;
match res { Ok(item) => match respond_to { RespondTo::Html => format::html(&format!("<html><body>{:?}</body></html>", item.title)), _ => format::json(item), }, Err(Error::Model(ModelError::Validation(errors))) => match respond_to { RespondTo::Html => format::html(&format!("<html><body>errors: {errors:?}</body></html>")), _ => bad_request("opaque message: cannot respond!"), }, // unhandled error kinds: let the framework's default error rendering take over Err(err) => Err(err), }}See Handle errors for what “the framework’s default error rendering” produces for each Error variant.
Verify
curl -s localhost:5150/notes/1 -H 'accept: application/json'curl -s localhost:5150/notes/1 -H 'accept: text/html'