Responders
The whole chapter in one sentence: a handler returns a value, and actix-web only accepts it if its type implements Responder. You don't call that trait yourself - you return a type that implements it, and the framework does the conversion.
The question for every handler isn't "how do I write a response?" but "what type do I return?" Pick the right one for the job:
- Need full control over status, headers, and body? Return
HttpResponse- the workhorse. - Just shipping some JSON with a 200? Return
web::Json<T>and let it serialize for you. - Returning one consistent shape and want to stay terse? Return
impl Responder.
📝 Phase 2 covered the input half of a handler - extractors pull
Path,Query, andJsonout of the request. This chapter is the output half. Input by extractor, output byResponder: that symmetry is the heart of how actix handlers are shaped.
Throughout we'll keep growing the articles API. Here's the model we're returning:
use Serialize;
What just happened: #[derive(Serialize)] from serde is the one prerequisite for sending a struct as JSON - without it, none of the .json() calls below would compile. Everything here assumes that derive is present.
HttpResponse: the workhorse
HttpResponse is a builder. Start with a status from a named method (Ok(), Created(), NotFound(), and friends), then finish it one of three ways: .json(&value) to serialize a body as JSON, .body("…") for a raw body, or .finish() for no body at all.
use ;
async
What just happened: HttpResponse::Ok() starts a 200 OK response, and .json(&article) serializes the struct into the body and sets Content-Type: application/json for you. The return type is impl Responder because HttpResponse implements Responder - read it as "returns something that can become a response."
Returning a list is the same move - serde serializes a Vec<Article> into a JSON array:
use ;
async
What just happened: .json() serializes anything that's Serialize, including a Vec - a list of articles becomes a JSON array with no extra ceremony.
HttpResponse earns "workhorse" status from its other status methods. Each is a different status code, pairing naturally with .json(), .body(), or .finish():
use HttpResponse;
// 201 Created - return the thing you just made.
Created.json;
// 204 No Content - success, nothing to send back (e.g. a DELETE).
NoContent.finish;
// 404 Not Found - no body needed.
NotFound.finish;
// 400 Bad Request - a plain-text explanation.
BadRequest.body;
What just happened: each builder picks a status; the finisher decides the body. Use .json() for a serializable value, .body() for plain text or raw bytes, .finish() when the status is the whole message. Named helpers cover the common codes; for anything exotic, HttpResponse::build(StatusCode::IM_A_TEAPOT) builds from a raw status.
💡 A useful instinct: the moment you want to control the status code, that's your cue to use
HttpResponse. The other return types are conveniences that pin the status for you - great until you need to say201or404.
web::Json: the shorthand
If your handler always returns a 200 with a JSON body, HttpResponse::Ok().json(…) is a touch verbose. web::Json is the shortcut: wrap your value in web::Json(...), return it, and actix serializes it as a 200 automatically.
use ;
async
What just happened: web::Json(article) is a responder that serializes its inner value and responds with 200 OK - the same as HttpResponse::Ok().json(&article), with less typing. You hand it the value by ownership, since the wrapper takes it over.
You met web::Json in Phase 2 as an extractor pulling a JSON body out of the request. Same type, both directions: input as a parameter, output as a return value.
⚠️ The tradeoff is real:
web::Jsonalways responds with 200. Need a201 Createdafter a POST, or a404when the article doesn't exist?web::Jsoncan't express it - go back toHttpResponse::Ok().json(...)/HttpResponse::Created().json(...)to choose the status. Reach forweb::Jsonon read paths where 200 is genuinely always correct; useHttpResponseeverywhere the status varies.
The trap: different branches, different types
This one bites everyone exactly once. Write a handler that returns 200 when it finds the article and 404 when it doesn't, and the compiler refuses to build it:
use ;
// ⚠️ This does NOT compile.
async
What just happened: the two branches return different concrete types - one HttpResponse, the other web::Json<Article>. impl Responder means "some single type that implements Responder," and a function can only return one concrete type, even if both implement it. The compiler error, "expected HttpResponse, found Json<Article>," is its way of saying "pick one type."
The fix: make every branch produce the same concrete type. Easiest choice is HttpResponse for both, since it can represent any status:
use ;
async
What just happened: both branches now return HttpResponse, so the function has one consistent return type and the compiler is happy. Status and body differ, but the type is identical, and that's all Rust cares about.
💡 Rule of thumb: the moment a handler can return more than one status, return
HttpResponsefrom every branch. Saveweb::Jsonand bareimpl Responderfor handlers with exactly one outcome shape. (A cleaner way to vary status exists - returning aResultand lettingResponseErrormap errors to status codes, covered in Phase 6. For now, oneHttpResponsetype in branchy handlers is the working answer.)
impl Responder vs HttpResponse: which to write
impl Responder in the return position means "I'm returning some type that implements Responder, and I'd rather not spell out which." It's ergonomic when there's a single, obvious response shape - a handler that always returns a web::Json<Article>, or always an HttpResponse.
HttpResponse is the explicit, flexible choice: write it when you need control over the status, when branches must agree on a type (the trap above), or when the signature should state plainly "this returns an HTTP response."
use ;
// Single shape, terse: impl Responder is a fine fit.
async
// Status varies / branches: be explicit with HttpResponse.
async
What just happened: the first handler has one outcome, so impl Responder keeps it clean. The second can return two statuses, so it names HttpResponse outright, and both branches line up with no fuss. The difference is flexibility (name HttpResponse) versus brevity (impl Responder for a single shape).
📝 Strings work too: a
&'static strorStringsends a200 OKtext body - handy for a health check, rarely what you want for a real API. The articles API speaks JSON, soHttpResponseandweb::Jsonare your day-to-day tools.
Recap
- A handler's return type must implement
Responder; the framework calls into that trait to turn your value into an HTTP response. HttpResponseis the workhorse: a builder with status helpers (Ok,Created,NotFound,NoContent,BadRequest) finished by.json(&value),.body("…"), or.finish().web::Json(value)is the shorthand for "200 + JSON body" - terse, but locked to status 200. Same wrapper, extractor on input, responder on output.- Different branches must return the same concrete type. Mixing
HttpResponseandweb::Jsonacrossifbranches won't compile; use oneHttpResponsetype everywhere a handler can vary status (or use Phase 6'sResult/ResponseError). - Choose
impl Responderfor single-shape, terse handlers;HttpResponsefor control and branchy handlers.
Quick check
[
{
"q": "A handler needs to return 200 with an article on success and 404 when it's missing. What return type keeps both branches compiling cleanly?",
"choices": ["web::Json from one branch, HttpResponse from the other", "HttpResponse from both branches", "impl Responder with the two different wrapper types", "String from both branches"],
"answer": 1,
"explain": "Both branches must produce the same concrete type. HttpResponse can represent any status, so returning it from every branch compiles and lets you send 200 or 404."
},
{
"q": "What status does returning web::Json(value) produce?",
"choices": ["Whatever you set with .status()", "201 Created", "200 OK, always", "204 No Content"],
"answer": 2,
"explain": "web::Json is the shorthand for a 200 OK JSON response. To choose a different status you must switch to HttpResponse::Ok().json(...) / HttpResponse::Created().json(...)."
},
{
"q": "You want a 204 No Content response after a successful delete. Which finisher fits?",
"choices": ["HttpResponse::NoContent().json(&article)", "HttpResponse::NoContent().finish()", "web::Json(())", "HttpResponse::NoContent().body(\"deleted\")"],
"answer": 1,
"explain": "A 204 carries no body, so .finish() is the right finisher - it sends the status with no payload. .json() and .body() would attach a body the status says shouldn't exist."
}
]
Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.
Check your understanding 3 questions
1. A handler needs to return 200 with an article on success and 404 when it's missing. What return type keeps both branches compiling cleanly?
2. What status does returning web::Json(value) produce?
3. You want a 204 No Content response after a successful delete. Which finisher fits?