Skip to main content

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.