Overview
Lifecycle hooks allow you to execute code at specific points in a moduleβs lifetime. Nest Dart provides two lifecycle hooks: onModuleInit for initialization and onModuleDestroy for cleanup.
Lifecycle hooks are executed in dependency order, ensuring that imported modules are initialized before modules that depend on them.
Lifecycle Hooks
onModuleInit
Called after module registration and dependency resolution, when all services are available.
Future<void> onModuleInit(Locator locator, ModuleContext context) async {
// Default implementation does nothing
// Override in your modules for custom initialization
}
The scoped dependency injection container for accessing registered services.
The module context containing information about imports, exports, and service providers.
Use cases:
- Database connections and migrations
- Service warm-up and configuration
- Data seeding
- Health checks
- Eager initialization of services
onModuleDestroy
Called when the module is being destroyed or the container is reset.
Future<void> onModuleDestroy(Locator locator, ModuleContext context) async {
// Default implementation does nothing
// Override in your modules for custom cleanup
}
Use cases:
- Closing database connections
- Saving state to disk
- Releasing resources
- Cleanup operations
- Graceful shutdown
Basic Examples
Database Initialization
class DatabaseModule extends Module {
@override
Future<void> providers(Locator locator) async {
locator.registerLazySingleton<DatabaseService>(
() => DatabaseService(
host: 'localhost',
port: 5432,
),
);
}
@override
Future<void> onModuleInit(Locator locator, ModuleContext context) async {
// Connect to database when module initializes
final db = locator<DatabaseService>();
await db.connect();
// Run migrations
await db.runMigrations();
print('β Database connected and migrations applied');
}
@override
Future<void> onModuleDestroy(Locator locator, ModuleContext context) async {
// Close connection when module is destroyed
final db = locator<DatabaseService>();
await db.disconnect();
print('β Database connection closed');
}
@override
List<Type> get exports => [DatabaseService];
}
Cache Warm-up
class CacheModule extends Module {
@override
Future<void> providers(Locator locator) async {
locator.registerLazySingleton<CacheService>(
() => CacheService(),
);
}
@override
Future<void> onModuleInit(Locator locator, ModuleContext context) async {
final cache = locator<CacheService>();
// Pre-populate cache with frequently accessed data
await cache.warmUp([
'app_config',
'feature_flags',
'user_preferences',
]);
print('β Cache warmed up with ${cache.size} entries');
}
@override
Future<void> onModuleDestroy(Locator locator, ModuleContext context) async {
final cache = locator<CacheService>();
// Persist cache to disk before shutdown
await cache.persist();
await cache.clear();
print('β Cache persisted and cleared');
}
}
Execution Order
Hooks are executed in dependency order:
Initialization Order
From container.dart:63-104:
Future<void> _initializeModuleRecursive(
Module module,
Set<Type> visited,
) async {
final moduleType = module.runtimeType;
// Skip if already visited or initialized
if (visited.contains(moduleType) || _initializedModules.contains(module)) {
return;
}
visited.add(moduleType);
// Initialize all imported modules first (dependency-first order)
for (final importedModule in module.imports) {
await _initializeModuleRecursive(importedModule, visited);
}
// Initialize this module
if (!_initializedModules.contains(module)) {
final startTime = DateTime.now();
final scopedLocator = _ScopedGetIt(_getIt, _context, moduleType);
try {
await module.onModuleInit(scopedLocator, _context);
_initializedModules.add(module);
_logger.log(
Level.info,
'[NestCore] Module initialized successfully: $moduleType',
);
} catch (e) {
_logger.log(
Level.error,
'[NestCore] Error initializing module $moduleType: $e',
);
rethrow;
}
}
}
Example:
class ConfigModule extends Module {
@override
Future<void> onModuleInit(Locator locator, ModuleContext context) async {
print('1. ConfigModule initialized');
}
}
class DatabaseModule extends Module {
@override
List<Module> get imports => [ConfigModule()];
@override
Future<void> onModuleInit(Locator locator, ModuleContext context) async {
print('2. DatabaseModule initialized');
}
}
class AppModule extends Module {
@override
List<Module> get imports => [DatabaseModule()];
@override
Future<void> onModuleInit(Locator locator, ModuleContext context) async {
print('3. AppModule initialized');
}
}
// Output:
// 1. ConfigModule initialized
// 2. DatabaseModule initialized
// 3. AppModule initialized
Destruction Order
Modules are destroyed in reverse order of initialization:
From container.dart:260-277:
Future<void> reset() async {
// Destroy modules in reverse order of initialization
final modulesToDestroy = List<Module>.from(_initializedModules.reversed);
for (final module in modulesToDestroy) {
try {
print('Destroying module: ${module.runtimeType}');
final scopedLocator = _ScopedGetIt(
_getIt,
_context,
module.runtimeType,
);
await module.onModuleDestroy(scopedLocator, _context);
print('Module destroyed successfully: ${module.runtimeType}');
} catch (e) {
print('Error destroying module ${module.runtimeType}: $e');
}
}
// ...
}
Reverse destruction order ensures that modules are cleaned up after their dependents, preventing use-after-free scenarios.
Advanced Examples
Multi-Module Coordination
class MetricsModule extends Module {
@override
Future<void> providers(Locator locator) async {
locator.registerLazySingleton<MetricsService>(
() => MetricsService(),
);
}
@override
Future<void> onModuleInit(Locator locator, ModuleContext context) async {
final metrics = locator<MetricsService>();
// Record which modules are initialized
final availableServices = context.getAvailableServices(
runtimeType,
);
await metrics.record('modules_initialized', {
'count': availableServices.length,
'services': availableServices.map((s) => s.toString()).toList(),
});
}
}
Health Checks
class HealthCheckModule extends Module {
@override
List<Module> get imports => [
DatabaseModule(),
CacheModule(),
ApiModule(),
];
@override
Future<void> providers(Locator locator) async {
locator.registerLazySingleton<HealthCheckService>(
() => HealthCheckService(),
);
}
@override
Future<void> onModuleInit(Locator locator, ModuleContext context) async {
final health = locator<HealthCheckService>();
// Verify all critical services are healthy
final checks = [
() => locator<DatabaseService>().ping(),
() => locator<CacheService>().ping(),
() => locator<ApiClient>().ping(),
];
for (final check in checks) {
await health.verify(check);
}
if (!health.isHealthy) {
throw Exception('Health check failed: ${health.failures}');
}
print('β All health checks passed');
}
}
Data Seeding
class SeedModule extends Module {
@override
List<Module> get imports => [DatabaseModule()];
@override
Future<void> onModuleInit(Locator locator, ModuleContext context) async {
final db = locator<DatabaseService>();
// Only seed in development
if (Environment.isDevelopment) {
final hasData = await db.hasData();
if (!hasData) {
print('Seeding database with test data...');
await db.insert('users', [
{'name': 'Alice', 'email': 'alice@example.com'},
{'name': 'Bob', 'email': 'bob@example.com'},
]);
await db.insert('products', [
{'name': 'Widget', 'price': 9.99},
{'name': 'Gadget', 'price': 19.99},
]);
print('β Database seeded with test data');
}
}
}
}
Graceful Shutdown
class ServerModule extends Module {
@override
Future<void> providers(Locator locator) async {
locator.registerSingleton<HttpServer>(
await HttpServer.bind('localhost', 8080),
);
}
@override
Future<void> onModuleInit(Locator locator, ModuleContext context) async {
final server = locator<HttpServer>();
print('β Server listening on ${server.address}:${server.port}');
}
@override
Future<void> onModuleDestroy(Locator locator, ModuleContext context) async {
final server = locator<HttpServer>();
print('Initiating graceful shutdown...');
// Wait for pending requests to complete (with timeout)
await server.close().timeout(
Duration(seconds: 30),
onTimeout: () {
print('Shutdown timeout - forcing close');
},
);
print('β Server shutdown complete');
}
}
Error Handling
Initialization Errors
From container.dart:88-102:
try {
await module.onModuleInit(scopedLocator, _context);
_initializedModules.add(module);
_logger.log(
Level.info,
'[NestCore] Module initialized successfully: $moduleType',
);
} catch (e) {
_logger.log(
Level.error,
'[NestCore] Error initializing module $moduleType: $e',
);
rethrow;
}
If onModuleInit throws an error, the entire initialization process fails. Ensure proper error handling in your hooks.
Destruction Errors
for (final module in modulesToDestroy) {
try {
await module.onModuleDestroy(scopedLocator, _context);
} catch (e) {
print('Error destroying module ${module.runtimeType}: $e');
// Continue destroying other modules
}
}
Errors in onModuleDestroy are logged but donβt stop the destruction of other modules.
Using Module Context
Access module information during lifecycle hooks:
class AnalyticsModule extends Module {
@override
Future<void> onModuleInit(Locator locator, ModuleContext context) async {
// Get all services available to this module
final available = context.getAvailableServices(runtimeType);
print('Available services: ${available.length}');
// Check what this module exports
final ourExports = context.moduleExports[runtimeType] ?? {};
print('Exporting: $ourExports');
// Check our imports
final ourImports = context.moduleImports[runtimeType] ?? {};
print('Importing from: $ourImports');
// Check global services
print('Global services: ${context.globalServices}');
}
}
Best Practices
Keep Hooks Fast: Lifecycle hooks block module initialization. Perform only essential setup and defer heavy operations when possible.
Idempotent Initialization: Make onModuleInit idempotent in case itβs called multiple times (though the framework prevents this).
Cleanup Resources: Always implement onModuleDestroy for modules that acquire resources (connections, files, timers).
Donβt access services from modules that havenβt been initialized yet. The framework ensures dependency order, but be mindful of your import graph.
Lifecycle with Testing
test('module lifecycle', () async {
final container = ApplicationContainer();
// Track initialization
var initialized = false;
var destroyed = false;
final module = TestModule(
onInit: () => initialized = true,
onDestroy: () => destroyed = true,
);
// Initialize
await container.registerModule(module);
expect(initialized, isTrue);
// Cleanup
await container.reset();
expect(destroyed, isTrue);
});
Complete Example
class AppModule extends Module {
@override
List<Module> get imports => [
ConfigModule(),
DatabaseModule(),
CacheModule(),
];
@override
Future<void> providers(Locator locator) async {
locator.registerLazySingleton<AppService>(
() => AppService(
config: locator<ConfigService>(),
db: locator<DatabaseService>(),
cache: locator<CacheService>(),
),
);
}
@override
Future<void> onModuleInit(Locator locator, ModuleContext context) async {
print('π Application starting...');
final config = locator<ConfigService>();
print('Environment: ${config.environment}');
final db = locator<DatabaseService>();
await db.connect();
print('β Database connected');
final cache = locator<CacheService>();
await cache.warmUp();
print('β Cache initialized');
final app = locator<AppService>();
await app.initialize();
print('β Application ready');
}
@override
Future<void> onModuleDestroy(Locator locator, ModuleContext context) async {
print('π Application shutting down...');
try {
final app = locator<AppService>();
await app.shutdown();
print('β Application shutdown complete');
} catch (e) {
print('Error during shutdown: $e');
}
}
@override
List<Type> get exports => [AppService];
}
Next Steps