Service_layer
Chapter 22: Service Layer Design
Section titled “Chapter 22: Service Layer Design”Business Logic Implementation in TypeScript
Section titled “Business Logic Implementation in TypeScript”22.1 Service Layer Overview
Section titled “22.1 Service Layer Overview”The service layer contains the core business logic of your application. It acts as an intermediary between controllers (or HTTP handlers) and the data access layer (repositories).
Service Layer Architecture ================================================================================
+-------------------+ +-------------------+ +-------------------+ | Controllers | --> | Services | --> | Repositories | | (HTTP Layer) | | (Business Logic)| | (Data Access) | +-------------------+ +-------------------+ +-------------------+ | v +-------------------+ | Entities | | (Domain Model) | +-------------------+
Responsibilities of Service Layer: - Business rule enforcement - Transaction management - Data validation - Coordination between repositories - Error handling
================================================================================22.2 Creating a Service Class
Section titled “22.2 Creating a Service Class”import { UserRepository } from '../repositories/user.repository';import { User } from '../entities/user.entity';import { CreateUserDto } from '../dto/create-user.dto';import { UpdateUserDto } from '../dto/update-user.dto';
export class UserService { constructor(private userRepository: UserRepository) {}
async findAll(): Promise<User[]> { return this.userRepository.findAll(); }
async findById(id: number): Promise<User | null> { return this.userRepository.findOneById(id); }
async findByEmail(email: string): Promise<User | null> { return this.userRepository.findByEmail(email); }
async create(createUserDto: CreateUserDto): Promise<User> { // Check if user already exists const existingUser = await this.findByEmail(createUserDto.email); if (existingUser) { throw new Error('User with this email already exists'); }
// Validate password strength if (createUserDto.password.length < 8) { throw new Error('Password must be at least 8 characters'); }
// Create user with hashed password const hashedPassword = await this.hashPassword(createUserDto.password);
return this.userRepository.create({ ...createUserDto, password: hashedPassword, }); }
async update(id: number, updateUserDto: UpdateUserDto): Promise<User> { const user = await this.findById(id); if (!user) { throw new Error('User not found'); }
// If updating email, check for duplicates if (updateUserDto.email && updateUserDto.email !== user.email) { const existingUser = await this.findByEmail(updateUserDto.email); if (existingUser) { throw new Error('Email already in use'); } }
return this.userRepository.update(id, updateUserDto); }
async delete(id: number): Promise<void> { const user = await this.findById(id); if (!user) { throw new Error('User not found'); } await this.userRepository.delete(id); }
async activateUser(id: number): Promise<User> { return this.userRepository.update(id, { isActive: true }); }
async deactivateUser(id: number): Promise<User> { return this.userRepository.update(id, { isActive: false }); }
private async hashPassword(password: string): Promise<string> { // Use bcrypt or argon2 for production const salt = await Bun.password.hash(password); return salt; }}22.3 Transactional Services
Section titled “22.3 Transactional Services”import { DataSource } from 'typeorm';import { OrderRepository } from '../repositories/order.repository';import { ProductRepository } from '../repositories/product.repository';import { Order } from '../entities/order.entity';import { CreateOrderDto } from '../dto/create-order.dto';
export class OrderService { constructor( private dataSource: DataSource, private orderRepository: OrderRepository, private productRepository: ProductRepository, ) {}
async createOrder(dto: CreateOrderDto): Promise<Order> { return this.dataSource.transaction(async (manager) => { // Verify all products exist and have sufficient stock for (const item of dto.items) { const product = await this.productRepository.findOneById(item.productId);
if (!product) { throw new Error(`Product ${item.productId} not found`); }
if (product.stock < item.quantity) { throw new Error(`Insufficient stock for product ${product.name}`); } }
// Calculate total amount let totalAmount = 0; for (const item of dto.items) { const product = await this.productRepository.findOneById(item.productId); totalAmount += (product!.price * item.quantity); }
// Create order const order = await this.orderRepository.create({ ...dto, totalAmount, status: 'pending', }, manager);
// Reduce stock for each item for (const item of dto.items) { await this.productRepository.decreaseStock( item.productId, item.quantity, manager ); }
return order; }); }
async cancelOrder(orderId: number): Promise<Order> { return this.dataSource.transaction(async (manager) => { const order = await this.orderRepository.findOneById(orderId, manager);
if (!order) { throw new Error('Order not found'); }
if (order.status === 'cancelled') { throw new Error('Order already cancelled'); }
// Restore stock for (const item of order.items) { await this.productRepository.increaseStock( item.productId, item.quantity, manager ); }
return this.orderRepository.update(orderId, { status: 'cancelled' }, manager); }); }}22.4 Service with Caching
Section titled “22.4 Service with Caching”import { UserRepository } from '../repositories/user.repository';import { User } from '../entities/user.entity';
// Simple in-memory cache (use Redis for production)const cache = new Map<string, { data: any; expiry: number }>();
export class ProductService { constructor(private userRepository: UserRepository) {}
private async getCached<T>(key: string, fn: () => Promise<T>): Promise<T> { const cached = cache.get(key); const now = Date.now();
if (cached && cached.expiry > now) { return cached.data as T; }
const data = await fn(); cache.set(key, { data, expiry: now + 60000 }); // 1 minute cache return data; }
async findAllProducts(): Promise<User[]> { return this.getCached('products:all', () => this.userRepository.findAll()); }
async findProductById(id: number): Promise<User | null> { return this.getCached(`products:${id}`, () => this.userRepository.findOneById(id)); }
async createProduct(data: Partial<User>): Promise<User> { const product = await this.userRepository.create(data); // Invalidate cache cache.clear(); return product; }
async updateProduct(id: number, data: Partial<User>): Promise<User> { const product = await this.userRepository.update(id, data); // Invalidate specific cache cache.delete(`products:${id}`); cache.delete('products:all'); return product; }
clearCache(): void { cache.clear(); }}22.5 Error Handling in Services
Section titled “22.5 Error Handling in Services”export class UserService { // Custom error classes private throwNotFound(id: number): never { throw new NotFoundError(`User with id ${id} not found`); }
private throwValidationError(message: string): never { throw new ValidationError(message); }
private throwConflictError(message: string): never { throw new ConflictError(message); }
async findByIdOrFail(id: number): Promise<User> { const user = await this.findById(id); if (!user) { this.throwNotFound(id); } return user; }
async createUser(dto: CreateUserDto): Promise<User> { // Validate input if (!dto.email || !dto.email.includes('@')) { this.throwValidationError('Invalid email address'); }
if (!dto.password || dto.password.length < 8) { this.throwValidationError('Password must be at least 8 characters'); }
// Check for existing user const existing = await this.findByEmail(dto.email); if (existing) { this.throwConflictError('Email already registered'); }
return this.userRepository.create(dto); }}
// Error classesexport class NotFoundError extends Error { constructor(message: string) { super(message); this.name = 'NotFoundError'; }}
export class ValidationError extends Error { constructor(message: string) { super(message); this.name = 'ValidationError'; }}
export class ConflictError extends Error { constructor(message: string) { super(message); this.name = 'ConflictError'; }}22.6 Summary
Section titled “22.6 Summary”This chapter covered:
- Service Layer Responsibilities: Business logic, validation, transactions
- Service Implementation: Creating comprehensive service classes
- Transaction Management: Multi-step operations with data consistency
- Caching Strategies: Improving performance with caching
- Error Handling: Custom error classes and handling patterns
Key takeaways:
- Keep services focused on business logic
- Use transactions for multi-step operations
- Implement proper error handling
- Consider caching for frequently accessed data
Next Chapter
Section titled “Next Chapter”Chapter 23: Repository Pattern Implementation
Last Updated: February 2026