service-layer-extractor
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
More from monkey1sai/openai-cli
multi-tenant-safety-checker
Ensures tenant isolation at query and policy level using Row Level Security, automated testing, and security audits. Prevents data leakage between tenants. Use for "multi-tenancy", "tenant isolation", "RLS", or "data security".
10modal-drawer-system
Implements accessible modals and drawers with focus trap, ESC to close, scroll lock, portal rendering, and ARIA attributes. Includes sample implementations for common use cases like edit forms, confirmations, and detail views. Use when building "modals", "dialogs", "drawers", "sidebars", or "overlays".
10eslint-prettier-config
Configures ESLint and Prettier for consistent code quality with TypeScript, React, and modern best practices. Use when users request "ESLint setup", "Prettier config", "linting configuration", "code formatting", or "lint rules".
9api-security-hardener
Hardens API security with rate limiting, input validation, authentication, and protection against common attacks. Use when users request "API security", "secure API", "rate limiting", "input validation", or "API protection".
9websocket-realtime-builder
Implements real-time features using WebSockets with Socket.io, rooms, authentication, and reconnection handling. Use when users request "real-time updates", "WebSocket", "Socket.io", "live chat", or "push notifications".
9input-validation-sanitization-auditor
Identifies and fixes XSS, SQL injection, and command injection vulnerabilities with validation schemas, sanitization libraries, and safe coding patterns. Use for "input validation", "XSS prevention", "SQL injection", or "sanitization".
9