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.

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,
  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, return plain data.
  4. Composed from repos — a service calls multiple repos when needed; never queries MongoDB directly.
  5. Transactions — use useAtlas().startSession() when multiple write operations must be atomic.

Transaction Example

import { useAtlas } from '@codisolutions23/node-utils'

async function createResourceWithRelated(input: TResourceCreate) {
  const session = await useAtlas().startSession()
  try {
    session.startTransaction()
    const resource = await resourceRepo.add(new Resource(input), session)
    const related  = await relatedRepo.add(new Related({ resourceId: resource._id }), session)
    await session.commitTransaction()
    return { resource, related }
  } catch (err) {
    await session.abortTransaction()
    throw err
  } finally {
    session.endSession()
  }
}

Resource Service Example

// src/services/resource.service.ts
import { useResourceRepo }       from '../repositories/resource.repo'
import { ConflictError, NotFoundError } from '@codisolutions23/node-utils'
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 result = await repo.createResource(input)
    log('info', resource, method, 'Resource created.', { id: result._id })
    return result
  }

  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.')

    const result = await repo.updateResource(id, input)
    log('info', resource, method, 'Resource updated.', { id })
    return result
  }

  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 }
}