# Backend Architecture

The backend uses a three-layer architecture, mirroring the frontend
(`frontend/docs/frontend-architecture.md`):

- API layer (HTTP)
- Business logic layer (BLL)
- Data access layer (DAL)

The goal is to keep routes thin, keep business rules testable and free of HTTP,
and keep all persistence in one place.

## Layer 1: API

Location:

- `src/routes/` — thin route wiring: `path → middleware → wrapAsync(controller)`.
- `src/api/controllers/` — one `<feature>.controller.ts` per feature; exported
  async handler functions `(req, res) => …`.
- `src/api/http/` — request helpers (`wrapAsync`, `queryStr`, `queryNum`,
  `paramStr`).
- `src/middlewares/` — `authenticate` (passport), `checkPermissions`,
  `csrf-origin`, `error-handler`, `upload`.

Responsibilities:

- Parse and validate the HTTP request (query, params, body, cookies, uploads).
- Run middleware (auth, permissions, CSRF).
- Call exactly one BLL service and shape the HTTP response (status + body).
- Own multipart/upload parsing; pass parsed data (e.g. a file buffer) to the BLL.

The API layer must not:

- Import the DAL (`@/db/api/*`, `@/db/models/*`) — it goes through a service.
- Contain tenant/role/permission/workflow rules or DTO mapping.
- Run database queries.

## Layer 2: Business Logic (BLL)

Location:

- `src/services/` — one `<feature>.ts` (class with static methods) per feature,
  plus per-feature mappers/validators/helpers as needed. Infra BLL lives in
  `src/services/email/`.

Responsibilities:

- Own workflows, transactions, and coordination across repositories.
- Apply tenant, role, campus, and permission rules.
- Map DB records to response DTOs; validate and normalize inputs.
- Accept typed inputs and return typed values/DTOs.

The BLL must not:

- Touch Express `req`/`res` or import `express`/middleware. (Two legacy
  exceptions remain — `services/file.ts` streaming and `services/auth.ts` session
  IP/UA + cookies — tracked by the boundary test and to be revisited.)
- Import the API layer.
- Render HTTP responses.

## Layer 3: Data Access (DAL)

Location:

- `src/db/api/` — one `*DBApi` class per entity (the repository layer).
- `src/db/models/` — Sequelize models.
- `src/db/migrations/`, `src/db/seeders/`, `src/db/utils.ts`, `db.config.ts`.
- `src/db/api/types.ts` — DB-entity contract types (`AuthenticatedUser`,
  `CurrentUser`, `DbApiOptions`, …); DAL-coupled, so it stays in `db/`.

Responsibilities:

- Own all Sequelize queries and schema.
- Return records/plain data to the BLL.

The DAL must not:

- Import the API layer or the BLL. (One legacy exception: `db/api/file.ts`
  imports `services/file` for GCloud blob deletion — tracked, to be inverted.)
- Apply business rules or touch HTTP.

## Cross-cutting

Location: `src/shared/` (+ ambient types in `src/types/`).

- `shared/constants/` — all constants/config values (was `src/constants`).
- `shared/config/` — env-driven runtime config (`index.ts` + `load-env.ts`).
- `shared/errors/` — `AppError` and subclasses.
- `shared/notifications/` — i18n message catalog + helpers.
- `shared/logger.ts`, `shared/csv.ts`, `shared/jwt.ts`.
- `shared/architecture/` — the import-boundary test.

Cross-cutting code depends on no layer and may be imported by any layer.

## Import direction

Allowed:

```text
Route → Controller → Service (BLL) → Repository/Model (DAL) → DB
```

`shared/*` may be imported by any layer. Disallowed:

```text
API (routes/controllers) → DAL          (skip the BLL)
BLL (services)           → Express / API
DAL (db)                 → BLL / API
shared/*                 → any layer
```

## Feature structure

Layer-first directories, one file per feature inside each layer (only create what
a feature needs):

```text
src/routes/<feature>.ts
src/api/controllers/<feature>.controller.ts
src/services/<feature>.ts            (+ mappers/validators when needed)
src/db/api/<feature>.ts              (repository)
src/db/models/<feature>.ts
src/shared/constants/<feature>.ts
```

## Module authoring (shared factories & helpers)

Most modules are assembled from shared factories/helpers — keep them that way.

- **Generic CRUD entity** = three one-line config files:
  - `src/services/<e>.ts` → `export default createCrudService(EntityDBApi, { notFoundCode });`
  - `src/api/controllers/<e>.controller.ts` → `export default createCrudController(service, { csvFields });`
  - `src/routes/<e>.ts` → `export default createCrudRouter(controller, { permission });`

  Factories: `services/shared/crud-service.ts`,
  `api/controllers/shared/crud-controller.ts`, `api/http/crud-router.ts` (generic
  over the repository's entity types — no casts). 23 of 26 entities use them;
  entities with genuinely different behavior (`users` invitations, `documents`
  DTO responses, `permissions` no-`globalAccess` queries) stay hand-written.
- **Repository (DAL)** = entity-specific `create`/`update`/`bulkImport`/`findBy`/
  `findAll`; the identical `remove`/`deleteByIds`/`findAllAutocomplete` delegate to
  `db/api/shared/repository.ts` (`removeRecord`, `deleteRecordsByIds`,
  `autocompleteByField`).
- **Feature service (BLL)** = reuse shared helpers: tenant/role access in
  `services/shared/access.ts` (`getOrganizationId`, `getOrganizationIdOrGlobal`,
  `hasGlobalAccess`, `requireUserId`, `hasRoleAccess(user, roleNames)`,
  `campusScope(user, tenantWideRoleNames)`, `assertAuthenticatedTenantUser`, …);
  validation in `services/shared/validate.ts` (`clampLimit`, `nullableString`,
  `requiredIsoDate`/`optionalIsoDate`); transactions via `db/with-transaction.ts`
  (`withTransaction(fn)`); CSV import via `services/shared/csv-import.ts`;
  `isRecord` from `shared/object.ts`.
  - `getOrganizationIdOrGlobal(user)`: returns `null` for global access users
    (bypassing org filter) or the user's org ID; throws `ForbiddenError` if neither.
  - `hasGlobalAccess(user)`: returns `true` when `app_role.globalAccess === true`.
  - `assertAuthenticatedTenantUser(user)`: allows global access users even without
    an organization (useful for platform-level admins).

## Error handling

Centralized — see `backend/docs/error-handling.md`. Handlers/services throw an
`AppError` subclass; the terminal `error-handler` middleware turns it into the
`{ message, code?, details? }` JSON body the frontend `ApiError` consumes.

## Enforcement & verification

- `src/shared/architecture/import-boundaries.test.ts` enforces the import
  direction. Hard invariants assert zero violations; the two remaining HTTP-in-BLL
  edge cases and the one DAL→BLL leak are capped by ceilings that must not grow.
- ESLint `no-restricted-imports` blocks (in `eslint.config.ts`) forbid the
  already-clean invariants at lint time (API→DAL, model/DAL/shared purity).
- `npm run typecheck`, `npm run lint`, `npm test` are the verification gates;
  `npm test` runs the Node test runner via `tsx` (error-handler + boundary tests).

## Known remaining items

- `services/file.ts` and `services/auth.ts` still depend on `req`/`res` (file
  streaming; session IP/UA + cookies). To be revisited with the upload subsystem.
- `db/api/file.ts` → `services/file` (GCloud delete) is a DAL→BLL leak to invert
  (the BLL should orchestrate blob + row deletion).
- `src/index.ts` remains the composition root + entry; an `app/server.ts` split is
  optional and deferred (deploy runs `dist/index.js`).
- Repositories still hand-roll the `findAll` filter→`where` building per entity; a
  declarative where-builder could dedup it, deferred until the data platform
  stabilizes (higher-risk Sequelize typing).
