Overview
The Module system in Nest Dart provides a powerful way to organize your application into cohesive, reusable units. Inspired by NestJS, modules encapsulate related functionality and manage dependencies through a controlled import/export mechanism.
Modules are the fundamental building blocks of a Nest Dart application. Each module defines its own providers, imports dependencies from other modules, and exports services for use by other modules.
Module Class
All modules extend the abstract Module class, which provides three key properties:
abstract class Module {
/// List of modules that this module depends on
List<Module> get imports => [];
/// Configure providers/services for dependency injection
Future<void> providers(Locator locator) async {}
/// List of provider types that this module exports
List<Type> get exports => [];
}
Creating a Module
Here’s a basic example of creating a module:
class DatabaseModule extends Module {
@override
Future<void> providers(Locator locator) async {
// Register services for this module
locator.registerLazySingleton<DatabaseService>(
() => DatabaseService(),
);
locator.registerFactory<UserRepository>(
() => UserRepository(locator<DatabaseService>()),
);
}
@override
List<Type> get exports => [DatabaseService, UserRepository];
}
Module Properties
Defines dependencies on other modules. Services from imported modules are accessible if they are exported.@override
List<Module> get imports => [DatabaseModule(), AuthModule()];
providers
Future<void> Function(Locator)
Configure all providers/services for dependency injection. This method registers services using the Locator interface.This method can be async to support services that require async initialization, such as SharedPreferences or database connections.
List of provider types that this module makes available to other modules. Only exported services can be accessed by modules that import this module.@override
List<Type> get exports => [UserService, AuthService];
Module Organization
Feature Modules
Organize related features into dedicated modules:
class UserModule extends Module {
@override
List<Module> get imports => [DatabaseModule()];
@override
Future<void> providers(Locator locator) async {
locator.registerLazySingleton<UserService>(
() => UserService(locator<UserRepository>()),
);
locator.registerFactory<UserController>(
() => UserController(locator<UserService>()),
);
}
@override
List<Type> get exports => [UserService];
}
Shared Modules
Create shared modules for common functionality:
class SharedModule extends Module {
@override
Future<void> providers(Locator locator) async {
locator.registerSingleton<LoggerService>(
LoggerService(),
);
locator.registerLazySingleton<ConfigService>(
() => ConfigService(),
);
}
@override
List<Type> get exports => [LoggerService, ConfigService];
}
Module Registration
Modules are registered with the ApplicationContainer:
final container = ApplicationContainer();
// Register a single module
await container.registerModule(AppModule());
// Register multiple modules
await container.registerModules([
DatabaseModule(),
AuthModule(),
UserModule(),
]);
When you register a module with the container, all its imported modules are automatically registered in the correct dependency order.
Module Imports and Dependencies
Modules can import other modules to access their exported services:
class AuthModule extends Module {
@override
List<Module> get imports => [
DatabaseModule(),
CryptoModule(),
];
@override
Future<void> providers(Locator locator) async {
// Can access DatabaseService because DatabaseModule exports it
locator.registerLazySingleton<AuthService>(
() => AuthService(
database: locator<DatabaseService>(),
crypto: locator<CryptoService>(),
),
);
}
@override
List<Type> get exports => [AuthService];
}
Circular dependencies between modules will prevent registration. Ensure your module dependency graph is acyclic.
Dependency Resolution Order
The framework automatically resolves module dependencies:
- Duplicate Prevention: Each module type is registered only once
- Dependency-First: Imported modules are registered before the importing module
- Provider Access: Services are only accessible if properly exported
From module.dart:128-149:
Future<void> _register(
GetIt getIt,
Set<Type> registeredModules,
ModuleContext context,
) async {
final moduleType = runtimeType;
// Prevent circular dependencies and duplicate registrations
if (registeredModules.contains(moduleType)) {
return;
}
registeredModules.add(moduleType);
// Register all imported modules first to establish dependency chain
for (final importedModule in imports) {
await importedModule._register(getIt, registeredModules, context);
}
// Register this module's services
final scopedGetIt = _ScopedGetIt(getIt, context, moduleType);
await providers(scopedGetIt);
}
Root Module Pattern
Create a root module that imports all feature modules:
class AppModule extends Module {
@override
List<Module> get imports => [
SharedModule(),
DatabaseModule(),
AuthModule(),
UserModule(),
ProductModule(),
];
@override
Future<void> providers(Locator locator) async {
// Register app-level services
locator.registerSingleton<AppService>(
AppService(),
);
}
@override
List<Type> get exports => [AppService];
}
Best Practices
Single Responsibility: Each module should have a clear, focused purpose (e.g., authentication, database access, user management).
Minimal Exports: Only export services that need to be used by other modules. Keep internal implementation details private.
Explicit Dependencies: Declare all module dependencies in the imports list for clear dependency visualization.
Services not listed in exports cannot be accessed by other modules, even if those modules import your module.
Next Steps