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.