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
- Validate first using Joi. Return
next(new UnprocessableEntityError(...))on failure — never throw directly. - Delegate everything to a service. Controllers must not query the DB or contain conditional logic.
return next(error)on catch — notres.status(500).json(...). The globalerrorHandlermiddleware handles the response.- Log every caught error with
log('error', resource, method, message, meta). resourceconstant at the top of every controller file —'auth.controller','booking.controller', etc.
Request Types
| Type | Source | When to use |
|---|---|---|
Request | Express | Unauthenticated routes |
AuthenticatedRequest | codi-node-utils | Routes behind authenticate() middleware — adds .user and .token |
TenantRequest | src/types/common.types.ts | Routes that resolve a tenant — adds .organizationId |