relic-request-response

star 124

Handle HTTP requests and create responses with Body, headers, and status codes in Relic. Use when reading request data, parsing JSON, building API responses, working with headers, or streaming content.

serverpod By serverpod schedule Updated 2/26/2026

name: relic-request-response description: Handle HTTP requests and create responses with Body, headers, and status codes in Relic. Use when reading request data, parsing JSON, building API responses, working with headers, or streaming content.

Relic Requests & Responses

Request object

Every handler receives a Request with these key properties:

  • method -- Method enum (Method.get, Method.post, etc.)
  • url -- full original URI
  • headers -- type-safe header access
  • body -- Body wrapper (single-read stream)
  • pathParameters -- route parameters set by routing
  • queryParameters -- query string parameters

Request.headers is immutable. Request.body is mutable (middleware may replace it), but the underlying stream can only be read once.

Query parameters

Raw access

app.get('/search', (req) {
  final query = req.queryParameters.raw['query'];
  final page = req.queryParameters.raw['page'];
  return Response.ok(body: Body.fromString('Search: $query, page: $page'));
});

Typed query parameters

const pageParam = IntQueryParam('page');
const limitParam = IntQueryParam('limit');
const priceParam = DoubleQueryParam('price');

app.get('/products', (req) {
  final page = req.queryParameters.get(pageParam);    // int (throws if missing)
  final limit = req.queryParameters.get(limitParam);   // int
  final maxPrice = req.queryParameters.get(priceParam); // double
  return Response.ok(body: Body.fromString('page=$page limit=$limit price=$maxPrice'));
});

Nullable variant:

final page = req.queryParameters(pageParam); // int? -- null if missing

Built-in: IntQueryParam, DoubleQueryParam, NumQueryParam. Custom:

const sortParam = QueryParam<SortOrder>('sort', SortOrder.parse);
const fromParam = QueryParam<DateTime>('from', DateTime.parse);

Reusable specialization:

final class DateTimeQueryParam extends QueryParam<DateTime> {
  const DateTimeQueryParam(String key) : super(key, DateTime.parse);
}

Multiple values

app.get('/filter', (req) {
  final tags = req.url.queryParametersAll['tag'] ?? []; // List<String>
  return Response.ok(body: Body.fromString('Tags: $tags'));
});
// GET /filter?tag=dart&tag=server → Tags: [dart, server]

Reading headers

app.get('/info', (req) {
  final userAgent = req.headers.userAgent;        // String?
  final contentLength = req.headers.contentLength; // int?
  final mimeType = req.mimeType;                   // MimeType?
  // ...
});

Authorization

app.get('/protected', (req) {
  final auth = req.headers.authorization;

  if (auth is BearerAuthorizationHeader) {
    final token = auth.token;
    return Response.ok(body: Body.fromString('Token: $token'));
  } else if (auth is BasicAuthorizationHeader) {
    final username = auth.username;
    return Response.ok(body: Body.fromString('User: $username'));
  }
  return Response.unauthorized();
});

Reading the request body

The body can only be read once. A second read throws StateError.

As string

app.post('/submit', (req) async {
  final text = await req.readAsString();
  return Response.ok(body: Body.fromString('Received: $text'));
});

JSON parsing

app.post('/api/users', (req) async {
  try {
    final body = await req.readAsString();
    final data = jsonDecode(body) as Map<String, dynamic>;

    final name = data['name'] as String?;
    final email = data['email'] as String?;
    if (name == null || email == null) {
      return Response.badRequest(
        body: Body.fromString(
          jsonEncode({'error': 'Name and email are required'}),
          mimeType: MimeType.json,
        ),
      );
    }

    return Response.ok(
      body: Body.fromString(
        jsonEncode({'message': 'User created', 'name': name}),
        mimeType: MimeType.json,
      ),
    );
  } catch (e) {
    return Response.badRequest(
      body: Body.fromString(jsonEncode({'error': 'Invalid JSON: $e'}), mimeType: MimeType.json),
    );
  }
});

Byte stream

app.post('/upload', (req) async {
  if (req.isEmpty) {
    return Response.badRequest(body: Body.fromString('Body required'));
  }

  final stream = req.read(); // Stream<Uint8List>
  int totalBytes = 0;
  await for (final chunk in stream) {
    totalBytes += chunk.length;
  }
  return Response.ok(body: Body.fromString('Received $totalBytes bytes'));
});

Size-limited reads

const maxFileSize = 10 * 1024 * 1024; // 10 MB

final sink = file.openWrite();
try {
  await sink.addStream(req.read(maxLength: maxFileSize));
} on MaxBodySizeExceeded {
  return Response.badRequest(body: Body.fromString('File too large'));
} finally {
  await sink.close();
}

Creating responses

Status code constructors

Response.ok(body: Body.fromString('Success'))           // 200
Response.noContent()                                     // 204
Response.badRequest(body: Body.fromString('Bad input'))  // 400
Response.unauthorized()                                  // 401
Response.notFound()                                      // 404
Response.internalServerError()                           // 500
Response(418, body: Body.fromString('I am a teapot'))   // custom

Body types

// Text (auto-detects MIME: JSON, HTML, XML, or plain text)
Body.fromString('Hello')                                    // text/plain
Body.fromString('{"key": "value"}')                         // application/json
Body.fromString('<!DOCTYPE html><html>...</html>')          // text/html

// Explicit MIME type
Body.fromString('<html>...</html>', mimeType: MimeType.html)
Body.fromString(jsonEncode(data), mimeType: MimeType.json)

// Binary (auto-detects PNG, JPEG, PDF, etc.)
Body.fromData(imageBytes)                                    // image/png, etc.
Body.fromData(data, mimeType: MimeType.octetStream)         // explicit

// Streaming (for large payloads)
Body.fromDataStream(fileStream, contentLength: fileSize)    // known size
Body.fromDataStream(dynamicStream)                          // chunked encoding

// Empty
Body.empty()

Encoding defaults to UTF-8. Override with:

Body.fromString('Café', mimeType: MimeType.plainText, encoding: latin1)

Response headers

app.get('/api/data', (req) {
  final headers = Headers.build((h) {
    h.cacheControl = CacheControlHeader(maxAge: 3600, publicCache: true);
    h['X-Custom-Header'] = ['value'];
  });

  return Response.ok(
    headers: headers,
    body: Body.fromString(jsonEncode({'status': 'ok'}), mimeType: MimeType.json),
  );
});

HTML response

app.get('/page', (req) {
  final html = '<!DOCTYPE html><html><body><h1>Welcome</h1></body></html>';
  return Response.ok(body: Body.fromString(html, mimeType: MimeType.html));
});

Streaming response

app.get('/stream', (req) async {
  Stream<Uint8List> generate() async* {
    for (var i = 0; i < 100; i++) {
      await Future<void>.delayed(Duration(milliseconds: 50));
      yield utf8.encode('{"item": $i}\n');
    }
  }

  return Response.ok(
    body: Body.fromDataStream(generate(), mimeType: MimeType.json),
  );
});

Modifying requests

Request is immutable. Use copyWith to create a modified copy:

final rewritten = request.copyWith(
  url: request.url.replace(path: '/new-path'),
);

final modified = request.copyWith(
  url: request.url.replace(path: '/other'),
  headers: Headers.build((h) => h['X-Custom'] = ['value']),
);
Install via CLI
npx skills add https://github.com/serverpod/relic --skill relic-request-response
Repository Details
star Stars 124
call_split Forks 14
navigation Branch main
article Path SKILL.md
More from Creator