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

  1. Throw HttpError subclasses — never return null to signal failure. The error handler catches them.
  2. Log meaningful events ('info' for successful operations, 'error' for caught exceptions).
  3. No req/res — accept plain typed inputs and return plain data.
  4. Composed from repos — a service calls multiple repos when needed; never queries MongoDB directly.
  5. Transactions — use useAtlas().startSession() from codi-node-utils when 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()
  }
}