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.
TL;DR — Factory function per domain, validate with Joi
{ convert: true }, delegate to service, catch errors → next(error). Never query DB or build responses for failures.Pattern
All controllers are factory functions — no classes:
// src/controllers/auth.controller.ts
import { NextFunction, Request, Response } from "express";
import {
AuthenticatedRequest,
NotFoundError,
UnauthorizedError,
UnprocessableEntityError,
} from "@codisolutions23/node-utils";
import { log } from "../utils/log.util";
import { useAuthService } from "../services/auth.service";
import { loginSchema } from "../validations/auth.validation";
import { commonSchemas } from "../validations/local.validation";
import { TenantRequest } from "../middleware/tenant.middleware";
export function useAuthController() {
const {
login: _login,
refreshToken: _refreshToken,
getCurrentUser: _getCurrentUser,
logout: _logout,
} = useAuthService();
const resource = "auth.controller";
async function login(req: TenantRequest, res: Response, next: NextFunction) {
const method = "login";
try {
const validationData = {
...req.body,
organizationId: req.organizationId,
};
const { error, value } = loginSchema.validate(validationData, {
convert: true,
});
if (error) {
log("error", resource, method, "Validation error", {
error: error.message,
});
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,
requestBody: req.body,
requestOrganizationId: req.organizationId,
});
return next(error);
}
}
async function getCurrentUser(
req: AuthenticatedRequest,
res: Response,
next: NextFunction,
) {
const method = "getCurrentUser";
const user = req.user as { id?: string };
try {
const userId = user?.id;
if (!userId) {
return next(
new UnauthorizedError(
"Invalid user session. Please log in to continue.",
),
);
}
const result = await _getCurrentUser(userId);
res.json(result);
} catch (error: any) {
log("error", resource, method, "Failed to get user.", {
error: error.message,
});
return next(error);
}
}
return { login, getCurrentUser };
}
Rules
- Validate first using Joi with
{ convert: true }. Returnnext(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 | @codisolutions23/node-utils | Routes behind authenticate() — adds .user (with .id, .role, .jti) and .token |
TenantRequest | src/middleware/tenant.middleware.ts | Routes that resolve a tenant — adds .organizationId and .slug |
AuthenticatedRequest uses .user.id (not ._id) and .user.role (not .type). These match the JWT payload shape set by the auth middleware in codi-node-utils.src/types/common.types.ts contains MongoQuery<T> and PaginatedResult<T> — not request types. Request types come from the sources listed above.Resource Controller Example
// src/controllers/resource.controller.ts
import { NextFunction, Request, Response } from "express";
import {
createResourceSchema,
updateResourceSchema,
} from "../validations/resource.validation";
import { useResourceService } from "../services/resource.service";
import {
AuthenticatedRequest,
UnprocessableEntityError,
} from "@codisolutions23/node-utils";
import { log } from "../utils/log.util";
export function useResourceController() {
const service = useResourceService();
const resource = "resource.controller";
async function getAll(
req: AuthenticatedRequest,
res: Response,
next: NextFunction,
) {
const method = "getAll";
try {
const user = req.user as { id: string; organizationId?: string };
const { page = 1, limit = 10 } = req.query;
const result = await service.getAll({
organizationId: 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, {
convert: true,
});
if (error) return next(new UnprocessableEntityError(error.message));
const user = req.user as { organizationId?: string };
const result = await service.create({
...value,
organizationId: 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, {
convert: true,
});
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 };
}
