Building Providers
Providers own a service integration and its configuration. Configuration is
constructed in Dart, validated before boot, and published through
ConfigStore. There is no YAML manifest, dot-notation override, or reload
callback in the provider API.
Provider lifecycle
import 'package:routed/routed.dart';
class MailConfig implements ValidatableConfiguration {
const MailConfig({required this.host, this.port = 587});
final String host;
final int port;
void validate(ConfigValidationContext context) {
context.require(host.trim().isNotEmpty, 'host', 'must not be empty');
context.require(port > 0 && port < 65536, 'port', 'must be valid');
}
}
class MailProvider extends ServiceProvider
with ProvidesTypedConfiguration<MailConfig> {
MailProvider(this.configuration);
final MailConfig configuration;
void register(Container container) {
container.singleton<Mailer>(
(_) async => Mailer(host: configuration.host, port: configuration.port),
);
}
Future<void> boot(Container container) async {
await container.make<Mailer>().connect();
}
Future<void> cleanup(Container container) async {
if (container.has<Mailer>()) {
await container.make<Mailer>().close();
}
}
}
register declares bindings synchronously. boot runs after providers have
registered, and cleanup releases owned resources.
Registering providers
Pass providers explicitly when constructing the engine:
final engine = await Engine.create(
providers: [
CoreServiceProvider(
EngineConfig(
security: const EngineSecurityFeatures(maxRequestSize: 10 << 20),
),
),
RoutingServiceProvider(const RoutingConfig()),
MailProvider(const MailConfig(host: 'smtp.example.com')),
],
);
Engine.defaultProviders contains only core and routing. The package:routed
barrel registers official feature providers, so Engine.builtins can be used
when an application wants the complete official catalogue:
import 'package:routed/routed.dart';
final engine = await Engine.create(providers: [
...Engine.builtins,
MailProvider(const MailConfig(host: 'smtp.example.com')),
]);
Registering the same provider type twice is not a configuration mechanism; the first provider wins. Choose one typed configuration instance for each provider.
Runtime environment and secrets
Deployment values belong at the application boundary. Pass them as a typed
RuntimeContext instead of having providers read process environment variables
or configuration files directly:
final runtime = RuntimeContext(
environment: RuntimeEnvironment({'PUBLIC_ORIGIN': 'https://example.test'}),
secrets: RuntimeSecrets({'SMTP_PASSWORD': passwordFromSecretStore}),
);
final engine = await Engine.create(
runtime: runtime,
providers: [
CoreServiceProvider(),
RoutingServiceProvider(),
MailProvider(MailConfig(host: runtime.environment.requiredString('SMTP_HOST'))),
],
);
Provider validation can also inspect context.runtime. Secret values are not
included in validation summaries or generic error responses.
Typed lookup
Inside a handler or provider, retrieve configuration by type:
final settings = ctx.config<MailConfig>();
final engineSettings = engine.configStore.get<EngineConfig>();
The lookup is immutable after startup. Missing or duplicate configuration types fail explicitly instead of silently falling back to a string key.
Dependencies and wiring
Use ProvidesDependencies when boot requires another service:
class ReportsProvider extends ServiceProvider with ProvidesDependencies {
List<Type> get dependencies => [Mailer];
void register(Container container) {
container.singleton<Reports>((c) async => Reports(await c.make<Mailer>()));
}
}
Use withMiddleware and withService only for imperative wiring owned by the
application. Feature configuration belongs in the provider constructor.
Testing
Providers can be tested with an in-memory engine and their real typed configuration:
test('mail provider publishes its configuration', () async {
const config = MailConfig(host: 'smtp.test');
final engine = await Engine.create(providers: [
CoreServiceProvider(),
RoutingServiceProvider(),
MailProvider(config),
]);
expect(engine.configStore.get<MailConfig>(), same(config));
await engine.close();
});
Invalid configurations fail during initialization with
ConfigValidationException, before requests are accepted.