Documentation Index Fetch the complete documentation index at: https://mintlify.com/khode-io/nest-dart/llms.txt
Use this file to discover all available pages before exploring further.
Best Practices and Patterns
Follow these patterns and best practices to build maintainable, scalable Nest Dart applications.
Module Organization
Feature-Based Module Structure
Organize modules by feature, not by layer:
// Good: Feature-based organization
lib /
├── modules /
│ ├── user /
│ │ ├── user_module.dart
│ │ ├── user_service.dart
│ │ ├── user_repository.dart
│ │ └── models /
│ │ └── user.dart
│ ├── auth /
│ │ ├── auth_module.dart
│ │ ├── auth_service.dart
│ │ └── guards /
│ │ └── jwt_guard.dart
│ └── todo /
│ ├── todo_module.dart
│ ├── todo_service.dart
│ └── todo_provider.dart
├── shared /
│ ├── core_module.dart
│ └── database_module.dart
└── app_module.dart
// Avoid: Layer-based organization
lib /
├── services /
│ ├── user_service.dart
│ ├── auth_service.dart
│ └── todo_service.dart
├── repositories /
│ └── user_repository.dart
└── modules /
└── app_module.dart
Module Responsibility
Each module should have a single, well-defined responsibility:
// Good: Focused modules
class UserModule extends Module {
@override
List < Module > get imports => [ DatabaseModule ()];
@override
Future < void > providers ( Locator locator) async {
locator. registerSingleton < UserRepository >(
UserRepository (locator. get < Database >()),
);
locator. registerSingleton < UserService >(
UserService (locator. get < UserRepository >()),
);
}
@override
List < Type > get exports => [ UserService ];
}
// Avoid: "God modules" that do everything
class EverythingModule extends Module {
// Too many responsibilities!
// Handles users, auth, todos, emails, etc.
}
When to Export Services
Export Public APIs Only
Only export services that other modules need to consume:
class UserModule extends Module {
@override
Future < void > providers ( Locator locator) async {
// Private: Only used within this module
locator. registerSingleton < UserRepository >(
UserRepository (locator. get < Database >()),
);
locator. registerSingleton < UserValidator >(
UserValidator (),
);
// Public: Used by other modules
locator. registerSingleton < UserService >(
UserService (
locator. get < UserRepository >(),
locator. get < UserValidator >(),
),
);
}
@override
List < Type > get exports => [
UserService , // Only export the public API
// UserRepository and UserValidator remain private
];
}
Exporting creates a public API contract. Only export what’s necessary to keep implementation details hidden.
Re-Exporting Dependencies
Use re-exports to simplify imports for consuming modules:
class CoreModule extends Module {
@override
Future < void > providers ( Locator locator) async {
final prefs = await SharedPreferences . getInstance ();
locator. registerSingleton < SharedPreferences >(prefs);
locator. registerSingleton < Client >( Client ());
locator. registerSingleton < ConfigService >(
ConfigService (prefs),
);
}
@override
List < Type > get exports => [
Client , // Re-export for HTTP calls
ConfigService , // Re-export for configuration
// SharedPreferences is private - use ConfigService instead
];
}
Service Scope Decisions
Singleton vs Factory
From packages/nest_core/lib/src/module.dart:241-305:
// Singleton: One instance shared across the application
locator. registerSingleton < DatabaseService >(
DatabaseService (),
);
// Lazy Singleton: Created on first access
locator. registerLazySingleton < ConfigService >(
() => ConfigService (),
);
// Factory: New instance on every access
locator. registerFactory < RequestLogger >(
() => RequestLogger (),
);
When to Use Each Scope
class AppModule extends Module {
@override
Future < void > providers ( Locator locator) async {
// Singleton: Shared state, expensive to create
locator. registerSingleton < DatabaseService >(
DatabaseService (),
);
// Lazy Singleton: Rarely used, expensive to create
locator. registerLazySingleton < EmailService >(
() => EmailService (config : locator. get < Config >()),
);
// Factory: Stateful, should not be shared
locator. registerFactory < UserSessionService >(
() => UserSessionService (),
);
// Factory: Cheap to create, need fresh instance
locator. registerFactory < DateTimeProvider >(
() => DateTimeProvider (),
);
}
}
Use singletons for stateless services and factories for stateful services or when you need a fresh instance.
Error Handling
Custom Exceptions
Create domain-specific exceptions for better error handling:
class UserNotFoundException implements Exception {
final int userId;
UserNotFoundException ( this .userId);
@override
String toString () => 'User with id $ userId not found' ;
}
class ValidationException implements Exception {
final Map < String , List < String >> errors;
ValidationException ( this .errors);
@override
String toString () => 'Validation failed: $ errors ' ;
}
class UserService {
Future < User > getUser ( int id) async {
final user = await repository. findById (id);
if (user == null ) {
throw UserNotFoundException (id);
}
return user;
}
Future < User > createUser ( CreateUserDto dto) async {
final errors = validator. validate (dto);
if (errors.isNotEmpty) {
throw ValidationException (errors);
}
return await repository. create (dto);
}
}
Error Handling in Services
class UserService {
final UserRepository _repository;
final Logger _logger;
UserService ( this ._repository, this ._logger);
Future < User > getUser ( int id) async {
try {
final user = await _repository. findById (id);
if (user == null ) {
throw UserNotFoundException (id);
}
return user;
} catch (e, stackTrace) {
_logger. error ( 'Failed to get user $ id ' , e, stackTrace);
rethrow ;
}
}
}
Module-Level Error Handling
Use onModuleInit to handle initialization errors:
class DatabaseModule extends Module {
@override
Future < void > onModuleInit ( Locator locator, ModuleContext context) async {
try {
final database = locator. get < DatabaseService >();
await database. connect ();
await database. runMigrations ();
} catch (e) {
print ( 'Failed to initialize database: $ e ' );
// Decide: rethrow to prevent app startup, or handle gracefully
rethrow ;
}
}
}
Lazy Initialization
Use lazy singletons for services that aren’t always needed:
class AppModule extends Module {
@override
Future < void > providers ( Locator locator) async {
// Eager: Created during module initialization
locator. registerSingleton < CriticalService >(
CriticalService (),
);
// Lazy: Created only when first accessed
locator. registerLazySingleton < EmailService >(
() => EmailService (),
);
locator. registerLazySingleton < ReportGenerator >(
() => ReportGenerator (),
);
}
}
Async Service Registration
Handle async initialization properly:
class CoreModule extends Module {
@override
Future < void > providers ( Locator locator) async {
// Services that require async initialization
final prefs = await SharedPreferences . getInstance ();
locator. registerSingleton < SharedPreferences >(prefs);
final database = await openDatabase ( 'app.db' );
locator. registerSingleton < Database >(database);
// Services that depend on async services
locator. registerSingleton < ConfigService >(
ConfigService (prefs),
);
}
}
Module Import Order
Import order affects initialization performance:
class AppModule extends Module {
@override
List < Module > get imports => [
CoreModule (), // Initialize first (no dependencies)
DatabaseModule (), // Then database (depends on Core)
UserModule (), // Then features (depend on Database)
AuthModule (), // Features can be parallel
TodoModule (),
];
}
From packages/nest_core/lib/src/container.dart:63-104, modules initialize in dependency-first order:
Future < void > _initializeModuleRecursive (
Module module,
Set < Type > visited,
) async {
// Initialize all imported modules first (dependency-first order)
for ( final importedModule in module.imports) {
await _initializeModuleRecursive (importedModule, visited);
}
// Then initialize this module
await module. onModuleInit (scopedLocator, _context);
}
Lifecycle Management
Proper Initialization
class DatabaseModule extends Module {
@override
Future < void > providers ( Locator locator) async {
locator. registerLazySingleton < DatabaseService >(
() => DatabaseService (),
);
}
@override
Future < void > onModuleInit ( Locator locator, ModuleContext context) async {
final db = locator. get < DatabaseService >();
await db. connect ();
await db. runMigrations ();
print ( 'Database initialized successfully' );
}
@override
Future < void > onModuleDestroy ( Locator locator, ModuleContext context) async {
final db = locator. get < DatabaseService >();
await db. close ();
print ( 'Database connection closed' );
}
}
Resource Cleanup
class CacheModule extends Module {
@override
Future < void > providers ( Locator locator) async {
locator. registerLazySingleton < CacheService >(
() => CacheService (),
dispose : (cache) async {
await cache. clear ();
print ( 'Cache cleared on disposal' );
},
);
}
@override
Future < void > onModuleDestroy ( Locator locator, ModuleContext context) async {
// Additional cleanup beyond dispose
final cache = locator. get < CacheService >();
await cache. saveToDisk ();
}
}
Dependency Injection Patterns
Constructor Injection
// Good: Clear dependencies in constructor
class UserService {
final UserRepository _repository;
final EmailService _emailService;
final Logger _logger;
UserService (
this ._repository,
this ._emailService,
this ._logger,
);
Future < User > createUser ( String email, String name) async {
final user = await _repository. create (email, name);
await _emailService. sendWelcomeEmail (user);
_logger. info ( 'User created: ${ user . id } ' );
return user;
}
}
// Module registration
class UserModule extends Module {
@override
Future < void > providers ( Locator locator) async {
locator. registerSingleton < UserService >(
UserService (
locator. get < UserRepository >(),
locator. get < EmailService >(),
locator. get < Logger >(),
),
);
}
}
Service Locator Pattern (Use Sparingly)
// Acceptable for complex initialization
class ComplexService {
late final Repository repository;
late final Cache cache;
late final Logger logger;
Future < void > initialize ( Locator locator) async {
repository = locator. get < Repository >();
cache = locator. get < Cache >();
logger = locator. get < Logger >();
await cache. load ();
}
}
class ComplexModule extends Module {
@override
Future < void > providers ( Locator locator) async {
final service = ComplexService ();
await service. initialize (locator);
locator. registerSingleton < ComplexService >(service);
}
}
Prefer constructor injection over service locator pattern for better testability and explicit dependencies.
Testing Best Practices
Test Module Pattern
class TestUserModule extends Module {
final UserRepository mockRepository;
TestUserModule ( this .mockRepository);
@override
Future < void > providers ( Locator locator) async {
locator. registerSingleton < UserRepository >(mockRepository);
locator. registerFactory < UserService >(
() => UserService (locator. get < UserRepository >()),
);
}
@override
List < Type > get exports => [ UserService ];
}
// In tests
void main () {
test ( 'user service test' , () async {
final mockRepo = MockUserRepository ();
final container = ApplicationContainer ();
await container. registerModule ( TestUserModule (mockRepo));
// Test with mocked repository
final userService = container. get < UserService >();
// ...
await container. reset ();
});
}
Summary
Module Organization : Feature-based, single responsibility
Exports : Only export public APIs, hide implementation details
Scoping : Use singletons for stateless, factories for stateful
Error Handling : Domain-specific exceptions, proper logging
Performance : Lazy initialization, async service registration
Lifecycle : Use hooks for initialization and cleanup
Testing : Create test modules, always reset container
Follow these patterns consistently across your codebase for maximum maintainability and team productivity.