Module System
Understanding Hypermodern Modules
The Hypermodern module system enables you to create reusable, self-contained components that encapsulate related functionality, schemas, and services. Modules promote code reuse, maintainability, and team collaboration by allowing you to package and distribute functionality as discrete units.
What Makes a Module
A Hypermodern module consists of:
- Module Manifest: Metadata describing the module's identity, dependencies, and exports
- Schema Definitions: Models, endpoints, and enums specific to the module
- Service Providers: Business logic and endpoint implementations
- Migrations: Database schema changes and data transformations
- Configuration Schema: Configurable parameters and their validation rules
- Documentation: Usage examples and API documentation
Module Structure
my_module/
├── module.json # Module manifest
├── schemas/
│ ├── models.json # Module-specific models
│ ├── endpoints.json # Module endpoints
│ └── enums.json # Module enumerations
├── lib/
│ ├── services/ # Business logic
│ ├── providers/ # Service providers
│ ├── middleware/ # Module-specific middleware
│ └── migrations/ # Database migrations
├── config/
│ └── schema.json # Configuration schema
├── tests/
│ └── module_test.dart # Module tests
├── examples/
│ └── usage_example.dart # Usage examples
└── README.md # Module documentation
Creating Custom Modules
Let's create a comprehensive authentication module to demonstrate module development.
Module Manifest
{
"name": "hypermodern_auth",
"version": "1.2.0",
"description": "Complete authentication and authorization module",
"author": "Your Name <your.email@example.com>",
"license": "MIT",
"homepage": "https://github.com/yourorg/hypermodern_auth",
"hypermodern_version": ">=1.0.0 <2.0.0",
"dependencies": {
"hypermodern_crypto": "^1.0.0",
"hypermodern_email": "^0.5.0"
},
"exports": {
"models": [
"User",
"AuthToken",
"Permission",
"Role"
],
"endpoints": [
"login",
"logout",
"register",
"refresh_token",
"reset_password",
"verify_email"
],
"enums": [
"AuthProvider",
"TokenType",
"PermissionLevel"
],
"services": [
"AuthService",
"TokenService",
"PermissionService"
]
},
"configuration": {
"jwt_secret": {
"type": "string",
"required": true,
"description": "Secret key for JWT token signing"
},
"token_expiry": {
"type": "duration",
"default": "24h",
"description": "Default token expiration time"
},
"password_min_length": {
"type": "int32",
"default": 8,
"minimum": 6,
"description": "Minimum password length"
},
"enable_email_verification": {
"type": "bool",
"default": true,
"description": "Require email verification for new accounts"
},
"oauth_providers": {
"type": "map<string, object>",
"default": {},
"description": "OAuth provider configurations"
}
},
"permissions": [
"auth.login",
"auth.register",
"auth.admin",
"users.read",
"users.write",
"users.delete"
],
"database_tables": [
"users",
"auth_tokens",
"roles",
"permissions",
"user_roles"
]
}
Module Schema Definitions
schemas/models.json:
{
"models": {
"user": {
"id": "int64",
"email": "string",
"username": "string",
"password_hash": "string",
"email_verified": "bool",
"email_verification_token": "string?",
"password_reset_token": "string?",
"password_reset_expires": "datetime?",
"last_login": "datetime?",
"created_at": "datetime",
"updated_at": "datetime",
"roles": ["@role"]
},
"auth_token": {
"id": "int64",
"user_id": "int64",
"token_hash": "string",
"type": "@token_type",
"expires_at": "datetime",
"created_at": "datetime",
"last_used": "datetime?",
"metadata": "map<string, any>"
},
"role": {
"id": "int64",
"name": "string",
"description": "string?",
"permissions": ["@permission"],
"created_at": "datetime"
},
"permission": {
"id": "int64",
"name": "string",
"description": "string?",
"resource": "string",
"action": "string"
}
}
}
schemas/endpoints.json:
{
"endpoints": {
"login": {
"method": "POST",
"path": "/auth/login",
"description": "Authenticate user and return access token",
"request": {
"email": "string",
"password": "string",
"remember_me": "bool?"
},
"response": {
"user": "@user",
"access_token": "string",
"refresh_token": "string",
"expires_in": "int32"
},
"errors": ["invalid_credentials", "account_locked", "email_not_verified"],
"transports": ["http", "websocket", "tcp"]
},
"register": {
"method": "POST",
"path": "/auth/register",
"description": "Create new user account",
"request": {
"email": "string",
"username": "string",
"password": "string",
"confirm_password": "string"
},
"response": {
"user": "@user",
"verification_required": "bool"
},
"errors": ["email_exists", "username_exists", "weak_password"],
"transports": ["http", "websocket", "tcp"]
},
"refresh_token": {
"method": "POST",
"path": "/auth/refresh",
"description": "Refresh access token using refresh token",
"request": {
"refresh_token": "string"
},
"response": {
"access_token": "string",
"refresh_token": "string",
"expires_in": "int32"
},
"errors": ["invalid_token", "token_expired"],
"transports": ["http", "websocket", "tcp"]
}
}
}
Module Service Implementation
lib/services/auth_service.dart:
import 'package:hypermodern/hypermodern.dart';
import 'package:hypermodern_server/hypermodern_server.dart';
import '../generated/models.dart';
class AuthService {
final Database db;
final ModuleConfiguration config;
final TokenService tokenService;
final PasswordService passwordService;
AuthService({
required this.db,
required this.config,
required this.tokenService,
required this.passwordService,
});
Future<LoginResponse> login(LoginRequest request) async {
// Find user by email
final user = await _findUserByEmail(request.email);
if (user == null) {
throw InvalidCredentialsException('Invalid email or password');
}
// Verify password
final isValidPassword = await passwordService.verify(
request.password,
user.passwordHash,
);
if (!isValidPassword) {
await _recordFailedLogin(user.id);
throw InvalidCredentialsException('Invalid email or password');
}
// Check if email is verified (if required)
if (config.getBool('enable_email_verification') && !user.emailVerified) {
throw EmailNotVerifiedException('Please verify your email address');
}
// Check account status
if (await _isAccountLocked(user.id)) {
throw AccountLockedException('Account is temporarily locked');
}
// Generate tokens
final tokenExpiry = config.getDuration('token_expiry');
final accessToken = await tokenService.generateAccessToken(user, tokenExpiry);
final refreshToken = await tokenService.generateRefreshToken(user);
// Update last login
await _updateLastLogin(user.id);
// Record successful login
await _recordSuccessfulLogin(user.id);
return LoginResponse(
user: user,
accessToken: accessToken,
refreshToken: refreshToken,
expiresIn: tokenExpiry.inSeconds,
);
}
Future<RegisterResponse> register(RegisterRequest request) async {
// Validate passwords match
if (request.password != request.confirmPassword) {
throw ValidationException('Passwords do not match');
}
// Validate password strength
await passwordService.validateStrength(
request.password,
minLength: config.getInt('password_min_length'),
);
// Check if email already exists
if (await _emailExists(request.email)) {
throw EmailExistsException('Email address is already registered');
}
// Check if username already exists
if (await _usernameExists(request.username)) {
throw UsernameExistsException('Username is already taken');
}
// Hash password
final passwordHash = await passwordService.hash(request.password);
// Create user
final user = await db.transaction((tx) async {
final newUser = await _createUser(tx, User(
email: request.email,
username: request.username,
passwordHash: passwordHash,
emailVerified: !config.getBool('enable_email_verification'),
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
));
// Assign default role
await _assignDefaultRole(tx, newUser.id);
return newUser;
});
// Send verification email if required
bool verificationRequired = false;
if (config.getBool('enable_email_verification')) {
await _sendVerificationEmail(user);
verificationRequired = true;
}
return RegisterResponse(
user: user,
verificationRequired: verificationRequired,
);
}
Future<RefreshTokenResponse> refreshToken(RefreshTokenRequest request) async {
// Validate refresh token
final tokenData = await tokenService.validateRefreshToken(request.refreshToken);
if (tokenData == null) {
throw InvalidTokenException('Invalid refresh token');
}
// Get user
final user = await _findUserById(tokenData.userId);
if (user == null) {
throw InvalidTokenException('User not found');
}
// Generate new tokens
final tokenExpiry = config.getDuration('token_expiry');
final newAccessToken = await tokenService.generateAccessToken(user, tokenExpiry);
final newRefreshToken = await tokenService.generateRefreshToken(user);
// Revoke old refresh token
await tokenService.revokeToken(request.refreshToken);
return RefreshTokenResponse(
accessToken: newAccessToken,
refreshToken: newRefreshToken,
expiresIn: tokenExpiry.inSeconds,
);
}
// Private helper methods
Future<User?> _findUserByEmail(String email) async {
final result = await db.query(
'SELECT * FROM users WHERE email = ? AND deleted_at IS NULL',
[email],
);
return result.isEmpty ? null : User.fromJson(result.first);
}
Future<bool> _emailExists(String email) async {
final result = await db.query(
'SELECT 1 FROM users WHERE email = ? AND deleted_at IS NULL',
[email],
);
return result.isNotEmpty;
}
Future<void> _recordSuccessfulLogin(int userId) async {
await db.query(
'INSERT INTO login_attempts (user_id, success, ip_address, created_at) VALUES (?, ?, ?, ?)',
[userId, true, 'unknown', DateTime.now()],
);
}
}
Module Provider
lib/providers/auth_provider.dart:
import 'package:hypermodern_server/hypermodern_server.dart';
import '../services/auth_service.dart';
import '../services/token_service.dart';
import '../services/password_service.dart';
class AuthModuleProvider extends ModuleProvider {
@override
String get name => 'hypermodern_auth';
@override
Future<void> register(ModuleContainer container) async {
// Register services
container.singleton<PasswordService>((c) => PasswordService());
container.singleton<TokenService>((c) => TokenService(
database: c.get<Database>(),
jwtSecret: c.config.getString('jwt_secret'),
));
container.singleton<AuthService>((c) => AuthService(
db: c.get<Database>(),
config: c.config,
tokenService: c.get<TokenService>(),
passwordService: c.get<PasswordService>(),
));
}
@override
Future<void> boot(ModuleContainer container) async {
final server = container.get<HypermodernServer>();
final authService = container.get<AuthService>();
// Register endpoints
server.registerEndpoint<LoginRequest, LoginResponse>(
'login',
(request) => authService.login(request),
);
server.registerEndpoint<RegisterRequest, RegisterResponse>(
'register',
(request) => authService.register(request),
);
server.registerEndpoint<RefreshTokenRequest, RefreshTokenResponse>(
'refresh_token',
(request) => authService.refreshToken(request),
);
// Register middleware
server.middleware.add(AuthenticationMiddleware(
tokenService: container.get<TokenService>(),
));
}
@override
List<Migration> get migrations => [
CreateUsersTableMigration(),
CreateAuthTokensTableMigration(),
CreateRolesTableMigration(),
CreatePermissionsTableMigration(),
];
}
Module Manifests and Configuration
Module manifests define the module's identity, dependencies, and configuration schema.
Advanced Manifest Features
{
"name": "hypermodern_payments",
"version": "2.1.0",
"description": "Payment processing module with multiple provider support",
"tags": ["payments", "stripe", "paypal", "billing"],
"keywords": ["payment", "billing", "subscription", "invoice"],
"compatibility": {
"hypermodern": ">=1.2.0 <3.0.0",
"dart": ">=3.0.0 <4.0.0"
},
"dependencies": {
"hypermodern_auth": "^1.0.0",
"hypermodern_notifications": "^0.8.0"
},
"optional_dependencies": {
"hypermodern_analytics": "^1.0.0"
},
"peer_dependencies": {
"hypermodern_users": "^2.0.0"
},
"configuration": {
"default_currency": {
"type": "string",
"default": "USD",
"enum": ["USD", "EUR", "GBP", "JPY"],
"description": "Default currency for transactions"
},
"payment_providers": {
"type": "object",
"required": true,
"properties": {
"stripe": {
"type": "object",
"properties": {
"secret_key": {"type": "string", "required": true},
"webhook_secret": {"type": "string", "required": true},
"enabled": {"type": "bool", "default": true}
}
},
"paypal": {
"type": "object",
"properties": {
"client_id": {"type": "string", "required": true},
"client_secret": {"type": "string", "required": true},
"sandbox": {"type": "bool", "default": false}
}
}
}
},
"webhook_endpoints": {
"type": "array",
"items": {"type": "string"},
"default": [],
"description": "External webhook URLs to notify on payment events"
},
"retry_policy": {
"type": "object",
"properties": {
"max_attempts": {"type": "int32", "default": 3, "minimum": 1},
"backoff_multiplier": {"type": "float64", "default": 2.0},
"initial_delay": {"type": "duration", "default": "1s"}
}
}
},
"events": [
"payment.created",
"payment.succeeded",
"payment.failed",
"subscription.created",
"subscription.cancelled",
"invoice.generated"
],
"hooks": {
"pre_install": ["scripts/pre_install.dart"],
"post_install": ["scripts/setup_webhooks.dart"],
"pre_uninstall": ["scripts/cleanup.dart"]
},
"assets": [
"templates/invoice.html",
"templates/receipt.html",
"static/payment_icons/"
]
}
Configuration Validation
class ModuleConfiguration {
final Map<String, dynamic> _config;
final Map<String, ConfigurationField> _schema;
ModuleConfiguration(this._config, this._schema) {
_validate();
}
void _validate() {
for (final entry in _schema.entries) {
final key = entry.key;
final field = entry.value;
final value = _config[key];
// Check required fields
if (field.required && value == null) {
throw ConfigurationException('Required field missing: $key');
}
// Skip validation for null optional fields
if (value == null) continue;
// Type validation
if (!_isValidType(value, field.type)) {
throw ConfigurationException('Invalid type for $key: expected ${field.type}');
}
// Range validation
if (field.minimum != null && value < field.minimum!) {
throw ConfigurationException('Value for $key below minimum: ${field.minimum}');
}
if (field.maximum != null && value > field.maximum!) {
throw ConfigurationException('Value for $key above maximum: ${field.maximum}');
}
// Enum validation
if (field.enumValues != null && !field.enumValues!.contains(value)) {
throw ConfigurationException('Invalid value for $key: must be one of ${field.enumValues}');
}
// Pattern validation
if (field.pattern != null && value is String) {
if (!RegExp(field.pattern!).hasMatch(value)) {
throw ConfigurationException('Value for $key does not match pattern: ${field.pattern}');
}
}
}
}
T get<T>(String key) {
final value = _config[key];
if (value == null) {
final field = _schema[key];
if (field?.defaultValue != null) {
return field!.defaultValue as T;
}
throw ConfigurationException('Configuration key not found: $key');
}
return value as T;
}
String getString(String key) => get<String>(key);
int getInt(String key) => get<int>(key);
bool getBool(String key) => get<bool>(key);
double getDouble(String key) => get<double>(key);
List<T> getList<T>(String key) => (get<List>(key)).cast<T>();
Map<String, T> getMap<T>(String key) => (get<Map>(key)).cast<String, T>();
Duration getDuration(String key) {
final value = getString(key);
return _parseDuration(value);
}
Duration _parseDuration(String value) {
final regex = RegExp(r'^(\d+)(ms|s|m|h|d)$');
final match = regex.firstMatch(value);
if (match == null) {
throw ConfigurationException('Invalid duration format: $value');
}
final amount = int.parse(match.group(1)!);
final unit = match.group(2)!;
switch (unit) {
case 'ms': return Duration(milliseconds: amount);
case 's': return Duration(seconds: amount);
case 'm': return Duration(minutes: amount);
case 'h': return Duration(hours: amount);
case 'd': return Duration(days: amount);
default: throw ConfigurationException('Unknown duration unit: $unit');
}
}
}
Sharing and Distributing Modules
Module Registry
class ModuleRegistry {
final String registryUrl;
final HttpClient httpClient;
ModuleRegistry({
this.registryUrl = 'https://registry.hypermodern.dev',
required this.httpClient,
});
Future<List<ModuleInfo>> search({
String? query,
List<String>? tags,
String? author,
int limit = 20,
int offset = 0,
}) async {
final params = <String, String>{
'limit': limit.toString(),
'offset': offset.toString(),
};
if (query != null) params['q'] = query;
if (tags != null) params['tags'] = tags.join(',');
if (author != null) params['author'] = author;
final uri = Uri.parse('$registryUrl/modules/search').replace(
queryParameters: params,
);
final response = await httpClient.get(uri);
if (response.statusCode != 200) {
throw RegistryException('Search failed: ${response.statusCode}');
}
final data = jsonDecode(response.body) as Map<String, dynamic>;
final modules = (data['modules'] as List)
.map((m) => ModuleInfo.fromJson(m))
.toList();
return modules;
}
Future<ModuleInfo> getModuleInfo(String name, {String? version}) async {
final path = version != null ? '/modules/$name/$version' : '/modules/$name';
final uri = Uri.parse('$registryUrl$path');
final response = await httpClient.get(uri);
if (response.statusCode == 404) {
throw ModuleNotFoundException('Module not found: $name');
} else if (response.statusCode != 200) {
throw RegistryException('Failed to get module info: ${response.statusCode}');
}
final data = jsonDecode(response.body) as Map<String, dynamic>;
return ModuleInfo.fromJson(data);
}
Future<void> publishModule(String modulePath, String apiKey) async {
final manifest = await _loadManifest(modulePath);
final packageData = await _createPackage(modulePath);
final request = http.MultipartRequest('POST', Uri.parse('$registryUrl/modules'));
request.headers['Authorization'] = 'Bearer $apiKey';
request.fields['manifest'] = jsonEncode(manifest.toJson());
request.files.add(http.MultipartFile.fromBytes(
'package',
packageData,
filename: '${manifest.name}-${manifest.version}.tar.gz',
));
final response = await request.send();
if (response.statusCode != 201) {
throw RegistryException('Failed to publish module: ${response.statusCode}');
}
}
Future<String> downloadModule(String name, String version, String targetPath) async {
final uri = Uri.parse('$registryUrl/modules/$name/$version/download');
final response = await httpClient.get(uri);
if (response.statusCode != 200) {
throw RegistryException('Failed to download module: ${response.statusCode}');
}
final packagePath = path.join(targetPath, '$name-$version.tar.gz');
await File(packagePath).writeAsBytes(response.bodyBytes);
// Extract package
await _extractPackage(packagePath, targetPath);
return path.join(targetPath, name);
}
}
Module Installation
class ModuleInstaller {
final ModuleRegistry registry;
final String modulesPath;
ModuleInstaller({
required this.registry,
required this.modulesPath,
});
Future<void> install(String moduleSpec) async {
final (name, version) = _parseModuleSpec(moduleSpec);
print('Installing $name${version != null ? '@$version' : ''}...');
// Get module info
final moduleInfo = await registry.getModuleInfo(name, version: version);
// Check compatibility
await _checkCompatibility(moduleInfo);
// Resolve dependencies
final dependencies = await _resolveDependencies(moduleInfo);
// Install dependencies first
for (final dep in dependencies) {
if (!await _isModuleInstalled(dep.name, dep.version)) {
await install('${dep.name}@${dep.version}');
}
}
// Download and install module
final modulePath = await registry.downloadModule(
moduleInfo.name,
moduleInfo.version,
modulesPath,
);
// Run installation hooks
await _runInstallationHooks(modulePath, moduleInfo);
// Update module registry
await _updateInstalledModules(moduleInfo);
print('✅ Successfully installed ${moduleInfo.name}@${moduleInfo.version}');
}
Future<void> uninstall(String moduleName) async {
print('Uninstalling $moduleName...');
// Check if module is installed
if (!await _isModuleInstalled(moduleName)) {
throw ModuleException('Module not installed: $moduleName');
}
// Check for dependent modules
final dependents = await _findDependentModules(moduleName);
if (dependents.isNotEmpty) {
throw ModuleException(
'Cannot uninstall $moduleName: required by ${dependents.join(', ')}',
);
}
final modulePath = path.join(modulesPath, moduleName);
final manifest = await _loadManifest(modulePath);
// Run uninstallation hooks
await _runUninstallationHooks(modulePath, manifest);
// Remove module files
await Directory(modulePath).delete(recursive: true);
// Update module registry
await _removeFromInstalledModules(moduleName);
print('✅ Successfully uninstalled $moduleName');
}
Future<List<ModuleDependency>> _resolveDependencies(ModuleInfo moduleInfo) async {
final resolved = <ModuleDependency>[];
final visited = <String>{};
await _resolveDependenciesRecursive(
moduleInfo.dependencies,
resolved,
visited,
);
return resolved;
}
Future<void> _resolveDependenciesRecursive(
Map<String, String> dependencies,
List<ModuleDependency> resolved,
Set<String> visited,
) async {
for (final entry in dependencies.entries) {
final name = entry.key;
final versionConstraint = entry.value;
if (visited.contains(name)) {
continue; // Avoid circular dependencies
}
visited.add(name);
// Find compatible version
final version = await _findCompatibleVersion(name, versionConstraint);
final moduleInfo = await registry.getModuleInfo(name, version: version);
// Recursively resolve dependencies
await _resolveDependenciesRecursive(
moduleInfo.dependencies,
resolved,
visited,
);
resolved.add(ModuleDependency(name: name, version: version));
}
}
}
Module Lifecycle and Hooks
Lifecycle Events
abstract class ModuleLifecycle {
Future<void> onInstall(ModuleContext context);
Future<void> onUninstall(ModuleContext context);
Future<void> onEnable(ModuleContext context);
Future<void> onDisable(ModuleContext context);
Future<void> onUpdate(ModuleContext context, String fromVersion);
Future<void> onConfigurationChange(ModuleContext context, Map<String, dynamic> oldConfig);
}
class PaymentModuleLifecycle implements ModuleLifecycle {
@override
Future<void> onInstall(ModuleContext context) async {
// Set up database tables
await context.database.runMigrations();
// Create default configuration
await _createDefaultConfiguration(context);
// Set up webhook endpoints
await _setupWebhooks(context);
print('Payment module installed successfully');
}
@override
Future<void> onUninstall(ModuleContext context) async {
// Clean up webhooks
await _cleanupWebhooks(context);
// Archive payment data (don't delete for compliance)
await _archivePaymentData(context);
print('Payment module uninstalled');
}
@override
Future<void> onEnable(ModuleContext context) async {
// Start background services
await _startPaymentProcessor(context);
await _startWebhookListener(context);
print('Payment module enabled');
}
@override
Future<void> onDisable(ModuleContext context) async {
// Stop background services
await _stopPaymentProcessor(context);
await _stopWebhookListener(context);
print('Payment module disabled');
}
@override
Future<void> onUpdate(ModuleContext context, String fromVersion) async {
print('Updating payment module from $fromVersion to ${context.module.version}');
// Run version-specific migrations
if (_shouldMigrateFrom(fromVersion, '1.0.0')) {
await _migrateFrom1_0_0(context);
}
if (_shouldMigrateFrom(fromVersion, '2.0.0')) {
await _migrateFrom2_0_0(context);
}
// Update webhook configurations
await _updateWebhooks(context);
print('Payment module updated successfully');
}
@override
Future<void> onConfigurationChange(ModuleContext context, Map<String, dynamic> oldConfig) async {
final newConfig = context.configuration;
// Check if payment providers changed
if (oldConfig['payment_providers'] != newConfig.get('payment_providers')) {
await _reconfigurePaymentProviders(context);
}
// Check if webhook endpoints changed
if (oldConfig['webhook_endpoints'] != newConfig.get('webhook_endpoints')) {
await _updateWebhookEndpoints(context);
}
print('Payment module configuration updated');
}
}
Custom Installation Scripts
// scripts/setup_webhooks.dart
import 'dart:io';
import 'package:hypermodern_server/hypermodern_server.dart';
Future<void> main(List<String> args) async {
final context = ModuleContext.fromArgs(args);
final config = context.configuration;
print('Setting up payment webhooks...');
// Set up Stripe webhooks
if (config.has('payment_providers.stripe')) {
await setupStripeWebhooks(context);
}
// Set up PayPal webhooks
if (config.has('payment_providers.paypal')) {
await setupPayPalWebhooks(context);
}
print('✅ Webhooks configured successfully');
}
Future<void> setupStripeWebhooks(ModuleContext context) async {
final stripeConfig = context.configuration.getMap('payment_providers.stripe');
final secretKey = stripeConfig['secret_key'] as String;
final stripe = StripeClient(secretKey);
// Create webhook endpoint
final webhook = await stripe.webhookEndpoints.create({
'url': '${context.serverUrl}/webhooks/stripe',
'enabled_events': [
'payment_intent.succeeded',
'payment_intent.payment_failed',
'invoice.payment_succeeded',
'customer.subscription.created',
'customer.subscription.deleted',
],
});
// Store webhook secret in configuration
await context.updateConfiguration({
'payment_providers.stripe.webhook_secret': webhook.secret,
});
print('Stripe webhook created: ${webhook.id}');
}
Service Provider System
The Hypermodern platform includes a comprehensive service provider system that enables proper lifecycle management and dependency injection for modules. Service providers bridge the gap between module metadata and actual implementation, providing structured initialization and cleanup.
Understanding Service Providers
Service providers follow a structured 3-phase lifecycle that ensures proper dependency management and resource cleanup:
- Registration Phase - Register services in the IoC container
- Initialization Phase - Initialize services with dependencies
- Termination Phase - Clean up resources and terminate gracefully
Creating Service Providers
Service providers extend the ModuleServiceProvider class and implement the three lifecycle methods:
import 'package:hypermodern/modules.dart';
class DatabaseServiceProvider extends ModuleServiceProvider with ServiceContainerMixin {
DatabaseServiceProvider({
required super.manifest,
required super.config,
});
@override
Future<void> register() async {
// Register services in the IoC container
final connectionString = getConfig<String>('connection_string');
final maxConnections = getConfigOrDefault<int>('max_connections', 10);
registerSingleton<DatabaseService>(() => DatabaseService(
connectionString: connectionString,
maxConnections: maxConnections,
));
registerSingleton<QueryBuilder>(() => QueryBuilder());
}
@override
Future<void> initialize() async {
// Initialize services after all providers are registered
final dbService = resolve<DatabaseService>();
await dbService.connect();
// Set up connection pool
await dbService.initializeConnectionPool();
// Run any necessary migrations
await dbService.runMigrations();
}
@override
Future<void> terminate() async {
// Clean up resources
final dbService = tryResolve<DatabaseService>();
if (dbService != null) {
await dbService.closeConnections();
await dbService.cleanup();
}
}
}
Service Container and Dependency Injection
The service provider system includes a lightweight IoC container for managing dependencies:
// Register services
ServiceContainer.instance.register<MyService>(() => MyService());
ServiceContainer.instance.singleton<DatabaseService>(() => DatabaseService());
// Resolve services
final service = ServiceContainer.instance.resolve<MyService>();
final db = ServiceLocator.get<DatabaseService>();
// Using the ServiceContainerMixin
class MyServiceProvider extends ModuleServiceProvider with ServiceContainerMixin {
@override
Future<void> register() async {
// Register services
registerSingleton<MyService>(() => MyService());
registerInstance<Config>(myConfig);
}
@override
Future<void> initialize() async {
// Resolve services
final service = resolve<MyService>();
final config = tryResolve<Config>();
}
}
Module Manager Integration
The ModuleManager orchestrates the entire module lifecycle:
import 'package:hypermodern/modules.dart';
Future<void> main() async {
// Create module manager
final manager = ModuleManager.getInstance();
await manager.initialize();
// Load modules with service providers
await manager.loadModule('database', databaseProvider,
configOverrides: {
'connection_string': 'postgresql://localhost:5432/myapp',
'max_connections': 20,
});
await manager.loadModule('cache', cacheProvider,
configOverrides: {
'redis_url': 'redis://localhost:6379',
'default_ttl_seconds': 3600,
});
// Start all modules (register and initialize providers)
await manager.start();
// Your application logic here...
// Stop all modules (terminate providers)
await manager.stop();
}
Configuration Access in Service Providers
Service providers have type-safe access to module configuration:
class PaymentServiceProvider extends ModuleServiceProvider with ServiceContainerMixin {
@override
Future<void> register() async {
// Required configuration
final apiKey = getConfig<String>('stripe_api_key');
// Optional configuration with defaults
final timeout = getConfigOrDefault<int>('timeout_seconds', 30);
// Check if configuration exists
if (hasConfig('webhook_secret')) {
final webhookSecret = getConfig<String>('webhook_secret');
// Configure webhook validation
}
// Type-safe configuration access
final retryAttempts = getConfigInt('retry_attempts', defaultValue: 3);
final enableLogging = getConfigBool('enable_logging', defaultValue: true);
final endpoints = getConfigList<String>('webhook_endpoints');
registerSingleton<PaymentService>(() => PaymentService(
apiKey: apiKey,
timeout: Duration(seconds: timeout),
retryAttempts: retryAttempts,
enableLogging: enableLogging,
));
}
}
Advanced Service Provider Patterns
Multi-Service Registration
class AuthServiceProvider extends ModuleServiceProvider with ServiceContainerMixin {
@override
Future<void> register() async {
// Register multiple related services
registerSingleton<PasswordHasher>(() => BCryptPasswordHasher());
registerSingleton<TokenService>(() => JWTTokenService(
secret: getConfig<String>('jwt_secret'),
expiry: getConfigDuration('token_expiry'),
));
registerSingleton<AuthService>(() => AuthService(
passwordHasher: resolve<PasswordHasher>(),
tokenService: resolve<TokenService>(),
database: resolve<DatabaseService>(),
));
// Register middleware
registerSingleton<AuthMiddleware>(() => AuthMiddleware(
tokenService: resolve<TokenService>(),
));
}
@override
Future<void> initialize() async {
// Initialize authentication system
final authService = resolve<AuthService>();
await authService.initialize();
// Set up default admin user if configured
if (getConfigBool('create_admin_user', defaultValue: false)) {
await authService.createDefaultAdmin();
}
}
}
Conditional Service Registration
class NotificationServiceProvider extends ModuleServiceProvider with ServiceContainerMixin {
@override
Future<void> register() async {
// Register email service if configured
if (hasConfig('smtp_host')) {
registerSingleton<EmailService>(() => SMTPEmailService(
host: getConfig<String>('smtp_host'),
port: getConfigInt('smtp_port', defaultValue: 587),
username: getConfig<String>('smtp_username'),
password: getConfig<String>('smtp_password'),
));
}
// Register SMS service if configured
if (hasConfig('twilio_account_sid')) {
registerSingleton<SMSService>(() => TwilioSMSService(
accountSid: getConfig<String>('twilio_account_sid'),
authToken: getConfig<String>('twilio_auth_token'),
));
}
// Register push notification service if configured
if (hasConfig('firebase_server_key')) {
registerSingleton<PushNotificationService>(() => FirebasePushService(
serverKey: getConfig<String>('firebase_server_key'),
));
}
// Register the main notification service
registerSingleton<NotificationService>(() => NotificationService(
emailService: tryResolve<EmailService>(),
smsService: tryResolve<SMSService>(),
pushService: tryResolve<PushNotificationService>(),
));
}
}
Service Provider Testing
import 'package:test/test.dart';
import 'package:hypermodern/modules.dart';
void main() {
group('DatabaseServiceProvider', () {
late DatabaseServiceProvider provider;
late ServiceContainer container;
setUp(() {
container = ServiceContainer.instance;
container.clear();
final manifest = ModuleHelper.createManifest(
name: 'database',
version: '1.0.0',
description: 'Database module',
author: 'Test Author',
configuration: {
'connection_string': ModuleHelper.configField(
type: 'string',
required: true,
description: 'Database connection string',
),
},
);
final config = ModuleConfiguration(
createConfigSchema(manifest.configuration),
{'connection_string': 'postgresql://localhost:5432/testdb'},
);
provider = DatabaseServiceProvider(
manifest: manifest,
config: config,
);
});
test('should register services during registration phase', () async {
await provider.register();
provider.markRegistered();
expect(container.isRegistered<DatabaseService>(), isTrue);
expect(provider.isRegistered, isTrue);
});
test('should initialize services during initialization phase', () async {
await provider.register();
provider.markRegistered();
await provider.initialize();
provider.markInitialized();
final dbService = container.resolve<DatabaseService>();
expect(dbService.isConnected, isTrue);
expect(provider.isInitialized, isTrue);
});
test('should cleanup during termination', () async {
await provider.register();
provider.markRegistered();
await provider.initialize();
provider.markInitialized();
await provider.terminate();
provider.markTerminated();
final dbService = container.resolve<DatabaseService>();
expect(dbService.isConnected, isFalse);
expect(provider.isTerminated, isTrue);
});
});
}
Best Practices for Service Providers
- Keep registration lightweight: Only register services, don't initialize them during the registration phase
- Use initialize for initialization: Initialize services that depend on other modules during the initialization phase
- Implement proper termination: Clean up resources to prevent memory leaks
- Use configuration validation: Validate required configuration early
- Leverage dependency injection: Use the container instead of global state
- Handle errors gracefully: Wrap initialization code in try-catch blocks
Service Provider Error Handling
class RobustServiceProvider extends ModuleServiceProvider with ServiceContainerMixin {
@override
Future<void> register() async {
try {
// Service registration logic
registerSingleton<MyService>(() => MyService());
} catch (e) {
throw ServiceProviderException(
'Failed to register services in ${manifest.name}',
moduleName: manifest.name,
cause: e,
);
}
}
@override
Future<void> initialize() async {
try {
final service = resolve<MyService>();
await service.initialize();
} catch (e) {
// Log the error
print('Failed to initialize ${manifest.name}: $e');
// Attempt cleanup
await _attemptCleanup();
// Re-throw with context
throw ServiceProviderException(
'Initialization failed for ${manifest.name}',
moduleName: manifest.name,
cause: e,
);
}
}
@override
Future<void> terminate() async {
try {
await _attemptCleanup();
} catch (e) {
// Log termination errors but don't throw
print('Warning: Cleanup failed for ${manifest.name}: $e');
}
}
Future<void> _attemptCleanup() async {
final service = tryResolve<MyService>();
if (service != null) {
await service.cleanup();
}
}
}
Integration with Existing Module System
Service providers work seamlessly with the existing module system:
// Traditional module provider
class TraditionalAuthProvider extends ModuleProvider {
@override
Future<void> register(ModuleContainer container) async {
// Traditional registration
}
}
// New service provider approach
class AuthServiceProvider extends ModuleServiceProvider with ServiceContainerMixin {
@override
Future<void> register() async {
// Modern service provider registration
}
}
The service provider system provides a more structured, testable, and maintainable approach to module development while maintaining full compatibility with existing modules.
What's Next
You now understand how to create, configure, and distribute Hypermodern modules. In the next chapter, we'll explore ORM and data management features, including database integration, migrations, and relationship modeling that work seamlessly with the module system.
No Comments