Backend

Controllers

The factory-function controller pattern used in booki-api.

Controllers handle HTTP request/response. They validate inputs (Joi), delegate to a service, and return JSON. They never contain business logic.

Pattern

All controllers are factory functions — no classes:

// src/controllers/auth.controller.ts
import { Request, Response, NextFunction } from 'express'
import { loginSchema } from '../validations/auth.validation'
import { useAuthService } from '../services/auth.service'
import { UnprocessableEntityError } from '@codisolutions23/node-utils'
import { log } from '../utils/log.util'
import type { TenantRequest } from '../types/common.types'

export function useAuthController() {
  const { login: _login, refreshToken: _refreshToken, logout: _logout } = useAuthService()
  const resource = 'auth.controller'

  async function login(req: TenantRequest, res: Response, next: NextFunction) {
    const method = 'login'
    try {
      const { error, value } = loginSchema.validate({
        ...req.body,
        organizationId: req.organizationId,
      })
      if (error) return next(new UnprocessableEntityError(error.message))

      const session = await _login(value)
      res.json(session)
    } catch (error: any) {
      log('error', resource, method, 'Login failed.', { error: error.message })
      return next(error)
    }
  }

  async function refreshToken(req: Request, res: Response, next: NextFunction) {
    const method = 'refreshToken'
    try {
      const { error, value } = refreshTokenSchema.validate(req.body)
      if (error) return next(new UnprocessableEntityError(error.message))

      const result = await _refreshToken(value.refreshToken)
      res.json(result)
    } catch (error: any) {
      log('error', resource, method, 'Token refresh failed.', { error: error.message })
      return next(error)
    }
  }

  async function getCurrentUser(req: AuthenticatedRequest, res: Response, next: NextFunction) {
    const method = 'getCurrentUser'
    try {
      const user = await _getCurrentUser(req.user!._id.toString())
      res.json(user)
    } catch (error: any) {
      log('error', resource, method, 'Failed to get user.', { error: error.message })
      return next(error)
    }
  }

  return { login, refreshToken, getCurrentUser }
}

Rules

  1. Validate first using Joi. Return next(new UnprocessableEntityError(...)) on failure — never throw directly.
  2. Delegate everything to a service. Controllers must not query the DB or contain conditional logic.
  3. return next(error) on catch — not res.status(500).json(...). The global errorHandler middleware handles the response.
  4. Log every caught error with log('error', resource, method, message, meta).
  5. resource constant at the top of every controller file — 'auth.controller', 'booking.controller', etc.

Request Types

TypeSourceWhen to use
RequestExpressUnauthenticated routes
AuthenticatedRequestcodi-node-utilsRoutes behind authenticate() middleware — adds .user and .token
TenantRequestsrc/types/common.types.tsRoutes that resolve a tenant — adds .organizationId