Skip to content

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 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:

  1. Initializes the Database Connection: Establishes a connection to the MongoDB instance specified in the environment variables.
  2. Seeds the Database: Runs a seeding service to populate the database with initial data (countries, topics, etc.) if it doesn’t already exist.
  3. 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.
  4. Initializes Repositories: Wraps each data client in a DataRepository. Repositories provide a clean abstraction layer over the data clients.
  5. Initializes Services: Creates instances of all core business logic services (e.g., AuthService, DashboardSummaryService), injecting the repositories and other services they depend on.

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:

  1. Ensures AppDependencies.instance.init() has been called.
  2. Uses a series of .use(provider<T>((_) => deps.dependency)) calls to inject each repository and service from the AppDependencies singleton into the request context.

Because of this DI setup, any route handler or downstream middleware can easily access a required dependency by calling context.read<T>().

Example:

A route handler
// Read the AuthService provided by the root middleware
final authService = context.read<AuthService>();
// Now you can use the service
final 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.