Backend
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.
Pattern
Like controllers, services are factory functions:
// src/services/auth.service.ts
import { useUserRepo } from '../repositories/user.repo'
import { useRevokedTokenRepo } from '../repositories/revoked-token.repo'
import { signJwtToken, comparePasswords, hashToken } from '@codisolutions23/node-utils'
import { NotFoundError, UnauthorizedError } from '@codisolutions23/node-utils'
import { accessTokenSecret, refreshTokenSecret } from '../config'
import { log } from '../utils/log.util'
import type { TLoginInput } from '../validations/auth.validation'
export function useAuthService() {
const userRepo = useUserRepo()
const revokedTokenRepo = useRevokedTokenRepo()
const resource = 'auth.service'
async function login(input: TLoginInput) {
const method = 'login'
const user = await userRepo.getUserByEmail(input.email, input.organizationId)
if (!user) throw new NotFoundError('User not found.')
const passwordMatch = await comparePasswords(input.password, user.password!)
if (!passwordMatch) throw new UnauthorizedError('Invalid credentials.')
const accessToken = signJwtToken(
{ _id: user._id, type: user.type, organizationId: user.organizationId },
accessTokenSecret,
'15m',
)
const rawRefresh = crypto.randomUUID()
const refreshToken = hashToken(rawRefresh) // stored as hash
await userRepo.update(user._id!.toString(), {
refreshToken,
updatedAt: new Date(),
})
log('info', resource, method, 'User logged in.', { userId: user._id })
return { accessToken, refreshToken: rawRefresh, _id: user._id }
}
async function logout(jti: string) {
await revokedTokenRepo.add({ jti, createdAt: new Date() })
}
return { login, logout }
}
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 and return plain data. - Composed from repos — a service calls multiple repos when needed; never queries MongoDB directly.
- Transactions — use
useAtlas().startSession()fromcodi-node-utilswhen multiple write operations must be atomic.
// Atomic multi-collection write
import { useAtlas } from '@codisolutions23/node-utils'
async function createBookingWithPayment(input: TBookingCreate) {
const session = await useAtlas().startSession()
try {
session.startTransaction()
const booking = await bookingRepo.add(new Booking(input), session)
const payment = await paymentRepo.add(new Payment({ bookingId: booking._id }), session)
await session.commitTransaction()
return { booking, payment }
} catch (err) {
await session.abortTransaction()
throw err
} finally {
session.endSession()
}
}