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.