Home
Backend from First Principles / Module 9 — Handlers, Controllers & Services

Handlers, Controllers & Services

Layered architecture. Why your service layer should never know about HTTP.


The Layered Architecture

Production backends separate concerns into layers:

Handler/Controller Layer → Service/Business Logic Layer → Repository/Data Layer

Handler: Deals with HTTP. Reads request, calls service, sends response.
Service: Contains business rules. No HTTP knowledge. Pure logic.
Repository: Wraps database queries. No business logic. Just CRUD.

This separation means:
• Services are testable without HTTP or a database
• Handlers are thin and uniform
• Repositories can swap databases without touching business logic


Handlers (Controllers)

A handler's only job: translate HTTP ↔ domain.

JavaScript
// What a handler does:
async function createUserHandler(req, res) {
  // 1. Parse & validate input (or let middleware do it)
  const dto = CreateUserDto.parse(req.body);

  // 2. Call the service
  const user = await userService.createUser(dto);

  // 3. Serialize and respond
  res.status(201).json(toUserResponse(user));
}

What a handler should NOT do:
• Database queries
• Business logic ("if user is admin, give 20% discount")
• Sending emails directly
• Complex data transformations

Keep handlers stupid. They're glue code.


Services (Business Logic Layer)

The service layer is the heart of your application. This is where decisions live.

JavaScript
// Service example
class UserService {
  constructor(private userRepo, private emailService, private cache) {}

  async createUser(dto) {
    // Business rules:
    const existing = await this.userRepo.findByEmail(dto.email);
    if (existing) throw new ConflictError("Email already in use");

    const hashedPassword = await bcrypt.hash(dto.password, 12);
    const user = await this.userRepo.create({ ...dto, hashedPassword });

    await this.emailService.sendWelcomeEmail(user.email);
    await this.cache.invalidate("users:list");

    return user;
  }
}

Services should be:
• Framework-agnostic (no req/res objects)
• Testable with mocked dependencies
• Composed through dependency injection


Repository Pattern

A repository abstracts the database. Instead of raw SQL everywhere, you have methods:

JavaScript
interface UserRepository {
  findById(id: string): Promise<User | null>
  findByEmail(email: string): Promise<User | null>
  create(data: CreateUserInput): Promise<User>
  update(id: string, data: Partial<User>): Promise<User>
  delete(id: string): Promise<void>
  findMany(filters: UserFilters): Promise<User[]>
}

Benefits:
• Swap databases without touching business logic
• Easy to mock in tests
• Consistent query interface
• Enforce data access patterns (no raw queries scattered in services)


Source & Credit

The Backend from First Principles series is based on what I learnt from Sriniously's YouTube playlist — a thoughtful, framework-agnostic walk through backend engineering. If this material helped you, please go check the original out: youtube.com/@Sriniously. The notes here are my own restatement for revisiting later.

⁂ Back to all modules