REST Adapter
This feature is part of FD Components Pro, which is documented as a preview and is not publicly available yet. See Editions and Availability.
FdcRestAdapter adds transport-neutral HTTP request modeling to the canonical FDC JSON Contract. It extends FdcJsonAdapter: REST owns URI resolution, headers, HTTP methods, payload placement, status validation, and dispatch, while the JSON layer owns envelopes, typed materialization, apply semantics, aggregates, capabilities, and timeouts.
FDC does not require a particular HTTP package. The application supplies one onSend callback and keeps ownership of authentication, retries, interceptors, certificates, telemetry, and client lifecycle.
Minimal configuration
final adapter = FdcRestAdapter(
source: 'customers',
baseUri: Uri.parse('https://api.example.com/fdc'),
onSend: sendFdcRequest,
);
final customers = FdcDataSet(
fields: const [
FdcIntegerField(name: 'customer_id', isKey: true),
FdcStringField(name: 'company', size: 120),
FdcStringField(name: 'city', size: 80),
],
adapter: adapter,
paging: const FdcDataPagingOptions(
enabled: true,
pageSize: 100,
),
);
await customers.open();
The baseUri must be absolute. With the default configuration, FDC sends JSON POST requests to:
https://api.example.com/fdc/customers/open
https://api.example.com/fdc/customers/apply
https://api.example.com/fdc/customers/aggregate
The apply endpoint is omitted when readOnly: true. The aggregate endpoint is used only when aggregates: true.
Implement onSend with package:http
import 'package:http/http.dart' as http;
Future<FdcRestResponse> sendFdcRequest(FdcRestRequest request) async {
final nativeRequest = http.Request(
request.method.name.toUpperCase(),
request.uri,
)
..headers.addAll(request.headers)
..bodyBytes = request.bodyBytes;
final nativeResponse = await nativeRequest.send();
final responseBytes = await nativeResponse.stream.toBytes();
return FdcRestResponse(
statusCode: nativeResponse.statusCode,
bodyBytes: responseBytes,
headers: nativeResponse.headers,
reasonPhrase: nativeResponse.reasonPhrase,
);
}
FdcRestRequest exposes:
| Property | Meaning |
|---|---|
operation | open, apply, or aggregate |
method | Configured FdcRestMethod |
uri | Fully resolved URI, including an optional query payload |
headers | Normalized request headers |
bodyBytes | UTF-8 JSON bytes, or an empty list when no body is used |
hasBody | Whether bodyBytes is non-empty |
bodyText | Diagnostic UTF-8 representation of the body |
Always return an FdcRestResponse, even when the native client uses a different response model.
Authentication and custom headers
Use headersBuilder for per-request authentication, tenant information, localization, and correlation IDs:
final adapter = FdcRestAdapter(
source: 'customers',
baseUri: apiBase,
headersBuilder: (operation) async => <String, String>{
'authorization': 'Bearer ${await tokenProvider.token()}',
'x-tenant': currentTenantId,
'x-fdc-operation': operation.name,
},
onSend: sendFdcRequest,
);
FDC adds accept: application/json by default. Requests with a body also receive content-type: application/json; charset=utf-8.
Header names are normalized to lowercase. Custom values override default headers case-insensitively, so Content-Type and content-type address the same header.
Custom endpoint URIs
Use uriBuilder when the backend route differs from the default <base>/<source>/<operation> shape:
FdcRestAdapter(
source: 'customers',
baseUri: Uri.parse('https://api.example.com/v2'),
uriBuilder: (context) => context.baseUri.replace(
pathSegments: <String>[
...context.baseUri.pathSegments.where((part) => part.isNotEmpty),
context.operation.name,
context.source,
],
),
onSend: sendFdcRequest,
)
FdcRestUriContext contains the validated baseUri, trimmed source, and current FdcRestOperation. Existing query parameters on the returned URI are preserved.
HTTP methods and payload placement
Methods are configured per operation:
FdcRestAdapter(
source: 'customers',
baseUri: apiBase,
openMethod: FdcRestMethod.get,
applyMethod: FdcRestMethod.patch,
aggregateMethod: FdcRestMethod.post,
onSend: sendFdcRequest,
)
Default payload placement depends on the method:
| Method | Default placement | Notes |
|---|---|---|
GET | queryParameter | Canonical JSON is encoded in the fdc query parameter. |
POST | body | UTF-8 JSON request body. |
PUT | body | UTF-8 JSON request body. |
PATCH | body | UTF-8 JSON request body. |
DELETE | body | UTF-8 JSON request body. |
HEAD | none | Rejected for dataset operations because a JSON response body is required. |
FDC intentionally rejects GET request bodies. To customize a GET payload:
FdcRestAdapter(
source: 'customers',
baseUri: apiBase,
openMethod: FdcRestMethod.get,
openPayloadPlacement: FdcRestPayloadPlacement.queryParameter,
queryParameterName: 'query',
onSend: sendFdcRequest,
)
The canonical JSON string becomes one URI-encoded query value. Existing repeated query parameters are preserved, and a previous value with the configured payload name is replaced.
FdcRestPayloadPlacement.none is available for specialized endpoints, but the backend then has no canonical request payload. Use it only when the URI and surrounding transport context fully describe the operation.
Read-only and aggregate modes
final adapter = FdcRestAdapter(
source: 'sales',
baseUri: apiBase,
readOnly: true,
aggregates: true,
onSend: sendFdcRequest,
);
readOnly: trueremoves apply support from adapter capabilities.aggregates: trueenablesonAggregateinternally and advertises aggregate capability.
Filtering, sorting, search, paging, total count, and selected-key filtering remain part of the backend JSON contract.
Metadata and timeouts
The REST adapter inherits JSON metadata and timeout behavior:
FdcRestAdapter(
source: 'orders',
baseUri: apiBase,
openTimeout: const Duration(seconds: 30),
applyTimeout: const Duration(seconds: 20),
metaBuilder: (operation) => <String, Object?>{
'requestId': createRequestId(),
'operation': operation.name,
},
onSend: sendFdcRequest,
)
openTimeout covers open and aggregate transport work; applyTimeout covers apply. Duration.zero disables the corresponding timeout.
JSON metadata belongs inside the canonical payload. HTTP-specific values such as authorization tokens belong in headersBuilder.
Responses and HTTP errors
Successful 2xx response bytes are passed to the normal JSON envelope parser. The response must therefore satisfy the operation-specific JSON Contract.
return FdcRestResponse.text(
statusCode: 200,
body: jsonEncode(<String, Object?>{
'source': 'customers',
'rows': rows,
'totalCount': totalCount,
}),
);
Every non-2xx status becomes FdcDataAdapterException with:
- operation name;
- code
http_<statusCode>; statusCode,reasonPhrase, and resolveduriin details.
The REST adapter does not interpret an error response body as a successful JSON apply envelope. Convert application-specific transport failures into an appropriate HTTP status or return a valid apply response with success: false for business-level apply errors.
Deterministic transport tests
Because onSend is an ordinary callback, adapter behavior can be tested without a server:
FdcRestRequest? capturedRequest;
final adapter = FdcRestAdapter(
source: 'customers',
baseUri: Uri.parse('https://api.example.test'),
onSend: (request) {
capturedRequest = request;
return FdcRestResponse.text(
statusCode: 200,
body: '{"source":"customers","rows":[],"totalCount":0}',
);
},
);
await adapter.load(const FdcDataLoadRequest());
assert(capturedRequest!.uri.path == '/customers/open');
assert(capturedRequest!.method == FdcRestMethod.post);
This boundary is useful for verifying endpoint selection, headers, payload encoding, and error mapping independently from a real HTTP client.