Applying Design Patterns for Scalable Express Applications

Design patterns are proven solutions to common architectural problems in software development. Applying the right design patterns can make your Express.js application scalable, maintainable, and robust. In this chapter, we will explore a range of design patterns, from basic to advanced, tailored for building scalable Express applications.

Introduction to Design Patterns

What are Design Patterns?

Design patterns are reusable solutions to common problems encountered in software design. They provide templates for writing code in a structured and efficient manner.

Why Use Design Patterns in Express.js Applications?

  • Improved Scalability: Handle increased traffic and features without major refactoring.
  • Maintainability: Makes the code easier to read, debug, and extend.
  • Reusability: Common solutions can be applied across different modules.

Overview of Scalability and Maintainability

Scalability ensures that an application can handle a growing number of requests, while maintainability ensures the ease of updating or fixing the code.

Basic Design Patterns

Middleware Pattern

The middleware pattern is fundamental to Express.js. Middleware functions process requests in a sequence.

Example:

				
					const express = require('express');
const app = express();

// Middleware for logging
function logger(req, res, next) {
  console.log(`Request Method: ${req.method}, URL: ${req.url}`);
  next();
}

app.use(logger);

app.get('/', (req, res) => {
  res.send('Hello, World!');
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

				
			

Explanation:

Middleware functions like logger are used to perform actions like logging, authentication, or error handling before passing control to the next handler.

Router Pattern

The router pattern organizes application routes into separate modules for better scalability.

Example:

				
					// userRoutes.js
const express = require('express');
const router = express.Router();

router.get('/', (req, res) => {
  res.send('List of users');
});

router.post('/', (req, res) => {
  res.send('Create a user');
});

module.exports = router;

// main app
const userRoutes = require('./userRoutes');
const app = express();

app.use('/users', userRoutes);

app.listen(3000, () => console.log('Server running on port 3000'));

				
			

Explanation:

The router pattern modularizes routes, making it easier to scale and manage as the application grows.

Intermediate Design Patterns

Dependency Injection

Dependency injection decouples modules by injecting dependencies instead of hardcoding them.

Example:

				
					class Logger {
  log(message) {
    console.log(message);
  }
}

class UserService {
  constructor(logger) {
    this.logger = logger;
  }

  getUser() {
    this.logger.log('Fetching user');
    return { id: 1, name: 'John Doe' };
  }
}

const logger = new Logger();
const userService = new UserService(logger);

console.log(userService.getUser());

				
			

Explanation:

Injecting Logger into UserService makes the UserService more testable and easier to replace dependencies.

Singleton Pattern

The singleton pattern ensures that a class has only one instance.

Example:

				
					class Database {
  constructor() {
    if (Database.instance) {
      return Database.instance;
    }
    this.connection = 'Connected to DB';
    Database.instance = this;
  }
}

const db1 = new Database();
const db2 = new Database();

console.log(db1 === db2); // true

				
			

Explanation:

Using a singleton ensures that only one database connection is created, saving resources.

Advanced Design Patterns

Repository Pattern

The repository pattern abstracts data access, making it easier to switch databases.

Example:

				
					class UserRepository {
  constructor(db) {
    this.db = db;
  }

  async getUserById(id) {
    return this.db.query(`SELECT * FROM users WHERE id = ${id}`);
  }
}

const db = {
  query: (sql) => Promise.resolve({ id: 1, name: 'John Doe' }), // Mock DB
};
const userRepository = new UserRepository(db);

userRepository.getUserById(1).then(console.log);

				
			

Factory Pattern

The factory pattern creates objects without specifying the exact class.

Example:

				
					class UserService {
  static createUser(type) {
    if (type === 'admin') {
      return { id: 1, role: 'admin' };
    } else {
      return { id: 2, role: 'user' };
    }
  }
}

console.log(UserService.createUser('admin'));

				
			

Event-Driven Architecture

Observer Pattern

The observer pattern allows modules to subscribe to events and react asynchronously.

Example:

				
					const EventEmitter = require('events');
const eventEmitter = new EventEmitter();

eventEmitter.on('userCreated', (user) => {
  console.log(`User created: ${user.name}`);
});

eventEmitter.emit('userCreated', { id: 1, name: 'John Doe' });

				
			

Layered Architecture for Express.js

Service Layer

Handles business logic.

Controller Layer

Handles HTTP requests and responses.

Repository Layer

Handles database queries.

Example:

				
					// repository.js
class UserRepository {
  getUserById(id) {
    return { id, name: 'John Doe' }; // Mock DB
  }
}

// service.js
class UserService {
  constructor(userRepository) {
    this.userRepository = userRepository;
  }

  getUser(id) {
    return this.userRepository.getUserById(id);
  }
}

// controller.js
const express = require('express');
const app = express();
const UserRepository = new UserRepository();
const UserService = new UserService(UserRepository);

app.get('/user/:id', (req, res) => {
  const user = UserService.getUser(req.params.id);
  res.send(user);
});

app.listen(3000, () => console.log('Server running on port 3000'));

				
			

Case Study: Building a Scalable REST API

Applying the patterns discussed, a REST API is built with:

  • Middleware for logging.
  • Router for endpoints.
  • Repository pattern for data access.

Best Practices

  • Choose patterns based on application needs.
  • Avoid over-engineering with unnecessary patterns.
  • Test thoroughly.

Design patterns provide a structured approach to building scalable and maintainable Express.js applications. By understanding and applying these patterns, developers can create robust systems ready to handle growth and complexity. Happy coding !❤️

Table of Contents

Contact here

Copyright © 2025 Diginode

Made with ❤️ in India