My Favorite Express Architecture
💽

My Favorite Express Architecture

🗣
If you just want to create a new project with this architecture, look at the following link
🛠️My Favorite Express Architecture Setup Guide

Express.js Enterprise Architecture Tutorial

This comprehensive tutorial demonstrates how to implement proven enterprise architectural patterns in Express.js applications. Follow this step-by-step guide to build scalable, maintainable applications with enterprise-level patterns.

🏗️ Table of Contents

  1. Project Foundation
  1. Domain-Driven Directory Structure
  1. Dependency Injection Setup
  1. Database Architecture with MikroORM
  1. Middleware Architecture
  1. Route Organization
  1. Service Layer Patterns
  1. Error Handling Strategy
  1. Authentication & Authorization
  1. Utility Functions
  1. Real-time Features
  1. Testing Setup

1. Project Foundation

ℹ️
A clear foundation is crucial for any enterprise application. This section helps you set up the necessary dependencies, TypeScript configuration, and project structure to ensure scalability and maintainability from the start.

Initial Setup

# Initialize project
npm init -y
npm install express typescript ts-node-dev @types/express @types/node

# Core dependencies
npm install @mikro-orm/core @mikro-orm/mongodb @mikro-orm/cli
npm install express-validator cors helmet morgan winston
npm install jsonwebtoken bcryptjs dotenv luxon
npm install accesscontrol zod

# Development dependencies
npm install @types/cors @types/morgan @types/luxon
npm install jest @types/jest ts-jest supertest @types/supertest
npm install eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser

TypeScript Configuration

Create tsconfig.json:
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "resolveJsonModule": true,
    "declaration": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "**/*.test.ts"]
}

Package.json Scripts

{
  "scripts": {
    "build": "tsc",
    "start": "node dist/src/index.js",
    "dev": "NODE_ENV=development ts-node-dev --watch .env.development src/index.ts",
    "test": "cross-env NODE_ENV=test jest",
    "lint": "eslint src/**/*.ts"
  }
}

2. Domain-Driven Directory Structure

ℹ️
Organizing your code by business domains rather than technical function creates better separation of concerns. This structure makes your codebase more intuitive to navigate and allows teams to work independently on different domains.
Create the following directory structure:
src/
├── shared/
│   ├── entities/           # Base entity classes
│   ├── middleware/         # Reusable middleware
│   ├── services/          # Shared services
│   ├── utils/             # Utility functions
│   ├── errors/            # Custom error classes
│   └── types/             # TypeScript type definitions
├── user/                  # User domain
│   ├── user.entity.ts
│   ├── user.service.ts
│   ├── user.controller.ts
│   ├── user.routes.ts
│   └── __tests__/
├── auth/                  # Authentication domain
├── product/               # Example business domain
└── config.ts

Core Configuration

Create src/config.ts:
import { config as dotenvConfig } from 'dotenv';

dotenvConfig({ path: `.env.${process.env.NODE_ENV || 'development'}` });

export const config = {
  nodeEnv: process.env.NODE_ENV || 'development',
  port: parseInt(process.env.PORT || '3000'),
  mongoConnectionString: process.env.MONGODB_URL || 'mongodb://localhost:27017/myapp',
  jwtSecret: process.env.JWT_SECRET || 'your-secret-key',
  clientOrigins: {
    development: ['http://localhost:3000'],
    production: ['https://yourdomain.com'],
    test: ['http://localhost:3000']
  }
};

3. Dependency Injection Setup

ℹ️
Dependency injection improves testability and decouples components. This pattern allows for easier mocking during tests and provides a clean way to manage service instances throughout your application.
Create src/DI.ts:
import { MikroORM, EntityManager } from '@mikro-orm/core';
import { Db } from 'mongodb';
import * as winston from 'winston';
import * as http from 'http';

export const DI = {} as {
  server: http.Server;
  orm: MikroORM;
  em: EntityManager;
  nativeClient: Db;
  logger: winston.Logger;
};
```

Create `src/services.ts` for service instantiation:

```typescript
import { UserService } from './user/user.service';
import { AuthService } from './auth/auth.service';

// Singleton service instances
export let userService: UserService;
export let authService: AuthService;

export const initializeServices = () => {
  userService = new UserService();
  authService = new AuthService();
};
Create src/services.ts for service instantiation:

import { UserService } from './user/user.service';
import { AuthService } from './auth/auth.service';

// Singleton service instances
export let userService: UserService;
export let authService: AuthService;

export const initializeServices = () => {
  userService = new UserService();
  authService = new AuthService();
};

4. Database Architecture with MikroORM

ℹ️
A robust database layer is essential for enterprise applications. MikroORM provides type-safe entity management with support for MongoDB, making it ideal for scalable applications while maintaining data integrity.

Base Entity Setup

Create src/shared/entities/base.entity.ts:
import { Entity, PrimaryKey, SerializedPrimaryKey, Property } from '@mikro-orm/core';
import { ObjectId } from '@mikro-orm/mongodb';

@Entity({ abstract: true })
export abstract class BaseEntity {
  @PrimaryKey()
  _id!: ObjectId;

  @SerializedPrimaryKey()
  id!: string;

  @Property()
  createdAt = new Date();

  @Property({ onUpdate: () => new Date() })
  updatedAt = new Date();

  @Property()
  owners: string[] = ['global'];

  @Property({ hidden: true })
  testValue = false;

  // Multi-tenant ownership methods
  addOwner(groupID: string): void {
    if (!this.owners.includes(groupID)) {
      this.owners.push(groupID);
    }
  }

  removeOwner(groupID: string): void {
    this.owners = this.owners.filter(owner => owner !== groupID);
  }

  checkUserOwnership(groupID: string): boolean {
    return this.owners.includes(groupID) || this.owners.includes('global');
  }
}

MikroORM Configuration

Create src/mikro-orm.config.ts:
import { defineConfig } from '@mikro-orm/core';
import { MongoHighlighter } from '@mikro-orm/mongo-highlighter';
import { config } from './config';

// Import all entities
import { User } from './user/user.entity';
import { Product } from './product/product.entity';

const entities = [User, Product];

export default defineConfig({
  entities,
  dbName: 'myapp',
  clientUrl: config.mongoConnectionString,
  highlighter: new MongoHighlighter(),
  debug: config.nodeEnv === 'development',
  allowGlobalContext: config.nodeEnv === 'test',
});

Example Entity

Create src/user/user.entity.ts:
import { Entity, Property, Collection, OneToMany } from '@mikro-orm/core';
import { BaseEntity } from '../shared/entities/base.entity';

interface UserRole {
  role: string;
  group: string;
}

@Entity()
export class User extends BaseEntity {
  @Property()
  username!: string;

  @Property()
  firstName?: string;

  @Property()
  lastName?: string;

  @Property()
  email!: string;

  @Property()
  passwordHash?: string;

  @Property()
  roles: UserRole[] = [{ role: 'user', group: 'global' }];

  constructor(username: string, email: string) {
    super();
    this.username = username;
    this.email = email;
  }

  get fullName(): string {
    return `${this.firstName || ''} ${this.lastName || ''}`.trim();
  }
}

5. Middleware Architecture

ℹ️
Middleware provides cross-cutting functionality across your application. This organized approach to middleware creation and registration ensures consistent request processing, error handling, and authentication throughout your application.

Global Middleware Setup

Create src/shared/middleware/index.ts:

export { tryCatch } from './tryCatch.middleware';
export { validateRequest } from './validateRequest.middleware';
export { errorHandler } from './error.middleware';
export { authMiddleware } from './auth.middleware';
export { routeNotFoundHandler } from './routeNotFound.middleware';

Try-Catch Wrapper

Create src/shared/middleware/tryCatch.middleware.ts:

import { Request, Response, NextFunction, RequestHandler } from 'express';

export const tryCatch = (fn: RequestHandler): RequestHandler => {
  return (req: Request, res: Response, next: NextFunction): void => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
};

Request Validation Middleware

Create src/shared/middleware/validateRequest.middleware.ts:

import { Request, Response, NextFunction } from 'express';
import { ValidationChain, validationResult } from 'express-validator';

export const validateRequest = (validatorArray: ValidationChain[]) => [
  validatorArray,
  (req: Request, res: Response, next: NextFunction) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ 
        error: 'Validation failed',
        details: errors.array() 
      });
    }
    next();
  },
];

Authentication Middleware

Create src/shared/middleware/auth.middleware.ts:
import { Request, Response, NextFunction } from 'express';
import * as jwt from 'jsonwebtoken';
import { userService } from '../../services';
import { config } from '../../config';

interface AuthenticatedRequest extends Request {
  userId?: string;
  user?: any;
}

export async function authMiddleware(
  req: AuthenticatedRequest,
  res: Response,
  next: NextFunction
): Promise<void> {
  const token = req.headers.authorization?.replace('Bearer ', '');

  if (!token) {
    return next();
  }

  try {
    const decoded = jwt.verify(token, config.jwtSecret) as { userId: string };
    req.userId = decoded.userId;

    const user = await userService.getUserById(decoded.userId);
    if (user) {
      req.user = user;
    }
  } catch (error) {
    console.error('Auth middleware error:', error);
  }

  next();
}

Error Handling Middleware

Create src/shared/middleware/error.middleware.ts:
import { Request, Response, NextFunction, ErrorRequestHandler } from 'express';
import { DI } from '../../DI';

export const errorHandler: ErrorRequestHandler = (err, req, res, next) => {
  const isDevelopment = process.env.NODE_ENV === 'development';
  
  DI.logger.error('Error occurred:', {
    message: err.message,
    stack: err.stack,
    url: req.url,
    method: req.method
  });

  // Custom error handling
  if (err.name === 'ValidationError') {
    return res.status(400).json({
      error: 'Validation Error',
      message: err.message,
      ...(isDevelopment && { stack: err.stack })
    });
  }

  if (err.name === 'UnauthorizedError') {
    return res.status(401).json({
      error: 'Unauthorized',
      message: 'Authentication required'
    });
  }

  // Generic error response
  res.status(err.statusCode || 500).json({
    error: 'Internal Server Error',
    message: isDevelopment ? err.message : 'Something went wrong',
    ...(isDevelopment && { stack: err.stack })
  });
};

6. Route Organization

ℹ️
Clean route organization improves API discoverability and maintenance. This section demonstrates how to structure routes by domain and separate public from authenticated endpoints for better security and clarity.

Route Structure Pattern

Create src/user/user.routes.ts:
import express from 'express';
import { body, param } from 'express-validator';
import { UserController } from './user.controller';
import { tryCatch, validateRequest } from '../shared/middleware';

const userController = new UserController();
const router = express.Router();

// GET /user/:id
router.get(
  '/:id',
  ...validateRequest([
    param('id').isMongoId().withMessage('Valid user ID required')
  ]),
  tryCatch(userController.getUserById)
);

// POST /user
router.post(
  '/',
  ...validateRequest([
    body('firstName').optional().isLength({ min: 1, max: 50 }),
    body('lastName').optional().isLength({ min: 1, max: 50 }),
    body('email').isEmail().withMessage('Valid email required')
  ]),
  tryCatch(userController.createUser)
);

// PUT /user/:id
router.put(
  '/:id',
  ...validateRequest([
    param('id').isMongoId().withMessage('Valid user ID required'),
    body('firstName').optional().isLength({ min: 1, max: 50 }),
    body('lastName').optional().isLength({ min: 1, max: 50 })
  ]),
  tryCatch(userController.updateUser)
);

export { router as UserRouter };

Public Routes Pattern

Create src/user/user.public-routes.ts:
import express from 'express';
import { param } from 'express-validator';
import { UserController } from './user.controller';
import { tryCatch, validateRequest } from '../shared/middleware';

const userController = new UserController();
const router = express.Router();

// GET /public/user/:id - Public user profile
router.get(
  '/:id',
  ...validateRequest([
    param('id').isMongoId().withMessage('Valid user ID required')
  ]),
  tryCatch(userController.getPublicUserProfile)
);

export { router as PublicUserRouter };

Controller Pattern

Create src/user/user.controller.ts:
import { Request, Response } from 'express';
import { UserService } from './user.service';

interface AuthenticatedRequest extends Request {
  user?: any;
}

export class UserController {
  private userService = new UserService();

  getUserById = async (req: Request, res: Response): Promise<void> => {
    const { id } = req.params;
    const user = await this.userService.getUserById(id);
    
    if (!user) {
      res.status(404).json({ error: 'User not found' });
      return;
    }

    res.json({ user });
  };

  getPublicUserProfile = async (req: Request, res: Response): Promise<void> => {
    const { id } = req.params;
    const profile = await this.userService.getPublicProfile(id);
    
    if (!profile) {
      res.status(404).json({ error: 'User not found' });
      return;
    }

    res.json({ profile });
  };

  createUser = async (req: AuthenticatedRequest, res: Response): Promise<void> => {
    const userData = req.body;
    const user = await this.userService.createUser(userData);
    res.status(201).json({ user });
  };

  updateUser = async (req: AuthenticatedRequest, res: Response): Promise<void> => {
    const { id } = req.params;
    const updates = req.body;
    
    // Check ownership
    if (req.user?.id !== id) {
      res.status(403).json({ error: 'Access denied' });
      return;
    }

    const user = await this.userService.updateUser(id, updates);
    res.json({ user });
  };
}

Central Route Registration

Create src/routes.ts:

// Export all route modules
export { UserRouter } from './user/user.routes';
export { PublicUserRouter } from './user/user.public-routes';
export { ProductRouter } from './product/product.routes';

7. Service Layer Patterns

ℹ️
The service layer encapsulates business logic away from controllers. This separation allows for reusable business logic that can be called from multiple entry points while maintaining a single source of truth.

Service Base Pattern

Create src/user/user.service.ts:
import { DI } from '../DI';
import { User } from './user.entity';

export class UserService {
  async getUserById(id: string): Promise<User | null> {
    return await DI.em.findOne(User, { id });
  }

  async getUserByUsername(username: string): Promise<User | null> {
    return await DI.em.findOne(User, { username });
  }

  async createUser(userData: Partial<User>): Promise<User> {
    const user = new User(userData.username!, userData.email!);
    Object.assign(user, userData);
    
    await DI.em.persistAndFlush(user);
    return user;
  }

  async updateUser(id: string, updates: Partial<User>): Promise<User | null> {
    const user = await this.getUserById(id);
    if (!user) return null;

    Object.assign(user, updates);
    await DI.em.flush();
    return user;
  }

  async getPublicProfile(id: string): Promise<Partial<User> | null> {
    const user = await this.getUserById(id);
    if (!user) return null;

    return {
      id: user.id,
      firstName: user.firstName,
      lastName: user.lastName,
      // Don't expose sensitive data
    };
  }

  async searchUsers(query: string, limit = 20): Promise<User[]> {
    return await DI.em.find(User, {
      $or: [
        { firstName: new RegExp(query, 'i') },
        { lastName: new RegExp(query, 'i') },
        { email: new RegExp(query, 'i') }
      ]
    }, { limit });
  }
}

8. Error Handling Strategy

ℹ️
Comprehensive error handling creates more robust applications. Using custom error classes helps standardize error responses and makes debugging easier while providing clear feedback to API consumers.

Custom Error Classes

Create src/shared/errors/index.ts:
export class AppError extends Error {
  constructor(
    public message: string,
    public statusCode: number = 500,
    public isOperational: boolean = true
  ) {
    super(message);
    this.name = this.constructor.name;
    Error.captureStackTrace(this, this.constructor);
  }
}

export class ValidationError extends AppError {
  constructor(message: string, public details?: any[]) {
    super(message, 400);
  }
}

export class NotFoundError extends AppError {
  constructor(message: string = 'Resource not found') {
    super(message, 404);
  }
}

export class UnauthorizedError extends AppError {
  constructor(message: string = 'Authentication required') {
    super(message, 401);
  }
}

export class ForbiddenError extends AppError {
  constructor(message: string = 'Access denied') {
    super(message, 403);
  }
}

9. Authentication & Authorization

ℹ️
Security is paramount in enterprise applications. This implementation of role-based access control ensures proper permission management and protects sensitive operations based on user roles.
 

Role-Based Access Control

Create src/shared/middleware/accessControl.middleware.ts:
import { Request, Response, NextFunction } from 'express';
import AccessControl from 'accesscontrol';

// Define roles and permissions
const ac = new AccessControl();

ac.grant('user')
  .readOwn('profile')
  .updateOwn('profile');

ac.grant('admin')
  .extend('user')
  .readAny('profile')
  .updateAny('profile')
  .deleteAny('profile');

interface AuthorizedRequest extends Request {
  user?: { id: string; roles: Array<{ role: string; group: string }> };
}

export const checkAccess = (resource: string, action: string) => {
  return (req: AuthorizedRequest, res: Response, next: NextFunction) => {
    const user = req.user;
    
    if (!user) {
      return res.status(401).json({ error: 'Authentication required' });
    }

    const userRoles = user.roles || [{ role: 'user', group: 'global' }];
    
    const hasPermission = userRoles.some(userRole => {
      const permission = ac.can(userRole.role)[action](resource);
      return permission.granted;
    });

    if (!hasPermission) {
      return res.status(403).json({ error: 'Access denied' });
    }

    next();
  };
};

10. Utility Functions

ℹ️
Reusable utilities prevent code duplication and standardize common operations. These pagination and search utilities ensure consistent API behavior and help maintain clean controller code.

Pagination Utilities

Create src/shared/utils/pagination.ts:
import { Request } from 'express';

export interface PaginationParams {
  limit: number;
  offset: number;
  orderBy: string;
  direction: 'asc' | 'desc';
}

export const getPaginationParams = (req: Request): PaginationParams => {
  const limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
  const offset = Math.max(parseInt(req.query.offset as string) || 0, 0);
  const orderBy = req.query.orderBy as string || 'createdAt';
  const direction = (req.query.direction as string)?.toLowerCase() === 'asc' ? 'asc' : 'desc';

  return { limit, offset, orderBy, direction };
};

Search Utilities

Create src/shared/utils/search.ts:
export const sanitizeSearchQuery = (query: string): string => {
  return query
    .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // Escape regex characters
    .trim()
    .substring(0, 100); // Limit length
};

export const buildSearchConditions = (searchTerm: string, fields: string[]) => {
  const sanitized = sanitizeSearchQuery(searchTerm);
  return {
    $or: fields.map(field => ({
      [field]: new RegExp(sanitized, 'i')
    }))
  };
};

11. Real-time Features

ℹ️
Modern applications often require real-time capabilities. The Server-Sent Events implementation provides an efficient way to push updates to clients without the complexity of WebSockets.

Server-Sent Events

Create src/shared/services/sse.service.ts:
import { Response } from 'express';

export class SSEService {
  private connections = new Map<string, Response>();

  addConnection(userId: string, res: Response): void {
    res.writeHead(200, {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive',
      'Access-Control-Allow-Origin': '*',
    });

    this.connections.set(userId, res);

    res.on('close', () => {
      this.connections.delete(userId);
    });
  }

  sendToUser(userId: string, event: string, data: any): void {
    const connection = this.connections.get(userId);
    if (connection) {
      connection.write(`event: ${event}\n`);
      connection.write(`data: ${JSON.stringify(data)}\n\n`);
    }
  }

  broadcast(event: string, data: any): void {
    for (const connection of this.connections.values()) {
      connection.write(`event: ${event}\n`);
      connection.write(`data: ${JSON.stringify(data)}\n\n`);
    }
  }
}

12. Testing Setup

ℹ️
A comprehensive testing strategy is essential for maintaining quality. This setup enables unit, integration, and e2e tests with proper database isolation for reliable test results.

Jest Configuration

Create jest.config.ts:
export default {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['<rootDir>/src'],
  testMatch: ['**/__tests__/**/*.test.ts'],
  collectCoverageFrom: [
    'src/**/*.ts',
    '!src/**/*.d.ts',
    '!src/index.ts'
  ],
  setupFilesAfterEnv: ['<rootDir>/src/__tests__/setup.ts']
};

Test Setup

Create src/__tests__/setup.ts:
import { DI } from '../DI';
import { MikroORM } from '@mikro-orm/core';
import config from '../mikro-orm.config';

beforeAll(async () => {
  DI.orm = await MikroORM.init({
    ...config,
    dbName: 'test-db',
    allowGlobalContext: true
  });
  DI.em = DI.orm.em;
});

afterAll(async () => {
  await DI.orm.close();
});

beforeEach(async () => {
  await DI.orm.getSchemaGenerator().clearDatabase();
});

Example Test

Create src/user/__tests__/user.service.test.ts:
import { UserService } from '../user.service';
import { User } from '../user.entity';
import { DI } from '../../DI';

describe('UserService', () => {
  let userService: UserService;

  beforeEach(() => {
    userService = new UserService();
  });

  it('should create a user', async () => {
    const userData = {
      username: 'johndoe',
      firstName: 'John',
      lastName: 'Doe',
      email: 'john@example.com'
    };

    const user = await userService.createUser(userData);

    expect(user).toBeInstanceOf(User);
    expect(user.firstName).toBe('John');
    expect(user.email).toBe('john@example.com');
  });

  it('should find user by ID', async () => {
    const user = new User('janedoe', 'jane@example.com');
    user.firstName = 'Jane';
    await DI.em.persistAndFlush(user);

    const foundUser = await userService.getUserById(user.id);

    expect(foundUser).toBeTruthy();
    expect(foundUser?.firstName).toBe('Jane');
  });
});

13. Main Application Setup

ℹ️
The application bootstrap process ties everything together. This section shows how to properly initialize your Express app with all the middleware, routes, and services while ensuring proper shutdown procedures.

App Configuration

Create src/app.ts:
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import morgan from 'morgan';
import { RequestContext } from '@mikro-orm/core';

import { config } from './config';
import { DI } from './DI';
import { initializeServices } from './services';
import { 
  errorHandler, 
  authMiddleware, 
  routeNotFoundHandler 
} from './shared/middleware';

// Import routes
import { 
  UserRouter, 
  PublicUserRouter, 
  ProductRouter 
} from './routes';

export const createApp = () => {
  const app = express();

  // Infrastructure middleware
  app.use(morgan('combined'));
  app.use(express.json({ limit: '10mb' }));
  app.use(express.urlencoded({ extended: true }));
  app.use(cors({ origin: config.clientOrigins[config.nodeEnv] }));
  app.use(helmet());

  // Database context middleware
  app.use((req, res, next) => RequestContext.create(DI.orm.em, next));

  // Apply auth middleware to all routes
  app.use(authMiddleware);

  // Public routes
  app.use('/public/user', PublicUserRouter);

  // Protected routes
  app.use('/user', UserRouter);
  app.use('/product', ProductRouter);

  // Health check
  app.get('/health', (req, res) => {
    res.json({ status: 'ok', timestamp: new Date().toISOString() });
  });

  // Error handling
  app.use(routeNotFoundHandler);
  app.use(errorHandler);

  return app;
};

Application Entry Point

Create src/index.ts:
import { MikroORM } from '@mikro-orm/core';
import { MongoClient } from 'mongodb';
import mikroOrmConfig from './mikro-orm.config';
import { createApp } from './app';
import { DI } from './DI';
import { config } from './config';
import { initializeServices } from './services';

const bootstrap = async () => {
  try {
    // Initialize database
    const client = await MongoClient.connect(config.mongoConnectionString);
    DI.orm = await MikroORM.init(mikroOrmConfig);
    DI.em = DI.orm.em;
    DI.nativeClient = client.db('myapp');

    // Initialize services
    initializeServices();

    // Create Express app
    const app = createApp();

    // Start server
    DI.server = app.listen(config.port, () => {
      console.log(`🚀 Server running on port ${config.port}`);
      console.log(`📖 Environment: ${config.nodeEnv}`);
    });

    // Graceful shutdown
    const shutdown = async () => {
      console.log('🛑 Shutting down gracefully...');
      DI.server.close(async () => {
        await DI.orm.close();
        process.exit(0);
      });
    };

    process.on('SIGTERM', shutdown);
    process.on('SIGINT', shutdown);

  } catch (error) {
    console.error('❌ Error starting application:', error);
    process.exit(1);
  }
};

bootstrap();

14. Environment Configuration

ℹ️
Environment-specific configuration allows your application to behave differently in development, testing, and production. This approach keeps sensitive information secure while maintaining flexibility across environments.
 
Create environment files:
.env.development:
NODE_ENV=development
PORT=3000
MONGODB_URL=mongodb://localhost:27017/myapp-dev
JWT_SECRET=your-jwt-secret-key
.env.test:
NODE_ENV=test
MONGODB_URL=mongodb://localhost:27017/myapp-test
JWT_SECRET=test-jwt-secret
.env.production:
NODE_ENV=production
PORT=3000
MONGODB_URL=your-production-mongodb-url
JWT_SECRET=your-production-jwt-secret

🎯 Key Benefits of This Architecture

  1. Scalability: Domain-driven structure grows with your application
  1. Maintainability: Clear separation of concerns and consistent patterns
  1. Type Safety: Full TypeScript integration with proper typing
  1. Testability: Easy to mock dependencies and test individual components
  1. Performance: Efficient database queries and proper error handling
  1. Security: Built-in authentication, authorization, and input validation
  1. Developer Experience: Hot reloading, comprehensive logging, and clear error messages
 

🚀 Getting Started

  1. Follow the setup steps in order
  1. Start with the basic structure and add domains as needed
  1. Customize the middleware and services for your specific requirements
  1. Add comprehensive tests as you build features
  1. Deploy using your preferred hosting platform
This architecture provides a solid foundation for building enterprise-level Express.js applications that can scale from MVP to production-ready systems.