Application Loaders
The Mifty framework uses a loader pattern to initialize different aspects of the application in the correct order. Loaders handle dependency injection, Express configuration, database connections, and route loading.
Overview
Loaders are responsible for:
- Setting up dependency injection containers
- Configuring Express middleware and settings
- Establishing database connections
- Loading and registering application routes
- Initializing Swagger documentation
Loader Modules
Dependency Injection Loader (di.ts)
Handles the loading and registration of dependency injection containers for enabled modules.
export const loadDI = async (modulesToLoad: string[]): Promise<void>
Parameters:
modulesToLoad: string[]- Array of module names to load DI for
Process:
- Iterates through each enabled module
- Looks for
di.tsordi.jsfiles in module directories - Imports and executes
registerDependencies()function if available - Logs success or failure for each module
Example:
// In a module's di.ts file
import { container } from 'tsyringe';
import { UserService } from './user.service';
import { UserRepository } from './user.repository';
export function registerDependencies() {
container.register('UserRepository', { useClass: UserRepository });
container.register('UserService', { useClass: UserService });
}
// Usage in app initialization
await loadDI(['user', 'auth', 'product']);
Module Structure:
src/modules/user/
├── di.ts # Dependency registration
├── user.service.ts
├── user.repository.ts
├── user.controller.ts
└── user.routes.ts
Express Loader (express.ts)
Configures Express application with middleware and security settings.
export const loadExpressApp = async (app: Application, config: ServerConfig): Promise<void>
Parameters:
app: Application- Express application instanceconfig: ServerConfig- Server configuration object
Configuration Options:
interface ServerConfig {
port: number;
enableCors?: boolean; // Default: true
enableHelmet?: boolean; // Default: true
enableCompression?: boolean; // Default: true
enableLogging?: boolean; // Default: true
}
Middleware Setup:
- CORS - Cross-origin resource sharing (if enabled)
- Helmet - Security headers (if enabled)
- Compression - Response compression (if enabled)
- Body Parsing - JSON and URL-encoded data parsing
- Static Files - Serves uploads directory
- Logging - HTTP request logging with Morgan (if enabled)
Example:
const config: ServerConfig = {
port: 3000,
enableCors: true,
enableHelmet: true,
enableCompression: true,
enableLogging: process.env.NODE_ENV !== 'test'
};
await loadExpressApp(app, config);
Static File Serving:
// Automatically serves files from uploads directory
// Files accessible at: http://localhost:3000/uploads/filename.jpg
const uploadsDir = path.resolve(process.env.LOCAL_UPLOAD_DIR || './uploads');
app.use('/uploads', express.static(uploadsDir));
Prisma Loader (prisma.ts)
Establishes database connection using Prisma ORM.
export const connectPrisma = async (): Promise<void>
Process:
- Initializes Prisma client
- Establishes database connection
- Handles connection errors gracefully
- Logs connection status
Example:
// Database connection
await connectPrisma();
console.log('Database connected successfully');
Environment Configuration:
# Database connection string
DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
# Or for SQLite (alternative)
DATABASE_URL="file:./dev.db"
Route Loader (routes.ts)
Dynamically loads and registers routes for enabled modules.
export const loadRoutes = async (app: Application, enabledModules: ModuleConfig[]): Promise<void>
Parameters:
app: Application- Express application instanceenabledModules: ModuleConfig[]- Array of enabled module configurations
Module Configuration:
interface ModuleConfig {
name: string;
enabled: boolean;
routesPrefix?: string; // Optional route prefix
includeRoutes?: string[]; // Specific routes to include
excludeRoutes?: string[]; // Specific routes to exclude
}
Route Discovery Process:
- Scans module directories for route files
- Applies include/exclude filters if specified
- Loads route files and registers with Express
- Applies route prefixes if configured
Example Module Routes:
// src/modules/user/user.routes.ts
import { Router } from 'express';
import { UserController } from './user.controller';
const router = Router();
const userController = new UserController();
router.get('/', userController.getAll);
router.get('/:id', userController.getById);
router.post('/', userController.create);
router.put('/:id', userController.update);
router.delete('/:id', userController.delete);
export default router;
Route Registration:
const moduleConfig: ModuleConfig[] = [
{
name: 'user',
enabled: true,
routesPrefix: '/api/v1',
includeRoutes: ['user', 'profile'], // Only load these route files
},
{
name: 'auth',
enabled: true,
routesPrefix: '/auth',
excludeRoutes: ['admin'] // Load all except admin routes
}
];
await loadRoutes(app, moduleConfig);
Resulting Routes:
GET /api/v1/users
GET /api/v1/users/:id
POST /api/v1/users
PUT /api/v1/users/:id
DELETE /api/v1/users/:id
GET /api/v1/profiles
POST /api/v1/profiles
POST /auth/login
POST /auth/register
POST /auth/refresh
Swagger Loader (swagger.ts)
Sets up API documentation using Swagger/OpenAPI.
export const loadSwagger = async (app: Application): Promise<void>
Features:
- Automatic API documentation generation
- Interactive API explorer
- Schema validation
- Request/response examples
Configuration:
const swaggerOptions = {
definition: {
openapi: '3.0.0',
info: {
title: 'Mifty API',
version: '1.0.0',
description: 'API documentation for Mifty application'
},
servers: [
{
url: 'http://localhost:3000',
description: 'Development server'
}
]
},
apis: ['./src/modules/**/*.routes.ts'] // Path to route files
};
Usage:
await loadSwagger(app);
// Swagger UI available at: http://localhost:3000/api-docs
Complete Initialization Example
import App from './app';
import { AppConfig } from './types/app-config';
const config: AppConfig = {
modules: [
{
name: 'user',
enabled: true,
routesPrefix: '/api/v1',
includeRoutes: ['user', 'profile']
},
{
name: 'auth',
enabled: true,
routesPrefix: '/auth'
},
{
name: 'product',
enabled: process.env.ENABLE_PRODUCTS === 'true',
routesPrefix: '/api/v1'
}
],
server: {
port: parseInt(process.env.PORT || '3000'),
enableCors: true,
enableHelmet: true,
enableCompression: true,
enableLogging: process.env.NODE_ENV !== 'test'
}
};
async function startApplication() {
const app = new App(config);
// Loaders are called automatically in App.build()
// 1. connectPrisma()
// 2. loadDI(enabledModules)
// 3. loadExpressApp(app, serverConfig)
// 4. loadRoutes(app, enabledModules)
// 5. loadSwagger(app) - if enabled
await app.build();
app.listen();
}
startApplication().catch(console.error);
Loader Execution Order
The loaders are executed in a specific order to ensure proper initialization:
-
Database Connection (
connectPrisma)- Establishes database connectivity
- Required before any database operations
-
Dependency Injection (
loadDI)- Registers service dependencies
- Required before route loading
-
Express Configuration (
loadExpressApp)- Sets up middleware and security
- Required before route registration
-
Route Loading (
loadRoutes)- Registers API endpoints
- Depends on DI and Express setup
-
Documentation (
loadSwagger)- Sets up API documentation
- Should be last to capture all routes
Error Handling
Loader Error Handling
try {
await connectPrisma();
console.log('✅ Database connected');
} catch (error) {
console.error('❌ Database connection failed:', error);
process.exit(1);
}
try {
await loadDI(enabledModules.map(m => m.name));
console.log('✅ Dependencies loaded');
} catch (error) {
console.error('❌ DI loading failed:', error);
// Continue with degraded functionality
}
Module Loading Errors
// In loadDI function
for (const moduleName of modulesToLoad) {
try {
const diModule = await import(modulePath);
if (typeof diModule.registerDependencies === 'function') {
diModule.registerDependencies();
console.info(`✅ Successfully registered dependencies for ${moduleName}`);
}
} catch (error) {
console.error(`❌ Failed to load DI for ${moduleName}:`, error);
// Continue with other modules
}
}
Environment-Specific Configuration
Development Environment
const developmentConfig: AppConfig = {
modules: [
{ name: 'user', enabled: true },
{ name: 'auth', enabled: true },
{ name: 'admin', enabled: true }, // Admin routes in development
],
server: {
port: 3000,
enableCors: true,
enableLogging: true // Detailed logging
}
};
Production Environment
const productionConfig: AppConfig = {
modules: [
{ name: 'user', enabled: true },
{ name: 'auth', enabled: true },
// Admin module disabled in production
],
server: {
port: parseInt(process.env.PORT || '8080'),
enableCors: process.env.ENABLE_CORS === 'true',
enableLogging: false // Minimal logging
}
};
Testing Environment
const testConfig: AppConfig = {
modules: [
{ name: 'user', enabled: true },
{ name: 'auth', enabled: false }, // Skip auth in tests
],
server: {
port: 0, // Random port
enableLogging: false // No logging in tests
}
};
Best Practices
1. Module Organization
// Good: Organized module structure
src/modules/user/
├── di.ts # Dependency registration
├── user.controller.ts
├── user.service.ts
├── user.repository.ts
├── user.routes.ts
├── user.types.ts
└── user.schema.ts
2. Conditional Module Loading
const config: AppConfig = {
modules: [
{ name: 'user', enabled: true },
{ name: 'admin', enabled: process.env.NODE_ENV === 'development' },
{ name: 'analytics', enabled: process.env.ENABLE_ANALYTICS === 'true' }
]
};
3. Error Recovery
// Graceful degradation for optional modules
const optionalModules = ['analytics', 'notifications'];
const criticalModules = ['user', 'auth'];
for (const module of criticalModules) {
try {
await loadModule(module);
} catch (error) {
console.error(`Critical module ${module} failed to load`);
process.exit(1);
}
}
for (const module of optionalModules) {
try {
await loadModule(module);
} catch (error) {
console.warn(`Optional module ${module} failed to load, continuing...`);
}
}
Related
- App Class - Main application orchestration
- Dependency Injection - DI patterns