Service Layer

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.

Service Layer Architecture

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:
  • Single Responsibility - Each service handles one domain
  • Dependency Injection - Services depend on abstractions
  • Layered Validation - Input validation before business logic
  • Authorization First - Permission checks before operations

Validation Layer

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:
  • Type Safety - Automatic TypeScript types
  • Custom Rules - Business-specific validation
  • Async Validation - Database checks, API calls
  • Internationalization - Localized error messages

Authorization Layer

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 and Cross-Cutting Concerns

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))
  }
}

Testing Services

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:
  • Validation - Test all validation scenarios
  • Authorization - Test permitted and denied access
  • Business Logic - Test core functionality
  • Error Handling - Test error cases
  • Side Effects - Test notifications, emails, etc.

Best Practices

Validation Best Practices

Validate early - At service boundary
Use Zod schemas - Type-safe validation
Async validation - Database constraints
Localized messages - User-friendly errors
Sanitize inputs - Transform data consistently
🔐

Authorization Best Practices

Check permissions first - Before business logic
Use CASL abilities - Declarative permissions
Resource-level checks - Fine-grained access
Fail secure - Deny by default
Audit access - Log permission checks
🏗️

Service Design

Single responsibility - One concern per service
Pure functions - Predictable, testable
Dependency injection - Mock-friendly
Error handling - Custom error types
Transaction support - Data consistency
🧪

Testing Strategy

Unit tests - Service logic isolation
Integration tests - End-to-end flows
Mock dependencies - Repository and external services
Test authorization - Both success and failure cases
Performance tests - Response time validation
Service layer ready! Your business logic is secure, validated, authorized, and thoroughly tested with clean separation of concerns and enterprise-grade patterns.
    Service Layer | ShipSaaS Documentation | ShipSaaS - Launch your SaaS with AI in days