Node / Express Guide
Services
The service layer — business logic and orchestration.
Services contain all business logic. They coordinate between repositories, third-party clients, and other services. They do not touch req/res.
TL;DR — Factory functions that call repos, throw
HttpError subclasses, use signJwtToken({ payload, secretKey, signOptions }) for JWT creation, and useAtlas.startSession() (static class) for transactions.Pattern
Like controllers, services are factory functions:
// src/services/auth.service.ts
import crypto from "crypto";
import jwt from "jsonwebtoken";
import type { SignOptions } from "jsonwebtoken";
import {
signJwtToken,
comparePasswords,
hashToken,
BadRequestError,
NotFoundError,
useAtlas,
useCache,
} from "@codisolutions23/node-utils";
import {
ACCESS_TOKEN_SECRET,
ACCESS_TOKEN_EXPIRY,
REFRESH_TOKEN_SECRET,
REFRESH_TOKEN_EXPIRY,
} from "../config";
import { log } from "../utils/log.util";
import { useUserRepo } from "../repositories/user.repo";
import { useRevokedTokenRepo } from "../repositories/revoked-token.repo";
import { useTokenRepo } from "../repositories/token.repo";
export function useAuthService() {
const { getUserByEmail, getUserById } = useUserRepo();
const { createRevokedToken, createTTLIndex } = useRevokedTokenRepo();
const { createToken, getByToken, deleteByToken } = useTokenRepo();
const { getCache, setCache, delCache } = useCache();
const resource = "auth.service";
// Both access and refresh are JWTs.
// Access token gets a `jti` (via crypto.randomUUID()) for revocation.
function generateTokens(
userId: string,
organizationId?: string,
role?: string,
) {
const payload = { user: userId, organizationId, role };
const refreshToken = signJwtToken({
payload,
secretKey: REFRESH_TOKEN_SECRET,
signOptions: {
expiresIn: REFRESH_TOKEN_EXPIRY as SignOptions["expiresIn"],
},
});
const accessToken = signJwtToken({
payload,
secretKey: ACCESS_TOKEN_SECRET,
signOptions: {
expiresIn: ACCESS_TOKEN_EXPIRY as SignOptions["expiresIn"],
jwtid: crypto.randomUUID(), // ← jti for revocation
},
});
return { accessToken, refreshToken };
}
async function login({
email,
password,
organizationId,
}: {
email: string;
password: string;
organizationId: string;
}) {
const method = "login";
const user = organizationId
? await getUserByEmail({ email, organizationId })
: await getUserByEmail({ email });
if (!user) throw new NotFoundError("Invalid credentials.");
const isValid = await comparePasswords(password, user.password || "");
if (!isValid) throw new BadRequestError("Invalid credentials.");
const userId = user._id!.toString();
const { accessToken, refreshToken } = generateTokens(
userId,
user.organizationId?.toString(),
user.type,
);
// Store refresh token hash in tokens collection (not on user document)
const tokenHash = hashToken(refreshToken);
await createToken({ type: "refresh", token: tokenHash, user: userId });
log("info", resource, method, "Successful login", { userId, email });
return { accessToken, refreshToken, _id: userId };
}
// Blacklist access token by storing its jti until expiry
async function blacklistAccessToken(
accessToken: string,
userId: string,
method: string,
) {
const decoded = jwt.verify(
accessToken,
ACCESS_TOKEN_SECRET,
) as jwt.JwtPayload;
const jti = decoded.jti;
const exp = decoded.exp;
if (jti && exp) {
const expiresAt = new Date(exp * 1000);
await createTTLIndex();
await createRevokedToken(jti, userId, expiresAt);
}
}
return { login, generateTokens, blacklistAccessToken };
}
signJwtToken Signature
signJwtToken from codi-node-utils takes a single object — not positional arguments:
import { signJwtToken } from "@codisolutions23/node-utils";
const token = signJwtToken({
payload: { user: userId, role: "OWNER" },
secretKey: ACCESS_TOKEN_SECRET,
signOptions: { expiresIn: "15m", jwtid: crypto.randomUUID() },
});
Token Strategy
| Token | Type | Secret | jti | Storage |
|---|---|---|---|---|
| Access | JWT | ACCESS_TOKEN_SECRET | Yes (crypto.randomUUID()) | Not stored — verified on each request |
| Refresh | JWT | REFRESH_TOKEN_SECRET | No | Hashed (hashToken) → tokens collection |
Revocation: On logout, the access token's jti is stored in a revoked_tokens collection with a TTL index that auto-expires matching the token's exp claim.
Rules
- Throw
HttpErrorsubclasses — never returnnullto signal failure. The error handler catches them. - Log meaningful events —
'info'for successful operations,'error'for caught exceptions. - No
req/res— accept plain typed inputs, return plain data. - Composed from repos — a service calls multiple repos when needed; never queries MongoDB directly.
- Transactions — use
useAtlas.startSession()when multiple write operations must be atomic.
useAtlas is a static class — call useAtlas.startSession(), not useAtlas().startSession().Transaction Example
import { useAtlas } from "@codisolutions23/node-utils";
async function createResourceWithRelated(input: TResourceCreate) {
const session = await useAtlas.startSession();
try {
session.startTransaction();
const resourceId = await resourceRepo.add(new Resource(input), session);
await relatedRepo.add(new Related({ resourceId }), session);
await session.commitTransaction();
return resourceId;
} catch (err) {
await session.abortTransaction();
throw err;
} finally {
session.endSession();
}
}
base.add() returns insertedId (an ObjectId), not the full document.Resource Service Example
// src/services/resource.service.ts
import { ConflictError, NotFoundError } from "@codisolutions23/node-utils";
import { useResourceRepo } from "../repositories/resource.repo";
import { Resource } from "../models/resource.model";
import { log } from "../utils/log.util";
import type { TResourceCreate } from "../models/resource.model";
export function useResourceService() {
const repo = useResourceRepo();
const resource = "resource.service";
async function getAll(params: {
organizationId: string;
page?: number;
limit?: number;
}) {
return repo.getResources(params);
}
async function create(input: TResourceCreate) {
const method = "create";
const existing = await repo.getByName(input.name, input.organizationId);
if (existing)
throw new ConflictError("A resource with this name already exists.");
const insertedId = await repo.createResource(input);
log("info", resource, method, "Resource created.", { id: insertedId });
return insertedId;
}
async function update(id: string, input: Partial<TResourceCreate>) {
const method = "update";
const existing = await repo.getById(id);
if (!existing) throw new NotFoundError("Resource not found.");
await repo.updateResource(id, input);
log("info", resource, method, "Resource updated.", { id });
}
async function remove(id: string) {
const method = "remove";
const existing = await repo.getById(id);
if (!existing) throw new NotFoundError("Resource not found.");
await repo.deleteResource(id);
log("info", resource, method, "Resource deleted.", { id });
}
return { getAll, create, update, remove };
}
