Skip to content

Responses

Build an explicit response when the status, content type, or headers matter:

Route.post('/posts', (Request request) async {
final post = await posts.create(request.all());
return Response.json(post, status: 201);
});

For simple handlers, Maat can also turn common Dart values into responses for you.

Returning Responses

Every route returns something, and Maat normalises it:

Route.get('/a', (Request request) => '<h1>Hello</h1>'); // 200 text/html
Route.get('/b', (Request request) => {'ok': true}); // 200 application/json
Route.get('/c', (Request request) => [1, 2, 3]); // 200 application/json
Route.get('/d', (Request request) => null); // 204 No Content
Route.get('/e', (Request request) => Response.json({})); // exactly what you built

Maps, lists, and objects with toJson() become JSON. Strings become HTML, null becomes 204, and a Response passes through untouched. Other values use their toString() representation as plain text. Use Response.text() when a string must be served as text/plain.

Building Responses

Response.json({'id': 1}); // 200, application/json
Response.json({'id': 1}, status: 201);
Response.text('pong'); // text/plain
Response.html('<h1>Hi</h1>'); // text/html
Response.redirect('/login'); // 302 with a location header
Response.redirect('/login', status: 301);
Response.noContent(); // 204

Stream a large or incremental body without collecting it in memory:

Response.stream(file.openRead(), headers: {
'content-type': 'application/octet-stream',
});

PublicFiles uses this path for files under public/.

Prefer a named constructor because it makes the intended content type clear. A bare Response(...) holding a map or list is JSON-encoded when sent and receives a JSON content type if you did not provide one, but Response.json makes that contract explicit at construction time.

Attaching Headers

Responses are immutable. Each of these returns a new response rather than mutating the original:

Response.json(post)
.header('x-request-id', id)
.withHeaders({'cache-control': 'no-store'})
.status(201);

Header names are lower-cased, so Content-Type and content-type are the same header. A header you pass explicitly wins over the default one a constructor would set.

Header values are part of the HTTP response boundary. Validate untrusted values before using them in headers such as location or content-disposition.

Inspecting a Response

response.statusCode; // int
response.headers; // unmodifiable, lower-cased keys
response.body; // the stored String, bytes, stream, or other value

JSON Encoding

Response.json encodes with Response.encodeJson, which understands any object exposing a toJson() method:

class Post {
Post(this.id, this.title);
final int id;
final String title;
Map<String, dynamic> toJson() => {'id': id, 'title': title};
}
Route.get('/posts/{id}', (Request request, String id) => Response.json(Post(1, 'Hello')));
// {"id":1,"title":"Hello"}

DateTime values are encoded as ISO-8601 strings. Unsupported values passed directly to Response.json throw a JSON encoding error; when an unsupported value is returned from a route, normalisation uses its string form instead.

Redirects

Redirect responses default to 302 and set the location header:

return Response.redirect('/login');
return Response.redirect(route('posts.show', {'id': post.id}), status: 303);

Use a local path or a destination you trust. If a request parameter controls the target without validation, the endpoint becomes an open redirect.

Aborting

Throw an HTTP exception from anywhere — a handler, a middleware, a service deep in your application — and the exception handler renders it:

abort(404);
abort(403, 'You do not own this post.');
abort(429, 'Slow down.', {'retry-after': '30'});

The typed exceptions are equivalent and read better where the status is fixed:

throw NotFoundHttpException();
throw UnauthorizedHttpException('Token expired.');
throw ForbiddenHttpException('This action is unauthorized.');
throw TooManyRequestsHttpException();

Each renders with its status and a {"message": "..."} body, and carries any headers it was given. Omit the message and you get the standard reason phrase for the status.

To return a specific response from deep in a call stack, throw it:

throw HttpResponseException(Response.json({'reason': 'locked'}, status: 423));

That response is returned exactly as built, and is never logged as an error.

See Error Handling for how these become responses, and how to customise the rendering.