Advanced: Dependency Injection
The API server is built with a clean, layered architecture that separates concerns and promotes testability. At the heart of this design is the use of a service layer and a centralized dependency injection (DI) mechanism.
The AppDependencies
Singleton
Section titled “The AppDependencies Singleton”The AppDependencies
class (lib/src/config/app_dependencies.dart
) is a singleton responsible for initializing and providing access to all major application-wide dependencies.
When the server first starts, the init()
method on this singleton is called. It performs several critical setup tasks in order:
- Initializes the Database Connection: Establishes a connection to the MongoDB instance specified in the environment variables.
- Seeds the Database: Runs a seeding service to populate the database with initial data (countries, topics, etc.) if it doesn’t already exist.
- Initializes Data Clients: Creates instances of the
DataMongodb
clients for each data model. These clients are the lowest-level components that directly interact with the database. - Initializes Repositories: Wraps each data client in a
DataRepository
. Repositories provide a clean abstraction layer over the data clients. - Initializes Services: Creates instances of all core business logic services (e.g.,
AuthService
,DashboardSummaryService
), injecting the repositories and other services they depend on.
Dependency Injection in Requests
Section titled “Dependency Injection in Requests”Once initialized, these dependencies are made available to every incoming request using Dart Frog’s middleware and provider system.
The root middleware (/routes/_middleware.dart
) is responsible for this. For each request, it:
- Ensures
AppDependencies.instance.init()
has been called. - Uses a series of
.use(provider<T>((_) => deps.dependency))
calls to inject each repository and service from theAppDependencies
singleton into the request context.
Accessing Dependencies in Route Handlers
Section titled “Accessing Dependencies in Route Handlers”Because of this DI setup, any route handler or downstream middleware can easily access a required dependency by calling context.read<T>()
.
Example:
// Read the AuthService provided by the root middlewarefinal authService = context.read<AuthService>();
// Now you can use the servicefinal result = await authService.performAnonymousSignIn();
This pattern decouples the route handlers from the concrete implementation of the services they use, making the code cleaner, more modular, and significantly easier to test.