Skip to content

Advanced Express Integration Example

More complex Express.js application with multiple services, middleware, authentication, error handling, and request context.

Code

typescript
import express, { Request, Response, NextFunction } from 'express';
import { ServiceCollection, ServiceProvider } from '@nodelibraries/ioc';

/**
 * Advanced Express Integration Example
 *
 * This example demonstrates a more complex Express.js application with:
 * - Multiple services with dependencies
 * - Middleware using IoC container
 * - Error handling with dependency injection
 * - Request-scoped services
 * - Service lifecycle management
 */

// ============================================
// Interfaces
// ============================================

interface ILogger {
  log(message: string, level?: string): void;
  error(message: string, error?: Error): void;
}

interface IConfig {
  get(key: string): string | undefined;
  getNumber(key: string): number | undefined;
  getBoolean(key: string): boolean | undefined;
}

interface IUserRepository {
  findById(id: number): Promise<User | null>;
  findAll(): Promise<User[]>;
  create(user: Partial<User>): Promise<User>;
}

interface IUserService {
  getUserById(id: number): Promise<User | null>;
  getAllUsers(): Promise<User[]>;
  createUser(data: CreateUserDto): Promise<User>;
}

interface IAuthService {
  validateToken(token: string): Promise<boolean>;
  getUserIdFromToken(token: string): Promise<number | null>;
}

interface IRequestContext {
  userId?: number;
  requestId: string;
  startTime: number;
}

// ============================================
// Models
// ============================================

interface User {
  id: number;
  name: string;
  email: string;
  createdAt: Date;
}

interface CreateUserDto {
  name: string;
  email: string;
}

// ============================================
// Implementations
// ============================================

class Logger implements ILogger {
  constructor(private config: IConfig) {}

  log(message: string, level: string = 'INFO'): void {
    const timestamp = new Date().toISOString();
    const prefix = this.config.getBoolean('LOG_COLORS') ? `\x1b[36m[${level}]\x1b[0m` : `[${level}]`;
    console.log(`${prefix} ${timestamp} - ${message}`);
  }

  error(message: string, error?: Error): void {
    const timestamp = new Date().toISOString();
    console.error(`[ERROR] ${timestamp} - ${message}`, error);
  }
}

class Config implements IConfig {
  private values: Map<string, string> = new Map();

  constructor() {
    // Load from environment variables
    this.values.set('PORT', process.env.PORT || '3000');
    this.values.set('NODE_ENV', process.env.NODE_ENV || 'development');
    this.values.set('LOG_COLORS', process.env.LOG_COLORS || 'true');
    this.values.set('JWT_SECRET', process.env.JWT_SECRET || 'secret-key');
  }

  get(key: string): string | undefined {
    return this.values.get(key);
  }

  getNumber(key: string): number | undefined {
    const value = this.values.get(key);
    return value ? parseInt(value, 10) : undefined;
  }

  getBoolean(key: string): boolean | undefined {
    const value = this.values.get(key);
    return value === 'true' || value === '1';
  }
}

class UserRepository implements IUserRepository {
  private users: User[] = [
    { id: 1, name: 'Alice', email: 'alice@example.com', createdAt: new Date() },
    { id: 2, name: 'Bob', email: 'bob@example.com', createdAt: new Date() },
    { id: 3, name: 'Charlie', email: 'charlie@example.com', createdAt: new Date() },
  ];

  constructor(private logger: ILogger) {
    this.logger.log('UserRepository initialized');
  }

  async findById(id: number): Promise<User | null> {
    this.logger.log(`Finding user by id: ${id}`);
    return this.users.find((u) => u.id === id) || null;
  }

  async findAll(): Promise<User[]> {
    this.logger.log('Finding all users');
    return [...this.users];
  }

  async create(user: Partial<User>): Promise<User> {
    const newUser: User = {
      id: this.users.length + 1,
      name: user.name!,
      email: user.email!,
      createdAt: new Date(),
    };
    this.users.push(newUser);
    this.logger.log(`Created user: ${newUser.name} (${newUser.id})`);
    return newUser;
  }
}

class UserService implements IUserService {
  constructor(private repository: IUserRepository, private logger: ILogger) {
    this.logger.log('UserService initialized');
  }

  async getUserById(id: number): Promise<User | null> {
    this.logger.log(`UserService.getUserById(${id})`);
    return this.repository.findById(id);
  }

  async getAllUsers(): Promise<User[]> {
    this.logger.log('UserService.getAllUsers()');
    return this.repository.findAll();
  }

  async createUser(data: CreateUserDto): Promise<User> {
    this.logger.log(`UserService.createUser(${data.name})`);
    return this.repository.create(data);
  }
}

class AuthService implements IAuthService {
  constructor(private config: IConfig, private logger: ILogger) {
    this.logger.log('AuthService initialized');
  }

  async validateToken(token: string): Promise<boolean> {
    // Simple token validation (in real app, use JWT)
    this.logger.log('Validating token');
    return token === 'valid-token' || token.startsWith('Bearer valid-');
  }

  async getUserIdFromToken(token: string): Promise<number | null> {
    // Simple token parsing (in real app, decode JWT)
    this.logger.log('Extracting user ID from token');
    if (token === 'valid-token') return 1;
    if (token.startsWith('Bearer valid-')) {
      const userId = parseInt(token.replace('Bearer valid-', ''), 10);
      return isNaN(userId) ? null : userId;
    }
    return null;
  }
}

// ============================================
// Tokens
// ============================================

const ILoggerToken = Symbol('ILogger');
const IConfigToken = Symbol('IConfig');
const IUserRepositoryToken = Symbol('IUserRepository');
const IUserServiceToken = Symbol('IUserService');
const IAuthServiceToken = Symbol('IAuthService');
const IRequestContextToken = Symbol('IRequestContext');

// ============================================
// Middleware
// ============================================

// Request context middleware - creates scoped container per request
function createRequestScopeMiddleware(provider: ServiceProvider) {
  return async (req: Request, res: Response, next: NextFunction) => {
    const requestId = `req-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
    const startTime = Date.now();

    // Create scoped container for this request
    const scope = provider.createScope();

    // Store request context in request object (since we can't add values to existing scope)
    (req as any).requestContext = {
      requestId,
      startTime,
    };

    // Attach scope to request
    (req as any).scope = scope;

    // Log request start
    const logger = await scope.getRequiredService<ILogger>(ILoggerToken);
    logger.log(`Request started: ${req.method} ${req.path} [${requestId}]`);

    // Cleanup on response finish
    res.on('finish', async () => {
      const duration = Date.now() - startTime;
      logger.log(`Request finished: ${req.method} ${req.path} [${requestId}] - ${duration}ms`);
      try {
        await scope.dispose();
      } catch (error) {
        console.error('Error disposing scope:', error);
      }
    });

    next();
  };
}

// Authentication middleware
function createAuthMiddleware() {
  return async (req: Request, res: Response, next: NextFunction) => {
    const scope: ServiceProvider = (req as any).scope;
    const authService = await scope.getRequiredService<IAuthService>(IAuthServiceToken);
    const logger = await scope.getRequiredService<ILogger>(ILoggerToken);
    const context: IRequestContext = (req as any).requestContext;

    const token = req.headers.authorization;

    if (!token) {
      logger.error('No authorization token provided', undefined);
      return res.status(401).json({ error: 'Unauthorized' });
    }

    const isValid = await authService.validateToken(token);
    if (!isValid) {
      logger.error('Invalid token', undefined);
      return res.status(401).json({ error: 'Invalid token' });
    }

    const userId = await authService.getUserIdFromToken(token);
    if (userId) {
      context.userId = userId;
      logger.log(`User authenticated: ${userId}`);
    }

    next();
  };
}

// Error handling middleware
function createErrorHandler() {
  return (err: Error, req: Request, res: Response, _next: NextFunction) => {
    const scope: ServiceProvider = (req as any).scope;
    if (scope) {
      scope
        .getRequiredService<ILogger>(ILoggerToken)
        .then((logger) => {
          logger.error('Request error', err);
        })
        .catch(() => {
          console.error('Error in error handler:', err);
        });
    } else {
      console.error('Request error (no scope):', err);
    }

    res.status(500).json({
      error: 'Internal server error',
      message: process.env.NODE_ENV === 'development' ? err.message : undefined,
    });
  };
}

// ============================================
// Routes
// ============================================

function createUserRoutes() {
  const router = express.Router();

  // GET /users - Get all users
  router.get('/', async (req: Request, res: Response, next: NextFunction) => {
    try {
      const scope: ServiceProvider = (req as any).scope;
      const userService = await scope.getRequiredService<IUserService>(IUserServiceToken);
      const context: IRequestContext = (req as any).requestContext;

      const logger = await scope.getRequiredService<ILogger>(ILoggerToken);
      logger.log(`Fetching all users [${context.requestId}]`);

      const users = await userService.getAllUsers();
      res.json({ users, requestId: context.requestId });
    } catch (error) {
      next(error);
    }
  });

  // GET /users/:id - Get user by ID
  router.get('/:id', async (req: Request, res: Response, next: NextFunction) => {
    try {
      const scope: ServiceProvider = (req as any).scope;
      const userService = await scope.getRequiredService<IUserService>(IUserServiceToken);
      const context: IRequestContext = (req as any).requestContext;

      const logger = await scope.getRequiredService<ILogger>(ILoggerToken);
      const userId = parseInt(req.params.id, 10);

      logger.log(`Fetching user ${userId} [${context.requestId}]`);

      const user = await userService.getUserById(userId);
      if (!user) {
        return res.status(404).json({ error: 'User not found', requestId: context.requestId });
      }

      res.json({ user, requestId: context.requestId });
    } catch (error) {
      next(error);
    }
  });

  // POST /users - Create new user (requires authentication)
  router.post('/', createAuthMiddleware(), async (req: Request, res: Response, next: NextFunction) => {
    try {
      const scope: ServiceProvider = (req as any).scope;
      const userService = await scope.getRequiredService<IUserService>(IUserServiceToken);
      const context: IRequestContext = (req as any).requestContext;

      const logger = await scope.getRequiredService<ILogger>(ILoggerToken);
      logger.log(`Creating user [${context.requestId}] by user ${context.userId}`);

      const { name, email } = req.body;
      if (!name || !email) {
        return res.status(400).json({ error: 'Name and email are required', requestId: context.requestId });
      }

      const user = await userService.createUser({ name, email });
      res.status(201).json({ user, requestId: context.requestId });
    } catch (error) {
      next(error);
    }
  });

  return router;
}

// ============================================
// Application Setup
// ============================================

async function createApp(): Promise<express.Application> {
  // Setup IoC container
  const services = new ServiceCollection();

  // Register services
  services.addSingleton<IConfig>(IConfigToken, Config);
  services.addSingleton<ILogger>(ILoggerToken, Logger, [IConfigToken]);
  services.addScoped<IUserRepository>(IUserRepositoryToken, UserRepository, [ILoggerToken]);
  services.addScoped<IUserService>(IUserServiceToken, UserService, [IUserRepositoryToken, ILoggerToken]);
  services.addSingleton<IAuthService>(IAuthServiceToken, AuthService, [IConfigToken, ILoggerToken]);

  // Build root provider
  const provider = services.buildServiceProvider();

  // Create Express app
  const app = express();
  app.use(express.json());

  // Request scope middleware (must be first)
  app.use(createRequestScopeMiddleware(provider));

  // Routes
  app.use('/users', createUserRoutes());

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

  // Error handler (must be last)
  app.use(createErrorHandler());

  return app;
}

// ============================================
// Main
// ============================================

async function main() {
  console.log('=== Advanced Express Integration Example ===\n');

  const app = await createApp();
  const port = process.env.PORT || 3000;

  app.listen(port, () => {
    console.log(`\n🚀 Server running on http://localhost:${port}`);
    console.log('\nAvailable endpoints:');
    console.log('  GET  /health              - Health check');
    console.log('  GET  /users               - Get all users');
    console.log('  GET  /users/:id           - Get user by ID');
    console.log('  POST /users               - Create user (requires auth)');
    console.log('\nTest with:');
    console.log('  curl http://localhost:3000/health');
    console.log('  curl http://localhost:3000/users');
    console.log('  curl http://localhost:3000/users/1');
    console.log('  curl -X POST http://localhost:3000/users \\');
    console.log('       -H "Authorization: valid-token" \\');
    console.log('       -H "Content-Type: application/json" \\');
    console.log('       -d \'{"name":"David","email":"david@example.com"}\'');
    console.log('\nPress Ctrl+C to stop the server\n');
  });
}

// Run if executed directly
if (require.main === module) {
  main().catch(console.error);
}

export { createApp };

Expected Output

=== Advanced Express Integration Example ===

[INFO] <timestamp> - Config initialized
[INFO] <timestamp> - Logger initialized
[INFO] <timestamp> - AuthService initialized
[INFO] <timestamp> - UserRepository initialized
[INFO] <timestamp> - UserService initialized

🚀 Server running on http://localhost:3000

Available endpoints:
  GET  /health              - Health check
  GET  /users               - Get all users
  GET  /users/:id           - Get user by ID
  POST /users               - Create user (requires auth)

Test with:
  curl http://localhost:3000/health
  curl http://localhost:3000/users
  curl http://localhost:3000/users/1
  curl -X POST http://localhost:3000/users \
       -H "Authorization: valid-token" \
       -H "Content-Type: application/json" \
       -d '{"name":"David","email":"david@example.com}'

Press Ctrl+C to stop the server

[INFO] <timestamp> - Request started: GET /health [req-...]
[INFO] <timestamp> - Request finished: GET /health [req-...] - <duration>ms

[INFO] <timestamp> - Request started: GET /users [req-...]
[INFO] <timestamp> - UserRepository initialized
[INFO] <timestamp> - UserService initialized
[INFO] <timestamp> - Fetching all users [req-...]
[INFO] <timestamp> - Finding all users
[INFO] <timestamp> - Request finished: GET /users [req-...] - <duration>ms

Run This Example

bash
# First, install Express dependencies
npm install --save-dev express @types/express ts-node

# Then run the example
npx ts-node examples/17-express-advanced.ts

Test the API

bash
# Health check
curl http://localhost:3000/health

# Get all users
curl http://localhost:3000/users

# Get user by ID
curl http://localhost:3000/users/1

# Create user (requires authentication)
curl -X POST http://localhost:3000/users \
  -H "Authorization: valid-token" \
  -H "Content-Type: application/json" \
  -d '{"name":"David","email":"david@example.com"}'

Key Points

  • Multiple Services: Complex service dependencies
  • Request Context: Request-scoped context with request ID and timing
  • Authentication Middleware: Token-based authentication
  • Error Handling: Centralized error handling with dependency injection
  • Logging: Request logging with timing information

Released under the ISC License. If you find this project helpful, consider buying me a coffee