Client
Spry Client is a generated client layer built from your Spry app.
It always starts from route metadata, so it can be generated even when you do not export openapi.json. OpenAPI only enhances the client with stronger request and response types.
Mental model
Think about Spry Client in two layers:
- route metadata gives Spry enough information to generate a callable client
- OpenAPI metadata adds typed inputs, queries, headers, outputs, and shared models
That means:
- you do not need to hand-write endpoints in the client
- you do not need to enable OpenAPI artifact output to generate a client
- you only need OpenAPI when you want stronger types
Configure client generation
Client generation is configured in spry.config.dart:
import 'package:spry/config.dart';
void main() {
defineSpryConfig(
target: .vm,
client: .new(
pkgDir: '.spry/client',
output: 'lib',
endpoint: 'https://api.example.com',
headers: .new({'x-client': 'web'}),
),
);
}ClientConfig fields
| Field | Description |
|---|---|
pkgDir | Package root for the generated client. Defaults to .spry/client. |
output | Output directory for generated Dart code. Defaults to lib. When pkgDir is set, this path is resolved relative to that package directory. |
endpoint | Default endpoint embedded into the generated SpryClient. It can still be overridden at runtime. |
headers | Static default global headers embedded into the generated SpryClient. |
Two header layers exist intentionally:
client.headersinspry.config.dartembeds static defaults into generated codeSpryClient(headers: ...)provides per-request runtime headers, such as tokens
Build commands
Spry Client lives under the normal build flow:
dart run spry build
dart run spry build clientUse them like this:
spry buildbuilds the app and also builds the client whenclientis configuredspry build clientonly builds the client artifact
Client generation does not depend on OpenAPI artifact output.
In other words, this is valid:
- route files contain
openapimetadata for type enhancement - no
openapi.outputis configured spry build clientstill generates a typed client
Output layout
Spry generates a thin client package. The shared runtime stays in package:spry/client.dart.
Typical output looks like this:
client/
├─ lib/
│ ├─ client.dart
│ ├─ routes.dart
│ ├─ params.dart
│ ├─ inputs.dart
│ ├─ headers.dart
│ ├─ queries.dart
│ ├─ outputs.dart
│ ├─ models.dart
│ ├─ routes/
│ ├─ params/
│ ├─ inputs/
│ ├─ headers/
│ ├─ queries/
│ ├─ outputs/
│ └─ models/
└─ pubspec.yamlThe generated directories follow different rules:
routes/mirrors route pathname structureparams/mirrors route pathname structureinputs/,headers/,queries/, andoutputs/mirror route file semantics and keep method suffixes when neededmodels/contains shared component schemas lifted from#/components/schemas/*
Generate into a standalone package
This is the simplest setup when you want a dedicated generated client package:
client: .new(
pkgDir: '.spry/client',
output: 'lib',
endpoint: 'http://127.0.0.1:4020',
)When pkgDir/pubspec.yaml does not exist, Spry creates a minimal package shell first.
This setup works well when:
- you want a generated client package next to the server
- you want to publish or share the client separately later
- you want a clean boundary between server code and generated client code
Generate into an existing package
You can also generate the client into an existing Dart or Flutter package:
client: .new(
pkgDir: '../app',
output: 'lib/generated/spry',
endpoint: 'https://api.example.com',
)This setup works well when:
- your app already has a package root
- you want generated files to live under
lib/generated/... - you want handwritten code and generated code in the same package
The important distinction is:
pkgDirpoints to the package rootoutputpoints to the generated code directory inside that package
Generated client shape
The generated entrypoint looks like a normal runtime object:
import 'package:example_client/client.dart';
final client = SpryClient();Generated route helpers are namespace-oriented:
final created = await client.users(
data: PostUsersInput(name: 'Seven'),
);
final user = await client.users.byId(
params: UsersByIdParams(id: 'u_1'),
);The generated runtime keeps fetch-like escape hatches in the same call signature:
datafor typed JSON inputbodyfor raw request bodyheadersfor request-level header overridesqueryfor request-level query overrides
body still wins over data.
OpenAPI enhancement
OpenAPI does not create the client. It enhances the client.
When route metadata includes useful OpenAPI information, Spry can generate:
*Inputfor safe JSON request bodies*Queryfor query parameters*Headersfor header parameters*Outputfor safe single-success JSON responses- shared
models/*for#/components/schemas/*
When OpenAPI information is missing or incomplete:
- the client is still generated
- request typing becomes weaker
- response typing falls back to
Response
Request body typing
Only safe JSON request bodies are typed.
For example, a route with a JSON body can generate:
await client.users(
data: PostUsersInput(name: 'Seven'),
);Non-JSON request bodies do not generate *Input. In those cases, keep using raw body:.
Query typing
If a route defines OpenAPI query parameters, Spry generates a typed query helper:
final query = GetSearchQuery(
q: 'spry',
page: 2,
startsAt: DateTime.now(),
);It also keeps a raw constructor for advanced cases:
final query = GetSearchQuery.raw({'q': 'spry'});Header typing
Header generation follows the same pattern:
final headers = GetProfileHeaders(
xApiKey: 'secret',
xRequestId: 'req_123',
);And still exposes:
final headers = GetProfileHeaders.raw({'x-api-key': 'secret'});Output typing
When a route has a safe single-success JSON response, Spry generates a route-local *Output.
That object is not just a plain DTO. It also keeps access to the original response:
final output = await client.root();
final response = output.toResponse();Shared models
Shared component schemas keep their declared names.
For example:
#/components/schemas/ParticipantbecomesParticipant#/components/schemas/AddressbecomesAddress
These types are generated into models/ and re-exported through models.dart.
Where to start
If you want to inspect a real generated client, use the dedicated example:
Start with these files:
- server/spry.config.dart
- client/lib/client.dart
- client/lib/routes/users/index.dart
- client/lib/queries/search/index.get.dart
- client/lib/headers/profile/index.get.dart
Next steps
After the client shape looks right:
- keep route metadata authoritative
- add OpenAPI only where stronger types improve DX
- choose whether the client should live in a standalone package or an existing package
- build with
spry build clientduring local iteration