This article covers essential Node.js design patterns with real-world applications. From the Module patterns, Singleton patterns and Observer patterns, you'll see how these concepts can optimize code organization, resource management, and event handling. By incorporating these patterns, you can create more scalable, efficient, and maintainable Node.js applications.
In large Node.js applications, you need to split the code into smaller, maintainable modules. The Module Pattern helps encapsulate functionality and avoid polluting the global namespace. This pattern is used to create reusable services like authentication, logging, and database connections.
Example: Authentication Module
// authModule.ts
export const AuthModule = (() => {
const users = [{ username: "admin", password: "secret" }];
const login = (username: string, password: string): string => {
return users.find(user => user.username === username && user.password === password)
? "Login successful"
: "Invalid credentials";
};
const logout = (): string => {
return "User logged out";
};
return {
login,
logout,
};
})();
// index.ts
import { AuthModule } from "./authModule";
console.log(AuthModule.login("admin", "secret")); // Login successful
Use Case: This pattern can be used for user authentication, session management, etc.
Â
The Singleton Pattern is used when you need a single instance of a class to be shared across an application. For example, in Node.js, a database connection pool should have a single instance to avoid creating multiple connections unnecessarily.
Example: Database Connection
// dbSingleton.ts
class Database {
private static instance: Database;
private connection: { status: string };
private constructor() {
this.connection = this.connect();
}
private connect(): { status: string } {
console.log("Connecting to DB...");
return { status: "connected" };
}
public static getInstance(): Database {
if (!Database.instance)
// index.ts
import Database from "./dbSingleton";
const db1 = Database.getInstance();
const db2 = Database.getInstance();
console.log(db1.getConnection()); // { status: 'connected' }
console.log(db1 === db2); // true
Use Case: You can use this to manage a single database connection instance across your app.
Â
The Observer Pattern is useful when you want to notify multiple objects of changes in one object. It’s often used for event-based architectures or Pub/Sub messaging systems, such as when sending real-time updates to connected users in a chat application.
Example: Event Emitter in Node.js
// eventObserver.ts
import { EventEmitter } from 'events';
class Chat extends EventEmitter {}
export const chat = new Chat();
// index.ts
import { chat } from "./eventObserver";
chat.on("message", (msg: string) => {
console.log(`User1 received: ${msg}`);
});
chat.on("message", (msg: string) => {
console.log(`User2 received: ${msg}`);
});
chat.emit("message", "Hello from TypeScript!");
/*
While this is a very raw example, imagine you would want to go Class Based, thiking about OOP, and it is what you should be aiming to, here below it follows:
*/
// ChatService.ts
import { EventEmitter } from 'events';
class ChatService extends EventEmitter {
sendMessage(message: string): void {
this.emit("message", message);
}
subscribe(listener: (msg: string) => void): void {
this.on("message", listener);
}
}
export default new ChatService(); // Shared instance
// index.ts
import ChatService from "./ChatService";
ChatService.subscribe((msg) => console.log("User1 got:", msg));
ChatService.subscribe((msg) => console.log("User2 got:", msg));
ChatService.sendMessage("Hello from class-based Observer!");
Use Case: Ideal for applications with real-time notifications like chat, stock updates, etc.
Â
In Node.js, middleware is widely used, especially in frameworks like Express. The Middleware Pattern allows you to build a chain of handlers to process requests and responses, providing flexibility for cross-cutting concerns like authentication, logging, or validation.
Example: Express Middleware for Logging and Authentication
// middlewareExample.ts
import express, { Request, Response, NextFunction } from 'express';
const app = express();
// Logger middleware
const logger = (req: Request, res: Response, next: NextFunction): void => {
console.log(`${req.method} ${req.url}`);
next();
};
// Auth middleware
const isAuthenticated = (req: Request, res: Response, next: NextFunction): void => {
const auth = req.headers['authorization'];
if (auth === 'Bearer token123') {
next();
} else {
res.status(401).send("Unauthorized");
}
};
app.use(logger);
app.use(isAuthenticated);
app.get('/secure', (req: Request, res: Response) => {
res.send("This is a secure route");
});
app.listen(3000, () => console.log("Server running on http://localhost:3000"));
Use Case: Express applications heavily rely on middleware patterns to handle HTTP request processing.
Â
Thanks for reading, hope this give you a glimpse of it and you are hungry to learn more about patterns and best practices in Software Development.
I'm navigating the worlds of Frontend, Backend, and DevOps—steering through projects while blending innovation with fundamentals. Join me on this journey to uncover fresh insights and level up your skills every step of the way.
A minimal portofolio and blog website, showing my expertise and spreading the rich content of programming.