Requests
The Request object represents an incoming HTTP request to your Relic server. It provides access to all information about the client's request, including the HTTP method, URL, headers, query parameters, and body content.
Every handler in Relic receives a Request object as a parameter, allowing you to inspect and process the incoming data before generating a response.
Understanding the request object
When a client makes a request to your server, Relic creates a Request object that encapsulates all the details of that request. This object provides type-safe access to request data.
Note on mutability:
Request.headersis immutable.Request.bodyis aBodywrapper and is mutable (handlers or middleware may replace it). The underlying body stream can be read only once.
The request flows through your middleware pipeline and reaches your handler, where you can extract the information you need to process the request and generate an appropriate response.
Key request properties
The Request object exposes several important properties:
method- The HTTP method (GET, POST, PUT, DELETE, etc.) as aMethodenum value.url- The complete original URI that was requested.headers- Type-safe access to HTTP headers.body- The request body wrapped in aBodyhelper. Useawait request.readAsString()for text, orrequest.read()to access the byte stream. Both are single-read.protocolVersion- The HTTP protocol version (typically1.1).
Accessing request data
HTTP method
The request method indicates what action the client wants to perform. Relic uses a type-safe Method enum rather than strings, which prevents typos and provides better IDE support.
app.get('/info', (final req) {
final method = req.method; // Method.get
return Response.ok(
body: Body.fromString('Received a ${method.name} request'),
);
});
Common methods include Method.get, Method.post, Method.put, Method.delete, Method.patch, and Method.options.
Request url and path
The url property provides the relative path and query parameters from the current handler's perspective. This is particularly useful when your handler is mounted at a specific path prefix.
app.get('/users/:id', (final req) {
final id = req.pathParameters.raw[#id]!;
final matchedPath = req.matchedPath;
final fullUri = req.url;
log('Matched path: $matchedPath, id: $id');
log('Full URI: $fullUri');
// Create a mock user object for the response and return it as JSON.
final user = {
'id': int.tryParse(id),
'name': 'User $id',
'email': 'user$id@example.com',
};
return Response.ok(
body: Body.fromString(jsonEncode(user), mimeType: MimeType.json),
);
});
Working with query parameters
Query parameters are key-value pairs appended to the URL after a question mark (?). They're commonly used to pass optional data, filters, or pagination information to your endpoints.
Single value parameters
You can access individual query parameter values through the queryParameters map. Each parameter is returned as a string, or null if not present.
app.get('/search', (final req) {
final query = req.queryParameters.raw['query'];
final page = req.queryParameters.raw['page'];
if (query == null) {
return Response.badRequest(
body: Body.fromString('Query parameter "query" is required'),
);
}
final pageNum = int.tryParse(page ?? '1') ?? 1;
final results = {
'query': query,
'page': pageNum,
'results': ['Result 1', 'Result 2', 'Result 3'],
};
return Response.ok(
body: Body.fromString(jsonEncode(results), mimeType: MimeType.json),
);
});
When a client requests /search?query=relic&page=2, the query variable will contain "relic" and the page variable will contain "2". Both values are strings, so you'll need to parse them if you need other types like integers.
Typed query parameters
Manually parsing query parameters can be tedious and error-prone. Relic provides typed parameter accessors that handle parsing automatically and give you compile-time type safety.
Define a parameter accessor once, then use it to extract typed values:
const pageParam = IntQueryParam('page');
const limitParam = IntQueryParam('limit');
const priceParam = DoubleQueryParam('price');
app.get('/products', (final req) {
// Automatically parsed as int (throws if missing or invalid)
final page = req.queryParameters.get(pageParam);
final limit = req.queryParameters.get(limitParam);
// Automatically parsed as double (throws if missing or invalid)
final maxPrice = req.queryParameters.get(priceParam);
final results = {
'page': page,
'limit': limit,
'max_price': maxPrice,
'products': ['Product A', 'Product B', 'Product C'],
};
return Response.ok(
body: Body.fromString(jsonEncode(results), mimeType: MimeType.json),
);
});
You can also use the nullable variant by calling the accessor directly:
app.get('/search-optional', (final req) {
final page = req.queryParameters(pageParam); // int? - null if missing
return Response.ok(body: Body.fromString('Page: $page'));
});
Relic provides these built-in typed query parameter accessors:
| Accessor | Type | Description |
|---|---|---|
IntQueryParam | int | Integer values (page numbers, limits) |
DoubleQueryParam | double | Decimal values (prices, percentages) |
NumQueryParam | num | Any numeric value |
QueryParam<T> | Custom | Create your own with a custom parser |
For custom types, use QueryParam<T> with your own parsing function:
// Custom enum parameter
const sortParam = QueryParam<SortOrder>('sort', SortOrder.parse);
// Custom date parameter
const fromParam = QueryParam<DateTime>('from', DateTime.parse);
To create a reusable accessor like the built-in ones, extend QueryParam<T> with a fixed decoder. The decoder must be a static function with signature T Function(String):
/// A reusable query parameter accessor for [DateTime] values.
final class DateTimeQueryParam extends QueryParam<DateTime> {
const DateTimeQueryParam(final String key) : super(key, DateTime.parse);
}
// Usage:
const fromDateParam = DateTimeQueryParam('from');
// In a handler: request.queryParameters.get(fromDateParam) returns DateTime
Typed parameter accessors are defined as const values, which means they can be defined once at the top level and reused across multiple handlers. This promotes consistency and reduces boilerplate.
Multiple values
Some query parameters can appear multiple times in a URL to represent lists or arrays. The queryParametersAll map provides access to all values for each parameter name.
app.get('/filter', (final req) {
final tags = req.url.queryParametersAll['tag'] ?? [];
final results = {
'tags': tags,
'filtered_items':
tags.map((final tag) => 'Item tagged with $tag').toList(),
};
return Response.ok(
body: Body.fromString(jsonEncode(results), mimeType: MimeType.json),
);
});
For a request to /filter?tag=dart&tag=server&tag=web, the tags variable will be a list containing ["dart", "server", "web"]. This allows you to handle multiple selections or filters cleanly.
Reading headers
HTTP headers carry metadata about the request, such as content type, authentication credentials, and client information. Relic provides type-safe access to common headers, automatically parsing them into appropriate Dart types.
Type-safe header access
Instead of working with raw string values, Relic's type-safe headers give you properly typed objects. This eliminates parsing errors and provides better code completion in your IDE.
app.get('/headers-info', (final req) {
final request = req;
// Get typed header values.
final mimeType = request.mimeType; // MimeType? (from Content-Type)
final userAgent = request.headers.userAgent; // String?
final contentLength = request.headers.contentLength; // int?
final info = {
'browser': userAgent ?? 'Unknown',
'content_type': mimeType?.toString() ?? 'None',
'content_length': contentLength ?? 0,
};
return Response.ok(
body: Body.fromString(jsonEncode(info), mimeType: MimeType.json),
);
});
In this example, the mimeType is automatically parsed into a MimeType object, and contentLength is parsed into an integer rather than a string. This type safety helps catch errors at compile time.
Authorization headers
The authorization header receives special handling in Relic to distinguish between different authentication schemes like Bearer tokens and Basic authentication.
app.get('/protected', (final req) {
final auth = req.headers.authorization;
if (auth is BearerAuthorizationHeader) {
final token = auth.token;
// In a real application, validate the token here.
return Response.ok(
body: Body.fromString(
jsonEncode({
'message': 'Access granted',
'token_length': token.length,
}),
mimeType: MimeType.json,
),
);
} else if (auth is BasicAuthorizationHeader) {
final username = auth.username;
// In a real application, validate the credentials here.
return Response.ok(
body: Body.fromString(
jsonEncode({'message': 'Basic auth received', 'username': username}),
mimeType: MimeType.json,
),
);
} else {
return Response.unauthorized();
}
});
Relic automatically parses the authorization header and creates the appropriate header object type, making it easy to handle different authentication schemes in a type-safe manner.
Reading the request body
The request body contains data sent by the client, typically in POST, PUT, or PATCH requests. Relic provides multiple ways to read body content depending on your needs.
The request body can only be read once. This is because the body is a stream that gets consumed as it's read. Attempting to read the body multiple times will result in a StateError.
// ❌ WRONG - This will throw an error
final first = await request.readAsString();
final second = await request.readAsString(); // StateError!
// ✅ CORRECT - Read once and store the result
final body = await request.readAsString();
// Use 'body' as many times as needed
Reading as string
The most common way to read the body is as a string, which works well for JSON, XML, or plain text data. The readAsString method automatically handles character encoding based on the Content-Type header.
app.post('/submit', (final req) async {
final bodyText = await req.readAsString();
return Response.ok(
body: Body.fromString(
jsonEncode({'received': bodyText, 'length': bodyText.length}),
mimeType: MimeType.json,
),
);
});
The method defaults to UTF-8 encoding if no encoding is specified in the request headers.
Parsing JSON data
For JSON APIs, you'll typically read the body as a string and then decode it using Dart's jsonDecode function. This two-step process gives you control over error handling.
app.post('/api/users', (final req) async {
try {
final bodyText = await req.readAsString();
final data = jsonDecode(bodyText) 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,
),
);
}
// Create a mock user object with generated data.
final newUser = {
'id': DateTime.now().millisecondsSinceEpoch,
'name': name,
'email': email,
'created_at': DateTime.now().toIso8601String(),
};
return Response.ok(
body: Body.fromString(
jsonEncode({'message': 'User created', 'user': newUser}),
mimeType: MimeType.json,
),
);
} catch (e) {
return Response.badRequest(
body: Body.fromString(
jsonEncode({'error': 'Invalid JSON: $e'}),
mimeType: MimeType.json,
),
);
}
});
This example shows proper validation of both the JSON structure and the required fields, providing clear error messages when something is wrong.
Reading as a byte stream
For large files or binary data, you can read the body as a stream of bytes to avoid loading everything into memory at once. This is essential for handling file uploads or large payloads efficiently.
app.post('/upload', (final req) async {
if (req.isEmpty) {
return Response.badRequest(
body: Body.fromString(
jsonEncode({'error': 'Request body is required'}),
mimeType: MimeType.json,
),
);
}
final stream = req.read(); // Stream<Uint8List>
int totalBytes = 0;
await for (final chunk in stream) {
totalBytes += chunk.length;
// Process chunk...
}
return Response.ok(
body: Body.fromString(
jsonEncode({
'message': 'Upload successful',
'bytes_received': totalBytes,
}),
mimeType: MimeType.json,
),
);
});
By processing the data in chunks, your server can handle large uploads without running out of memory.
Checking if body is empty
Before attempting to read the body, you can check if it's empty using the isEmpty property. This is useful when you want to require a body for certain requests.
isEmpty is based on the known content length when available. For requests using chunked transfer encoding (unknown length up front), isEmpty may return false even if no data is ultimately sent. Prefer defensive reads for streaming uploads.
app.post('/upload', (final req) async {
if (req.isEmpty) {
return Response.badRequest(
body: Body.fromString(
jsonEncode({'error': 'Request body is required'}),
mimeType: MimeType.json,
),
);
}
final stream = req.read(); // Stream<Uint8List>
int totalBytes = 0;
await for (final chunk in stream) {
totalBytes += chunk.length;
// Process chunk...
}
return Response.ok(
body: Body.fromString(
jsonEncode({
'message': 'Upload successful',
'bytes_received': totalBytes,
}),
mimeType: MimeType.json,
),
);
});
This check doesn't consume the body stream, so you can still read the body afterward.
Final tips
The Request object is your gateway to understanding what clients are asking for. By leveraging Relic's type-safe API, you can build secure, reliable handlers that properly validate and process client input.
Key principles for working with requests include accessing data through the Request parameter, using type-safe properties for methods and headers, reading query parameters safely, and handling request bodies appropriately. Remember that request bodies can only be read once, so design your handlers to consume the body early in the processing pipeline.
Always validate all incoming data since query parameters, headers, and body content come from untrusted sources. Use try-catch blocks for JSON parsing and validation to provide meaningful error responses. By following these patterns, you'll create handlers that are both secure and user-friendly.
Examples & further reading
Examples
- Requests example - Comprehensive example covering complete request-response cycles.
API documentation
- Request class - HTTP request object.
- Method enum - HTTP methods enumeration.
- Headers class - Type-safe HTTP headers.
- Body class - Request/response body handling.
- AuthorizationHeader class - Authorization header parsing.
Further reading
- HTTP request methods - Mozilla documentation on HTTP methods.
- HTTP headers - Mozilla documentation on HTTP headers.
- What is a URL? - Mozilla documentation on URL structure and query parameters.