Skip to main content

Module 33: Real-World Project Patterns

Explore production-ready patterns and architectural approaches for building scalable TypeScript applications.


1. Clean Architecture

// Domain Layer (entities)
export class User {
constructor(
public readonly id: string,
public name: string,
public email: string
) {}

updateEmail(email: string): void {
if (!this.isValidEmail(email)) {
throw new Error("Invalid email");
}
this.email = email;
}

private isValidEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
}

// Use Case Layer
export interface UserRepository {
findById(id: string): Promise<User | null>;
save(user: User): Promise<void>;
}

export class UpdateUserEmail {
constructor(private userRepository: UserRepository) {}

async execute(userId: string, newEmail: string): Promise<void> {
const user = await this.userRepository.findById(userId);
if (!user) {
throw new Error("User not found");
}
user.updateEmail(newEmail);
await this.userRepository.save(user);
}
}

// Infrastructure Layer
export class MongoUserRepository implements UserRepository {
async findById(id: string): Promise<User | null> {
// MongoDB implementation
return null;
}

async save(user: User): Promise<void> {
// MongoDB implementation
}
}

// Presentation Layer (API)
app.put("/users/:id/email", async (req, res) => {
const useCase = new UpdateUserEmail(new MongoUserRepository());
await useCase.execute(req.params.id, req.body.email);
res.json({ success: true });
});

2. Repository Pattern

export interface Repository<T> {
findById(id: string): Promise<T | null>;
findAll(): Promise<T[]>;
save(entity: T): Promise<T>;
delete(id: string): Promise<void>;
}

export class UserRepository implements Repository<User> {
constructor(private db: Database) {}

async findById(id: string): Promise<User | null> {
const row = await this.db.query(
"SELECT * FROM users WHERE id = $1",
[id]
);
return row ? this.mapToEntity(row) : null;
}

async findAll(): Promise<User[]> {
const rows = await this.db.query("SELECT * FROM users");
return rows.map(this.mapToEntity);
}

async save(user: User): Promise<User> {
const result = await this.db.query(
"INSERT INTO users (id, name, email) VALUES ($1, $2, $3) RETURNING *",
[user.id, user.name, user.email]
);
return this.mapToEntity(result);
}

async delete(id: string): Promise<void> {
await this.db.query("DELETE FROM users WHERE id = $1", [id]);
}

private mapToEntity(row: any): User {
return new User(row.id, row.name, row.email);
}
}

3. Service Layer Pattern

export class UserService {
constructor(
private userRepository: UserRepository,
private emailService: EmailService,
private logger: Logger
) {}

async createUser(data: CreateUserDTO): Promise<User> {
// Validation
if (!data.email || !data.name) {
throw new ValidationError("Missing required fields");
}

// Business logic
const user = new User(
generateId(),
data.name,
data.email
);

// Persistence
await this.userRepository.save(user);

// Side effects
await this.emailService.sendWelcomeEmail(user.email);
this.logger.info(`User created: ${user.id}`);

return user;
}

async getUserById(id: string): Promise<User> {
const user = await this.userRepository.findById(id);
if (!user) {
throw new NotFoundError("User not found");
}
return user;
}
}

4. Dependency Injection

// DI Container
export class Container {
private services = new Map<string, any>();

register<T>(key: string, factory: () => T): void {
this.services.set(key, factory);
}

resolve<T>(key: string): T {
const factory = this.services.get(key);
if (!factory) {
throw new Error(`Service not found: ${key}`);
}
return factory();
}
}

// Setup
const container = new Container();

container.register("database", () => new Database());
container.register("userRepository", () =>
new UserRepository(container.resolve("database"))
);
container.register("userService", () =>
new UserService(
container.resolve("userRepository"),
container.resolve("emailService"),
container.resolve("logger")
)
);

// Usage
const userService = container.resolve<UserService>("userService");

5. CQRS Pattern

// Commands (write operations)
export interface Command<T> {
execute(): Promise<T>;
}

export class CreateUserCommand implements Command<User> {
constructor(
private data: CreateUserDTO,
private repository: UserRepository
) {}

async execute(): Promise<User> {
const user = new User(generateId(), this.data.name, this.data.email);
await this.repository.save(user);
return user;
}
}

// Queries (read operations)
export interface Query<T> {
execute(): Promise<T>;
}

export class GetUserQuery implements Query<User | null> {
constructor(
private id: string,
private repository: UserRepository
) {}

async execute(): Promise<User | null> {
return this.repository.findById(this.id);
}
}

// Command/Query Bus
export class CommandBus {
async execute<T>(command: Command<T>): Promise<T> {
return command.execute();
}
}

export class QueryBus {
async execute<T>(query: Query<T>): Promise<T> {
return query.execute();
}
}

6. Middleware Pattern

type Middleware<T> = (
context: T,
next: () => Promise<void>
) => Promise<void>;

export class MiddlewareChain<T> {
private middlewares: Middleware<T>[] = [];

use(middleware: Middleware<T>): this {
this.middlewares.push(middleware);
return this;
}

async execute(context: T): Promise<void> {
let index = 0;

const next = async (): Promise<void> => {
if (index < this.middlewares.length) {
const middleware = this.middlewares[index++];
await middleware(context, next);
}
};

await next();
}
}

// Usage
const chain = new MiddlewareChain<RequestContext>();

chain
.use(async (ctx, next) => {
console.log("Authentication");
await next();
})
.use(async (ctx, next) => {
console.log("Authorization");
await next();
})
.use(async (ctx, next) => {
console.log("Request handler");
});

await chain.execute(context);

7. Error Handling Strategy

// Base error classes
export abstract class AppError extends Error {
abstract statusCode: number;
abstract serialize(): ErrorResponse;
}

export class ValidationError extends AppError {
statusCode = 400;

constructor(public errors: Record<string, string[]>) {
super("Validation failed");
}

serialize(): ErrorResponse {
return {
error: "Validation Error",
details: this.errors
};
}
}

export class NotFoundError extends AppError {
statusCode = 404;

constructor(public resource: string) {
super(`${resource} not found`);
}

serialize(): ErrorResponse {
return {
error: "Not Found",
message: this.message
};
}
}

// Global error handler
export function errorHandler(
err: Error,
req: Request,
res: Response,
next: NextFunction
): void {
if (err instanceof AppError) {
res.status(err.statusCode).json(err.serialize());
} else {
logger.error(err);
res.status(500).json({
error: "Internal Server Error",
message: "An unexpected error occurred"
});
}
}

8. Configuration Management

import { z } from "zod";

const configSchema = z.object({
env: z.enum(["development", "production", "test"]),
port: z.number().min(1000).max(65535),
database: z.object({
host: z.string(),
port: z.number(),
name: z.string(),
user: z.string(),
password: z.string()
}),
jwt: z.object({
secret: z.string().min(32),
expiresIn: z.string()
}),
redis: z.object({
host: z.string(),
port: z.number()
})
});

export type Config = z.infer<typeof configSchema>;

export function loadConfig(): Config {
const config = {
env: process.env.NODE_ENV || "development",
port: parseInt(process.env.PORT || "3000", 10),
database: {
host: process.env.DB_HOST || "localhost",
port: parseInt(process.env.DB_PORT || "5432", 10),
name: process.env.DB_NAME || "myapp",
user: process.env.DB_USER || "user",
password: process.env.DB_PASSWORD || "password"
},
jwt: {
secret: process.env.JWT_SECRET || "",
expiresIn: process.env.JWT_EXPIRES_IN || "7d"
},
redis: {
host: process.env.REDIS_HOST || "localhost",
port: parseInt(process.env.REDIS_PORT || "6379", 10)
}
};

return configSchema.parse(config);
}

9. Testing Structure

// Unit tests
describe("UserService", () => {
let userService: UserService;
let mockRepository: jest.Mocked<UserRepository>;
let mockEmailService: jest.Mocked<EmailService>;

beforeEach(() => {
mockRepository = {
save: jest.fn(),
findById: jest.fn()
} as any;

mockEmailService = {
sendWelcomeEmail: jest.fn()
} as any;

userService = new UserService(
mockRepository,
mockEmailService,
new Logger()
);
});

it("should create user", async () => {
const userData = { name: "John", email: "john@example.com" };
await userService.createUser(userData);

expect(mockRepository.save).toHaveBeenCalled();
expect(mockEmailService.sendWelcomeEmail).toHaveBeenCalledWith(
userData.email
);
});
});

// Integration tests
describe("User API", () => {
let app: Express;

beforeAll(async () => {
app = await createApp();
});

it("POST /users creates user", async () => {
const response = await request(app)
.post("/users")
.send({ name: "John", email: "john@example.com" });

expect(response.status).toBe(201);
expect(response.body).toHaveProperty("id");
});
});

Key Takeaways

Clean Architecture separates concerns
Repository Pattern abstracts data access
Service Layer encapsulates business logic
Dependency Injection improves testability
CQRS separates reads and writes
Error handling strategy for consistency


Practice Exercises

Exercise 1: Build REST API

Create a complete REST API with clean architecture.

Exercise 2: Implement CQRS

Build a system using CQRS pattern with event sourcing.


Next Steps

In Module 34, we'll prepare for TypeScript Interview Questions and common challenges.