Skip to main content

REST Adapter

PRO
Pro preview

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:

PropertyMeaning
operationopen, apply, or aggregate
methodConfigured FdcRestMethod
uriFully resolved URI, including an optional query payload
headersNormalized request headers
bodyBytesUTF-8 JSON bytes, or an empty list when no body is used
hasBodyWhether bodyBytes is non-empty
bodyTextDiagnostic 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:

MethodDefault placementNotes
GETqueryParameterCanonical JSON is encoded in the fdc query parameter.
POSTbodyUTF-8 JSON request body.
PUTbodyUTF-8 JSON request body.
PATCHbodyUTF-8 JSON request body.
DELETEbodyUTF-8 JSON request body.
HEADnoneRejected 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: true removes apply support from adapter capabilities.
  • aggregates: true enables onAggregate internally 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 resolved uri in 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.