Node / Express Guide
Controllers
The factory-function controller pattern for Express route handlers.
Controllers handle HTTP request/response. They validate inputs (Joi), delegate work 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, AuthenticatedRequest } from '../types/common.types'
export function useAuthController() {
const { login: _login, logout: _logout, getCurrentUser: _getCurrentUser } = 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, { abortEarly: false })
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 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)
}
}
async function logout(req: AuthenticatedRequest, res: Response, next: NextFunction) {
const method = 'logout'
try {
await _logout(req.user!.jti)
res.json({ message: 'Logged out successfully.' })
} catch (error: any) {
log('error', resource, method, 'Logout failed.', { error: error.message })
return next(error)
}
}
return { login, getCurrentUser, logout }
}
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 business 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 —'auth.controller','user.controller', etc.
Request Types
| Type | Source | When to use |
|---|---|---|
Request | Express | Unauthenticated routes |
AuthenticatedRequest | src/types/common.types.ts | Routes behind authenticate() — adds .user and .token |
TenantRequest | src/types/common.types.ts | Routes that resolve a tenant — adds .organizationId |
// src/types/common.types.ts
import type { Request } from 'express'
export interface AuthenticatedRequest extends Request {
user?: { _id: string; type: string; jti: string; organizationId?: string }
token?: string
}
export interface TenantRequest extends Request {
organizationId?: string
}
Resource Controller Example
// src/controllers/resource.controller.ts
import { Request, Response, NextFunction } from 'express'
import { createResourceSchema, updateResourceSchema } from '../validations/resource.validation'
import { useResourceService } from '../services/resource.service'
import { UnprocessableEntityError } from '@codisolutions23/node-utils'
import { log } from '../utils/log.util'
import type { AuthenticatedRequest } from '../types/common.types'
export function useResourceController() {
const service = useResourceService()
const resource = 'resource.controller'
async function getAll(req: AuthenticatedRequest, res: Response, next: NextFunction) {
const method = 'getAll'
try {
const { page = 1, limit = 10 } = req.query
const result = await service.getAll({
organizationId: req.user!.organizationId!,
page: Number(page),
limit: Number(limit),
})
res.json(result)
} catch (error: any) {
log('error', resource, method, 'Failed to fetch resources.', { error: error.message })
return next(error)
}
}
async function create(req: AuthenticatedRequest, res: Response, next: NextFunction) {
const method = 'create'
try {
const { error, value } = createResourceSchema.validate(req.body, { abortEarly: false })
if (error) return next(new UnprocessableEntityError(error.message))
const result = await service.create({ ...value, organizationId: req.user!.organizationId })
res.status(201).json(result)
} catch (error: any) {
log('error', resource, method, 'Failed to create resource.', { error: error.message })
return next(error)
}
}
async function update(req: AuthenticatedRequest, res: Response, next: NextFunction) {
const method = 'update'
try {
const { error, value } = updateResourceSchema.validate(req.body, { abortEarly: false })
if (error) return next(new UnprocessableEntityError(error.message))
const result = await service.update(req.params.id, value)
res.json(result)
} catch (error: any) {
log('error', resource, method, 'Failed to update resource.', { error: error.message })
return next(error)
}
}
async function remove(req: Request, res: Response, next: NextFunction) {
const method = 'remove'
try {
await service.remove(req.params.id)
res.json({ message: 'Deleted successfully.' })
} catch (error: any) {
log('error', resource, method, 'Failed to delete resource.', { error: error.message })
return next(error)
}
}
return { getAll, create, update, remove }
}