LogoRevali

Response

How an endpoint's return value becomes the HTTP response, and how to change the body, status and headers

An endpoint's return value becomes the response body. Revali picks the wire format from the return type: most values are JSON-encoded and wrapped as {"data": ...}, while StringContent, bytes, streams, File and MemoryFile are sent raw. To change the status code, headers or body from a lifecycle component, use the Response object.

Minimal example#

routes/controllers/greeting_controller.dart
import 'package:revali_router/revali_router.dart';

@Controller('greeting')
class GreetingController {
  const GreetingController();

  @Get()
  String json() => 'Hello world!';

  @Get('text')
  StringContent text() => const StringContent('Hello world!');
}
curl -i http://localhost:8080/api/greeting
# content-type: application/json
# {"data":"Hello world!"}

curl -i http://localhost:8080/api/greeting/text
# content-type: text/plain
# Hello world!

Return types#

Return typeBodyContent-Type
void, Future<void> Empty, status 200 none
String, int, double, bool {"data": value} application/json
Map, List, Set, Iterable {"data": [...]} / {"data": {...}} application/json
Class with toJson() {"data": toJson()} application/json
Record (a, b) {"data": [a, b]} application/json
Record ({a, b}) {"data": {"a": ..., "b": ...}} application/json
Record (a, {b}) {"data": [a, {"b": ...}]} application/json
StringContentThe raw stringtext/plain
List<int> The raw bytes application/octet-stream
File (dart:io) The file's contents, as when assigning body from the file's extension
MemoryFile The in-memory bytes, as when assigning body the given MIME type
Stream<T> Each event written as it is produced; each event is encoded by the rules above application/octet-stream
Future<T>Same as Tsame as T

Nested custom types are converted too: List<User>, Map<String, User> and records containing User all call User.toJson().

class User {
  const User({required this.name});

  final String name;

  Map<String, dynamic> toJson() => {'name': name};
}

@Get('users')
List<User> users() => const [User(name: 'Ada')];

GET /api/greeting/users returns {"data":[{"name":"Ada"}]}.

Errors#

Throw to send an error response. An uncaught exception becomes 500; a binding failure (MissingArgumentException) becomes 400. For a specific status with a machine-readable body, throw an HttpError:

@Get('users/:id')
Future<User> user(@Param() String id) async {
  final user = await repo.find(id);
  if (user == null) {
    throw const HttpError.notFound(code: 'user_not_found', message: 'No such user');
  }
  return user;
}
{"error": {"code": "user_not_found", "message": "No such user"}}

See Error responses for HttpError, and exception catchers to map your own exceptions.

The Response object#

Response is available as an implied parameter in endpoints and lifecycle components, and as context.response.

MemberTypeUse
statusCode int (read/write) See Status code.
headers Headers See Headers.
headers.setCookies SetCookies See Cookies.
body Body; setter takes any supported value Read with body.data , replace with body = value , add a key to a JSON body with body['key'] = value .

Assigning body accepts the same values as a return type, including dart:io File and MemoryFile. A File is streamed with Content-Type from its extension, Content-Disposition: attachment; filename="...", Last-Modified, and Range support. A MemoryFile sends in-memory bytes with a given MIME type and file name:

import 'dart:io';

@Get('report')
void report(Response response) {
  response.body = File('reports/latest.pdf');
}

@Get('export')
void export(Response response) {
  response.body = MemoryFile.from(
    'id,name\n1,Ada\n',
    mimeType: 'text/csv',
    basename: 'users',
    extension: 'csv',
  );
}

Changing the body in a post-interceptor, after the handler has run:

class AddTimestamp implements LifecycleComponent {
  const AddTimestamp();

  InterceptorPostResult stamp(Response response) {
    response.body['timestamp'] = DateTime.now().toIso8601String();
  }
}

A value returned by the endpoint overwrites anything set on response.body inside the endpoint. Endpoints that set response.body themselves should return void.

Related: Status code · Headers · Cookies · Server-Sent Events · WebSockets