Service Layer Extractor Extract business logic from controllers into a testable service layer. Architecture Layers routes/ → Define endpoints, parse requests controllers/ → Validate input, call services, format responses services/ → Business logic, orchestration repositories/ → Database queries models/ → Data structures Before: Fat Controller // ❌ Business logic mixed with HTTP concerns router . post ( "/users" , async ( req , res ) => { try { // Validation if ( ! req . body . email ) { return res . status ( 400 ) . json ( { error : "Email required" } ) ; } // Check duplicate const existing = await db . query ( "SELECT * FROM users WHERE email = $1" , [ req . body . email , ] ) ; if ( existing . rows . length
0 ) { return res . status ( 409 ) . json ( { error : "Email already exists" } ) ; } // Hash password const hashedPassword = await bcrypt . hash ( req . body . password , 10 ) ; // Create user const result = await db . query ( "INSERT INTO users (email, password, name) VALUES ($1, $2, $3) RETURNING *" , [ req . body . email , hashedPassword , req . body . name ] ) ; // Send welcome email await sendEmail ( req . body . email , "Welcome!" , "Thanks for joining" ) ; res . status ( 201 ) . json ( result . rows [ 0 ] ) ; } catch ( err ) { res . status ( 500 ) . json ( { error : err . message } ) ; } } ) ; After: Service Layer // ✅ Separated concerns // services/user.service.ts export class UserService { constructor ( private userRepository : UserRepository , private emailService : EmailService ) { } async createUser ( dto : CreateUserDto ) : Promise < User
{ // Business logic only const existing = await this . userRepository . findByEmail ( dto . email ) ; if ( existing ) { throw new ConflictError ( "Email already exists" ) ; } const hashedPassword = await bcrypt . hash ( dto . password , 10 ) ; const user = await this . userRepository . create ( { ... dto , password : hashedPassword , } ) ; await this . emailService . sendWelcome ( user . email ) ; return user ; } } // controllers/user.controller.ts export class UserController { constructor ( private userService : UserService ) { } create = asyncHandler ( async ( req , res ) => { const user = await this . userService . createUser ( req . body ) ; res . status ( 201 ) . json ( { success : true , data : user } ) ; } ) ; } // repositories/user.repository.ts export class UserRepository { async create ( data : CreateUserData ) : Promise < User
{ const result = await db . query ( "INSERT INTO users (email, password, name) VALUES ($1, $2, $3) RETURNING *" , [ data . email , data . password , data . name ] ) ; return result . rows [ 0 ] ; } async findByEmail ( email : string ) : Promise < User | null
{ const result = await db . query ( "SELECT * FROM users WHERE email = $1" , [ email , ] ) ; return result . rows [ 0 ] || null ; } } Dependency Injection // container.ts (using tsyringe or manual) import { UserService } from "./services/user.service" ; import { UserRepository } from "./repositories/user.repository" ; import { EmailService } from "./services/email.service" ; export class Container { private static instances = new Map ( ) ; static get < T
( constructor : new ( ... args : any [ ] ) => T ) : T { if ( ! this . instances . has ( constructor ) ) { // Create dependencies const deps = this . resolveDependencies ( constructor ) ; this . instances . set ( constructor , new constructor ( ... deps ) ) ; } return this . instances . get ( constructor ) ; } private static resolveDependencies ( constructor : any ) : any [ ] { // Resolve constructor dependencies return [ ] ; } } // Usage const userService = Container . get ( UserService ) ; Testing Services // user.service.test.ts describe ( "UserService" , ( ) => { let service : UserService ; let mockRepository : jest . Mocked < UserRepository
; let mockEmailService : jest . Mocked < EmailService
; beforeEach ( ( ) => { mockRepository = { create : jest . fn ( ) , findByEmail : jest . fn ( ) , } as any ; mockEmailService = { sendWelcome : jest . fn ( ) , } as any ; service = new UserService ( mockRepository , mockEmailService ) ; } ) ; it ( "creates user successfully" , async ( ) => { mockRepository . findByEmail . mockResolvedValue ( null ) ; mockRepository . create . mockResolvedValue ( { id : "1" , email : "test@example.com" , } ) ; const user = await service . createUser ( { email : "test@example.com" , password : "password123" , name : "Test User" , } ) ; expect ( user . id ) . toBe ( "1" ) ; expect ( mockEmailService . sendWelcome ) . toHaveBeenCalled ( ) ; } ) ; it ( "throws error if email exists" , async ( ) => { mockRepository . findByEmail . mockResolvedValue ( { id : "1" } as User ) ; await expect ( service . createUser ( { email : "existing@example.com" , password : "pass" , name : "Test" , } ) ) . rejects . toThrow ( ConflictError ) ; } ) ; } ) ; Folder Structure src/ ├── routes/ │ └── users.routes.ts ├── controllers/ │ └── user.controller.ts ├── services/ │ ├── user.service.ts │ ├── email.service.ts │ └── payment.service.ts ├── repositories/ │ └── user.repository.ts ├── models/ │ └── user.model.ts ├── types/ │ └── user.types.ts └── middleware/ └── validate.ts Migration Strategy
Phase 1: Create Service Layer (Week 1-2)
[ ] Create service classes
[ ] Move business logic to services
[ ] Keep controllers thin
[ ] No breaking changes
Phase 2: Add Tests (Week 3-4)
[ ] Write service unit tests
[ ] Mock dependencies
[ ] Achieve 80%+ coverage
Phase 3: Extract Repositories (Week 5-6)
[ ] Create repository layer
[ ] Move DB queries from services
[ ] Services depend on repositories
Phase 4: Dependency Injection (Week 7-8)
[ ] Set up DI container
[ ] Remove manual instantiation
- [ ] Wire up dependencies
- Benefits
- Testability
-
- Services testable without HTTP
- Reusability
-
- Logic reused across endpoints
- Separation
-
- Clear boundaries between layers
- Maintainability
- Easier to locate and modify logic Output Checklist Service classes created Business logic extracted from controllers Repository layer for data access Dependency injection setup Unit tests for services Folder structure reorganized Migration plan documented Team trained on new patterns