The Service Layer is the heart of business logic in the application. It handles validation, authorization, business rules, and coordinates between the presentation and persistence layers through well-defined interfaces.
Layer Responsibility: The Service Layer contains all business logic, validation rules, authorization checks, and coordinates data operations. It never handles UI concerns or direct database queries.
Perfect for: Centralizing business rules, ensuring data integrity, implementing security policies, and maintaining clean separation between UI and data layers.
The Service Layer is organized into specialized modules for different concerns:
Service Layer Organization:
src/services/ # 📁 Service Layer Root
├── facades/ # 🎭 Interface Layer
│ ├── user-service-facade.ts
│ ├── subscription-service-facade.ts
│ └── organization-service-facade.ts
├── validation/ # ✅ Input Validation
│ ├── user-validation.ts
│ ├── subscription-validation.ts
│ └── common-validation.ts
├── authorization/ # 🔐 Access Control
│ ├── user-authorization.ts
│ ├── subscription-authorization.ts
│ └── rbac-permissions.ts
├── errors/ # 🚨 Custom Errors
│ ├── validation-error.ts
│ ├── authorization-error.ts
│ └── business-error.ts
├── interceptors/ # 🔄 Cross-cutting Concerns
│ ├── logging-interceptor.ts
│ ├── audit-interceptor.ts
│ └── performance-interceptor.ts
├── helpers/ # 🛠️ Server-side Utilities
│ ├── billing-helper.ts
│ └── email-helper.ts
├── types/ # 📋 Type Definitions
│ ├── domain/ # Domain types
│ └── common-types.ts
├── __tests__/ # 🧪 Service Tests
└── [feature]-service.ts # Core service files
Key Principles:
The validation layer ensures data integrity with Zod schemas and custom validation rules:
Zod Validation Schemas:
// src/services/validation/user-validation.ts
import { z } from 'zod'
// Base schemas
export const userUuidSchema = z.string().uuid("Invalid user ID format")
export const createUserSchema = z.object({
name: z.string()
.min(2, "Name must be at least 2 characters")
.max(100, "Name must be under 100 characters")
.regex(/^[a-zA-Z\s]+$/, "Name can only contain letters and spaces"),
email: z.string()
.email("Invalid email format")
.toLowerCase()
.refine(async (email) => {
// Custom async validation
const existing = await getUserByEmailDao(email)
return !existing
}, "Email already exists"),
password: z.string()
.min(8, "Password must be at least 8 characters")
.regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
"Password must contain uppercase, lowercase, and number"),
organizationId: z.string().uuid().optional(),
metadata: z.record(z.string()).optional()
})
export const updateUserSchema = createUserSchema.partial().extend({
id: userUuidSchema
})
// Type inference
export type CreateUserInput = z.infer<typeof createUserSchema>
export type UpdateUserInput = z.infer<typeof updateUserSchema>Validation Features:
The authorization layer implements Role-Based Access Control (RBAC) using CASL:
Permission Definitions:
// src/services/authorization/permissions.ts
import { AbilityBuilder, createMongoAbility } from '@casl/ability'
export type Actions =
| 'create' | 'read' | 'update' | 'delete'
| 'manage' // Special action that represents all actions
export type Subjects =
| 'User' | 'Subscription' | 'Organization'
| 'all' // Special subject that represents all subjects
export const defineAbilityFor = (user: User) => {
const { can, cannot, build } = new AbilityBuilder(createMongoAbility)
// Super Admin - can do everything
if (user.role === 'SUPER_ADMIN') {
can('manage', 'all')
return build()
}
// Admin - can manage users and organizations
if (user.role === 'ADMIN') {
can('manage', 'User')
can('manage', 'Organization')
can('read', 'Subscription')
return build()
}
// User - can manage own data
if (user.role === 'USER') {
can('read', 'User', { id: user.id })
can('update', 'User', { id: user.id })
can('read', 'Subscription', { userId: user.id })
can('update', 'Subscription', { userId: user.id })
// Organization permissions
if (user.organizationId) {
can('read', 'Organization', { id: user.organizationId })
can('update', 'Organization', { id: user.organizationId })
}
return build()
}
// Guest - read only
can('read', 'User', { id: user.id })
return build()
}Authorization Functions:
// src/services/authorization/user-authorization.ts
import { getCurrentUser } from '@/lib/auth'
import { defineAbilityFor } from './permissions'
export async function canReadUser(userId: string): Promise<boolean> {
const currentUser = await getCurrentUser()
if (!currentUser) return false
const ability = defineAbilityFor(currentUser)
return ability.can('read', 'User', { id: userId })
}
export async function canUpdateUser(userId: string): Promise<boolean> {
const currentUser = await getCurrentUser()
if (!currentUser) return false
const ability = defineAbilityFor(currentUser)
return ability.can('update', 'User', { id: userId })
}
export async function canDeleteUser(userId: string): Promise<boolean> {
const currentUser = await getCurrentUser()
if (!currentUser) return false
const ability = defineAbilityFor(currentUser)
return ability.can('delete', 'User', { id: userId })
}Interceptors handle cross-cutting concerns like logging, auditing, and performance monitoring:
Logging Implementation:
// src/services/interceptors/logging-interceptor.ts
type ServiceFunction = (...args: any[]) => Promise<any>
export function withLogging<T extends ServiceFunction>(
serviceFn: T,
context: string
): T {
return (async (...args: any[]) => {
const startTime = Date.now()
const requestId = generateRequestId()
console.log(`[${context}] Starting operation`, {
requestId,
args: sanitizeArgs(args),
timestamp: new Date().toISOString()
})
try {
const result = await serviceFn(...args)
console.log(`[${context}] Operation completed`, {
requestId,
duration: Date.now() - startTime,
success: true
})
return result
} catch (error) {
console.error(`[${context}] Operation failed`, {
requestId,
duration: Date.now() - startTime,
error: error.message,
stack: error.stack
})
throw error
}
}) as T
}
// Usage
export const getUserByIdService = withLogging(
async (id: string) => {
// Service implementation
},
'getUserByIdService'
)Structured Logging:
interface LogEntry {
timestamp: string
level: 'info' | 'warn' | 'error'
service: string
operation: string
requestId: string
userId?: string
duration?: number
error?: string
metadata?: Record<string, any>
}
export class Logger {
static info(message: string, metadata: Partial<LogEntry>) {
const entry: LogEntry = {
timestamp: new Date().toISOString(),
level: 'info',
service: metadata.service || 'unknown',
operation: metadata.operation || 'unknown',
requestId: metadata.requestId || generateRequestId(),
...metadata
}
console.log(JSON.stringify(entry))
}
}Service testing focuses on business logic, validation, and authorization:
Test Structure:
// src/services/__tests__/user-service.test.ts
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { getUserByIdService, updateUserService } from '../user-service'
// Mock dependencies
vi.mock('../authorization/user-authorization', () => ({
canReadUser: vi.fn(),
canUpdateUser: vi.fn()
}))
vi.mock('../../db/repositories/user-repository', () => ({
getUserByIdDao: vi.fn(),
updateUserDao: vi.fn()
}))
describe('User Service', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('getUserByIdService', () => {
it('should return user when authorized', async () => {
// Arrange
const userId = 'test-user-id'
const mockUser = { id: userId, name: 'Test User' }
vi.mocked(canReadUser).mockResolvedValue(true)
vi.mocked(getUserByIdDao).mockResolvedValue(mockUser)
// Act
const result = await getUserByIdService(userId)
// Assert
expect(canReadUser).toHaveBeenCalledWith(userId)
expect(getUserByIdDao).toHaveBeenCalledWith(userId)
expect(result).toEqual(mockUser)
})
it('should throw AuthorizationError when not authorized', async () => {
// Arrange
vi.mocked(canReadUser).mockResolvedValue(false)
// Act & Assert
await expect(getUserByIdService('test-id'))
.rejects
.toThrow('AuthorizationError')
expect(getUserByIdDao).not.toHaveBeenCalled()
})
it('should throw ValidationError for invalid ID', async () => {
// Act & Assert
await expect(getUserByIdService('invalid-id'))
.rejects
.toThrow('ValidationError')
})
})
})Test Coverage Areas:
Service layer ready! Your business logic is secure, validated, authorized, and thoroughly tested with clean separation of concerns and enterprise-grade patterns.