CODEX // oaoisme wiki

§ unswayed-backend

API contract & docs

updated 2026-06-02

How the HTTP surface is shaped, why it looks the way it does, and where the always-current reference lives. This is the contract every client (and the frontend) sees.

The reference is read-only

endpoint_documentation.md in the repo is the legacy Laravel contract the mobile/web frontend was built against. We treat it as a read-only reference: the NestJS backend is reconciled to it, but we never edit it. The living, machine-checked version of the contract is the OpenAPI document the server generates at /docs (Swagger UI) and /docs-json (raw OpenAPI 3).

Four rules that hold for every endpoint

  1. Everything is under /api. POST /api/applicant/register, GET /api/auth/social/providers, and so on. The lone exception is the root GET / health ping, which stays at / for uptime checks.
  2. Bodies and payloads are snake_case. last_name, password_confirmation, zip_code, company_name, current_password, refresh_token; the user object returns user_type, is_verified, is_completed, profile_photo, created_at. This mirrors the legacy API exactly, so the frontend is a drop-in.
  3. One envelope. Success is always { status: "success", message, data }; failure is always { status: "error", message, errors? }. Validation failures are HTTP 422 with errors keyed by field name.
  4. Bearer auth (Authorization: Bearer <access token>) guards the authenticated routes; Swagger marks exactly which ones.

How the /api prefix works

A single helper, configureApp(app) in src/configure-app.ts, calls app.setGlobalPrefix('api', { exclude: [GET /] }). It is invoked from two places: main.ts (production) and every e2e suite's beforeAll. That shared call is deliberate — it means the supertest end-to-end tests hit /api/... exactly like the internet does, so the prefix can never silently regress.

How snake_case stays clean inside

The wire is snake_case, but the internal TypeScript stays idiomatic camelCase. The boundary is the DTO: request DTOs declare snake_case fields (last_name), and the controller/service maps them into camelCase internal inputs ({ lastName: dto.last_name }) before they touch persistence. Responses go the other way — toUserResource() projects a Prisma User into the snake_case shape. So only the edge speaks snake_case; the core never has to.

Gotcha that motivated all of this: the global ValidationPipe runs with whitelist: true, which strips any property not declared on the DTO. When the DTOs were camelCase, an incoming snake_case last_name was silently dropped and registration failed validation with a confusing "lastName required". Matching the DTO field names to the wire names is what makes the body bind at all.

The one status-code subtlety: 206

social-login returns 206 Partial Content when the signed-in user's profile is not yet complete (a brand-new social account), and 200 when it is. The controller sets this with a passthrough response — res.status(data.is_completed ? 200 : 206) — while still returning the normal envelope. A client that sees 206 knows to send the user through complete-profile next.

How the docs are generated

main.ts builds the OpenAPI document with DocumentBuilder: it declares the /api server, the Authentication and Social Auth tags, and bearer auth. Each controller route carries @ApiOperation + @ApiResponse decorators that wrap a shared successEnvelope()/errorEnvelope schema helper (src/common/swagger/), and every DTO field has an @ApiProperty example — so the "Try it out" panel is pre-filled with a valid snake_case body.

Gotcha: because the global prefix is set, SwaggerModule.createDocument would also fold /api into every path, and combined with the declared /api server you'd get a doubled /api/api/.... The fix is createDocument(app, config, { ignoreGlobalPrefix: true }): operation paths stay relative (/applicant/register) and the /api server supplies the base — exactly mirroring the reference doc's structure (base URL + relative paths).

Deliberate deviations from legacy (kept on purpose)

The reconciliation is not a blind copy — it keeps the security improvements made earlier, and documents them rather than reverting:

  • Rotating refresh tokens + POST /refresh and refresh_token in the register/login envelope (legacy had opaque, non-rotating Passport tokens).
  • Anti-enumerationsend-reset-otp always 200, login always 401, so the API never reveals which emails exist (legacy leaked existence via 404).
  • logout takes a refresh_token body to revoke the specific rotating token.
  • Social providers scoped to google + linkedin; the other legacy providers return 422/501 until built.

Documented but not built yet (known gaps)

The reconciliation covered the implemented surface (auth + social). These doc'd endpoints are intentionally not built yet and are tracked in docs/API-CONTRACT.md: POST /correct-email, GET /employerProfile/{id}, register's optional educations/skills arrays, and every non-auth section (master data, blog, posts, jobs, chat, settings, subscriptions, …). They land with their feature modules.