Skip to main content

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;
    }
  }
}

Performance Considerations

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.

Build docs developers (and LLMs) love