Skip to content

Service_layer

Business Logic Implementation in TypeScript

Section titled “Business Logic Implementation in TypeScript”

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
================================================================================

src/services/user.service.ts
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;
}
}

src/services/order.service.ts
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);
});
}
}

src/services/product.service.ts
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();
}
}

src/services/user.service.ts
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 classes
export 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';
}
}

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:

  1. Keep services focused on business logic
  2. Use transactions for multi-step operations
  3. Implement proper error handling
  4. Consider caching for frequently accessed data

Chapter 23: Repository Pattern Implementation


Last Updated: February 2026