API Design & Error Conventions¶
This guideline is the canonical reference for how HTTP APIs are shaped in the
mr-mentor-backend service: URL prefixing and resource naming, the auth header
contract, the JSON response envelope, status-code usage, pagination/filtering,
and how errors propagate from a controller catch (or a guard middleware) all
the way back to the client. It is descriptive — it documents the conventions the
codebase actually follows today (which are loose and manually enforced, not
schema-validated), and flags where they diverge so future code can converge.
Status: documented from source on this branch.
Overview¶
mr-mentor-backend is a single Express + TypeScript monolith that fronts the
entire MAS / Mr. Mentor product suite (mentorship, recruitment/Mr. Hire, LMS,
Sales CRM, Finance, AI platform, vendor API). Every product feature is exposed
as REST-ish JSON over HTTP, plus a Socket.IO layer for real-time meetings (out
of scope for this doc — see request-lifecycle-and-middleware).
There is no API framework abstraction on top of Express: no
express-validator, no zod/Joi/class-validator, no express-async-errors,
no central response serializer. Conventions are upheld by copy-paste discipline
across ~70 route files and ~42 controllers. This document captures that de-facto
contract so it can be applied consistently.
Consumers (personas) of the API:
| Tier | Consumer | Auth | Mount examples |
|---|---|---|---|
| Public | Anonymous browsers, marketing site, signup, quiz takers | none (or a token is the resource) | /api/health, /api/auth/*, /api/courses (public listing), /api/candidate-quiz/*, /api/judge0/* |
| Authenticated user | Logged-in students, mentors/experts, sales, CMs | Authorization: Bearer <JWT> (or cookie) |
/api/users/profile, /api/tokens/*, /api/student/* |
| Admin | ADMIN / SUPERADMIN dashboard | JWT + adminMiddleware |
/api/admin/* |
| Finance | FINANCE (or ADMIN) | JWT + financeOnlyMiddleware |
/api/finance/* |
| Vendor (machine) | External partners ingesting leads | X-API-Key: vk_live_xxx.yyy (or Bearer key) |
/api/v1/vendor/* |
| Webhook (machine) | Exotel telephony callbacks | shared-secret ?token= query param |
/api/exotel/* |
Key concepts & entities¶
| Term | Meaning |
|---|---|
| Response envelope | The JSON object shape returned by most handlers: { success, message, data, error } (not all fields always present). |
| Mount prefix | The path Express prepends when a route group is mounted in src/routes/index.ts (e.g. /api, /api/admin, /api/v1/vendor). The path inside a route file is relative to its mount prefix. |
| Guard middleware | A middleware that short-circuits with 401/403 before the controller runs (authMiddleware, adminMiddleware, etc.). |
x-platform header |
Multi-tenant routing hint. Values seen: mr-mentor, my-analytics-school, mr-hire. Defaults to my-analytics-school when absent. |
AuthenticatedRequest |
Request extended with req.user: User (the full DB row, not just the JWT claims). |
| Vendor key | An API key shaped vk_live_<prefix>.<secret> stored hashed; carries canSubmit / canRead permissions. |
Entities relevant to the API/error contract (paths under src/):
entities/User.ts— the principal attached asreq.user; carriesrole,isActive,isVerified,passwordChangedAt,salesHeadId.entities/RefreshToken.ts— session record; the JWTsidclaim references this for per-session revoke.entities/VendorApiKey.ts+entities/VendorApiKeyAccessSummary.ts— vendor auth + per-tuple access logging.types/UserTypes.ts—UserRoleenum (USER,ADMIN,SUPERADMIN,EXPERT,SALES,FINANCE, …).
Source of truth for conventions:
src/app.ts— CORS, body limits, the 404 handler, and the global error handler.src/routes/index.ts— every mount prefix.src/middleware/auth.middleware.ts,admin.middleware.ts,finance.middleware.ts,expert.middleware.ts,vendorAuth.middleware.ts,totpRateLimit.middleware.ts.
Architecture¶
flowchart TD
Client["Client (browser / vendor / webhook)"] -->|HTTP request| CORS["CORS + Helmet (app.ts)"]
CORS --> BODY["express.json 20mb limit"]
BODY --> ROUTER["Root Router (routes/index.ts)"]
ROUTER -->|"prefix /api"| PUB["Public + user routes"]
ROUTER -->|"prefix /api/admin"| ADM["Admin routes"]
ROUTER -->|"prefix /api/finance"| FIN["Finance routes"]
ROUTER -->|"prefix /api/v1/vendor"| VEN["Vendor routes"]
ROUTER -->|"prefix /api/exotel"| WH["Webhook routes"]
PUB --> GUARD{"Guard middleware?"}
ADM --> GUARD
FIN --> GUARD
VEN --> GUARD
WH --> GUARD
GUARD -->|"401 / 403 short-circuit"| ERRJSON["JSON error response"]
GUARD -->|pass| CTRL["Controller method (try/catch)"]
CTRL --> SVC["Service layer"]
SVC --> DB[("PostgreSQL via TypeORM")]
SVC --> REDIS[("Redis / BullMQ")]
SVC --> EXT["External APIs (Razorpay, S3, Exotel, LLM)"]
CTRL -->|"res.status.json success envelope"| OK["2xx JSON"]
CTRL -->|"caught error res.status.json"| ERRJSON
CTRL -->|"uncaught throw (rare)"| GEH["Global error handler (app.ts) 500"]
GEH --> ERRJSON
ROUTER -->|"no route matched"| NF["404 handler (app.ts)"]
NF --> ERRJSON
The dominant pattern is per-controller try/catch: nearly every handler
catches its own errors and emits an envelope itself. The Express global error
handler in src/app.ts is a backstop for uncaught synchronous throws — it is
rarely reached because handlers are async and Express 4 does not forward
rejected promises to the error handler automatically (there is no
express-async-errors). An unhandled rejection inside an async handler that
does not try/catch would hang the request rather than reach the global
handler — this is the single most important gotcha (see Edge cases).
Data model¶
The API-contract concern owns very few entities; it mostly reads User and the
auth/session/vendor records. This ER diagram shows the entities that shape
auth and the request contract.
erDiagram
USER ||--o{ REFRESH_TOKEN : "has sessions"
VENDOR_API_KEY ||--o{ VENDOR_API_KEY_ACCESS_SUMMARY : "logs"
USER {
uuid id PK
string email
string role
boolean isActive
boolean isVerified
datetime passwordChangedAt
uuid salesHeadId FK
}
REFRESH_TOKEN {
uuid id PK
string token
uuid userId FK
boolean revoked
string replacedByToken
datetime expiresAt
}
VENDOR_API_KEY {
uuid id PK
string keyPrefix UK
string keyHash
string status
boolean canSubmit
boolean canRead
datetime lastUsedAt
int requestCount
}
VENDOR_API_KEY_ACCESS_SUMMARY {
uuid id PK
uuid vendorApiKeyId FK
string ip
string method
string path
int hitCount
int lastStatusCode
}
API surface¶
This domain doc does not enumerate every product endpoint (see each feature
doc). Instead it documents the contract-defining routes and the mount
prefixes. All paths below are the full path (mount prefix + route-file path),
derived from src/routes/index.ts and the cited route files.
| Method | Path | Auth/role | Purpose |
|---|---|---|---|
| GET | / |
public | Banner JSON describing a few endpoints (src/app.ts). |
| GET | /api/health |
public | Liveness/health check (routes/health.routes.ts). |
| GET | /api/test-data |
public | Debug DB connectivity probe (defined inline in routes/index.ts). |
| POST | /api/auth/signup |
public | Register; sends OTP async (routes/auth.routes.ts). |
| POST | /api/auth/verify-otp |
public | Verify OTP, issue JWT + refresh cookies. |
| POST | /api/auth/login |
public | Login, issue tokens. |
| POST | /api/auth/refresh |
public (refresh cookie) | Rotate access token. |
| POST | /api/auth/logout |
Bearer JWT | Revoke current session. |
| POST | /api/auth/master-totp |
public + totpRateLimit |
TOTP master-auth, 5/15min per IP. |
| GET | /api/users/profile |
Bearer JWT | Current user profile (envelope data). |
| GET | /api/users |
Bearer JWT (admin-ish) | Paginated user list (page,limit,total). |
| POST | /api/users |
varies | Create user; 409 on duplicate email. |
| GET | /api/admin/... |
JWT + adminMiddleware |
All admin operations (routes/admin.routes.ts + others mounted at /api/admin). |
| GET | /api/finance/overview |
JWT + financeOnlyMiddleware |
Finance portal (FINANCE or ADMIN). |
| POST | /api/v1/vendor/leads |
X-API-Key + submit perm |
External vendor lead ingestion. |
| GET | /api/v1/vendor/leads |
X-API-Key + read perm |
List vendor's leads. |
| GET | /api/v1/vendor/leads/:id |
X-API-Key + read perm |
Fetch one vendor lead. |
| POST | /api/exotel/call-status |
shared-secret ?token= |
Exotel telephony status webhook. |
| POST | /api/exotel/sms-status |
shared-secret ?token= |
Exotel SMS status webhook. |
| ALL | /api/candidate-quiz/* |
public (token-is-auth) | Candidate quiz — the invite token authorizes. |
| ALL | /api/judge0/* |
public | Judge0 code-exec proxy, called from quiz pages. |
| * | * (unmatched) |
— | 404 { success:false, message:"Route not found", path }. |
Note:
/api/v1/vendoris the only version-prefixed namespace in the codebase. Everything else is unversioned under/api. Admin endpoints are not a separate router so much as a separate mount prefix (/api/admin) — many different route classes are mounted there (adminRoutes,jobApplicationRoutes,resumeAnalysisRoutes,allResponsesRoutes,aiScreeningConfigRoutes,new-courses,vendor-keys,applicationMeetingAdminRoutes).
Request shape¶
A typical authenticated mutating request:
POST /api/tokens/purchase HTTP/1.1
Host: api.mrmentor.in
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
X-Platform: mr-mentor
{ "amount": 5, "couponCode": "WELCOME" }
- Auth header:
Authorization: Bearer <JWT>. The token may also arrive as atokenorauthTokencookie —authMiddlewarechecks the header first, then those cookies (src/middleware/auth.middleware.ts). - Tenant header:
X-Platformselects the product tenant. Read per-handler; unknown/missing values fall back tomy-analytics-school. - Vendor auth:
X-API-Key: vk_live_<prefix>.<secret>orAuthorization: Bearer <vendorKey>. - Body: JSON, up to
20mb(express.json({ limit: "20mb" })). Form bodies viaexpress.urlencodedalso up to 20mb. - CORS allowed headers:
Content-Type,Authorization,X-Requested-With,X-Platform,X-Webhook-Secret(seesrc/app.ts).
Response envelope¶
The conventional success envelope (most controllers, e.g.
src/controllers/user.controller.ts):
{
"success": true,
"message": "User profile retrieved successfully",
"data": { "id": "…", "email": "…", "role": "USER" }
}
Paginated list envelope (getAllUsers in user.controller.ts):
{
"success": true,
"message": "Users retrieved successfully",
"data": [ /* rows */ ],
"total": 1234,
"page": 1,
"limit": 10
}
The conventional error envelope:
{
"success": false,
"message": "Failed to retrieve user profile",
"error": "duplicate key value violates unique constraint"
}
Inconsistency to know about: the envelope is not universal. -
authMiddleware/auth.routeshandlers frequently return{ message }withoutsuccess(e.g.{ message: "Authorization token missing" },{ message: "Signup successful…" }). - Some auth errors addreasonorerrorTypediscriminators (PASSWORD_CHANGED,SESSION_REVOKED,TOKEN_EXPIRED,INVALID_TOKEN). - Vendor and finance guards always includesuccess: false. - Theerrorfield is the rawerror.messageand is included broadly in controllers (the global handler only leaks it whenNODE_ENV=development).When writing new endpoints, prefer the full
{ success, message, data }envelope and only attacherrorfor diagnostic detail.
Pagination & filtering¶
There is no shared pagination helper. The de-facto convention (from
user.controller.ts) is query params parsed inline:
const page = parseInt(req.query.page as string, 10) || 1;
const limit = parseInt(req.query.limit as string, 10) || 10;
const [rows, total] = await service.getAll(page, limit); // TypeORM findAndCount
Returned as top-level total / page / limit siblings of data (not nested
in a meta object). Filtering is ad-hoc per controller via additional query
params (e.g. ?status=, ?search=) translated into TypeORM where clauses.
There is no cursor pagination.
User journeys¶
Journey 1 — Authenticated request through the auth guard (happy path)¶
The most-traveled path: a logged-in user hits a protected endpoint. The guard re-validates the JWT and re-checks the DB row on every request (it does not trust the token claims alone).
sequenceDiagram
participant FE as Frontend
participant MW as authMiddleware
participant DB as PostgreSQL
participant CT as Controller
participant SV as Service
FE->>MW: GET /api/users/profile with Bearer token
MW->>MW: extract token from header or cookie
alt token missing
MW-->>FE: 401 message Authorization token missing
else token present
MW->>MW: jwt.verify with HS256
MW->>DB: load User by decoded id select id email role isActive
alt user not found
MW-->>FE: 401 message User no longer exists
else account blocked
MW-->>FE: 403 message account has been blocked
else token predates password change
MW-->>FE: 401 reason PASSWORD_CHANGED
else session revoked
MW-->>FE: 401 reason SESSION_REVOKED
else valid
MW->>CT: attach req.user then next
CT->>SV: getUserById
SV->>DB: query row
DB-->>SV: row
SV-->>CT: user
CT-->>FE: 200 success true data user
end
end
Journey 2 — Controller error propagation to the client¶
Almost every controller wraps its body in try/catch and emits the error
envelope itself. The global handler is only the backstop for the unusual
synchronous throw.
sequenceDiagram
participant FE as Frontend
participant CT as Controller
participant SV as Service
participant DB as PostgreSQL
participant GEH as Global error handler
FE->>CT: POST /api/users body
CT->>CT: validate fullName and email present
alt missing required field
CT-->>FE: 400 success false message fullName and email are required
else duplicate email
CT->>SV: emailExists check
SV-->>CT: true
CT-->>FE: 409 success false message Email already exists
else service throws
CT->>SV: createUser
SV->>DB: insert
DB-->>SV: throws constraint error
SV-->>CT: rejected promise caught by try catch
CT-->>FE: 500 success false message Failed to create user error detail
end
Note over GEH: reached only on an uncaught synchronous throw, returns 500 generic envelope
Journey 3 — Vendor machine-to-machine request (API key tier)¶
External partners authenticate with a hashed API key, not a JWT. The middleware runs a constant-time compare and logs the access asynchronously after the response finishes.
sequenceDiagram
participant V as Vendor system
participant VM as vendorAuthMiddleware
participant DB as PostgreSQL
participant CT as VendorLeadController
V->>VM: POST /api/v1/vendor/leads X-API-Key vk_live_prefix.secret
VM->>VM: parseVendorKey split prefix and secret
alt malformed key
VM-->>V: 401 success false message Missing or malformed API key
else parsed
VM->>DB: find VendorApiKey by keyPrefix
VM->>VM: constant-time compare keyHash vs hash of secret
alt no record or hash mismatch
VM-->>V: 401 success false message Invalid API key
else key not active
VM-->>V: 403 success false message disabled or revoked
else lacks submit permission
VM-->>V: 403 success false message no submit permission
else authorized
VM->>CT: attach req.vendor then next
CT->>DB: insert lead
CT-->>V: 201 success true data lead
Note over VM: on response finish, async upsert access summary and bump requestCount
end
end
Journey 4 — Webhook with shared-secret token (Exotel callback)¶
Telephony status callbacks cannot present a JWT, so they are guarded by a
shared-secret token appended as a query param when the callback URL is
registered (src/routes/exotelWebhook.routes.ts).
sequenceDiagram
participant EX as Exotel
participant WH as exotelWebhook route
participant SV as ExotelService
participant DB as PostgreSQL
EX->>WH: POST /api/exotel/call-status with token query param
WH->>SV: verifyWebhookToken token
alt token invalid
WH-->>EX: 403 success false message Invalid webhook token
else valid
WH->>SV: process call-status payload
SV->>DB: update lead activity log
WH-->>EX: 200 success true
Note over WH: handler still try-catches, logs and returns error on failure
end
Journey 5 — CORS preflight¶
Browsers send OPTIONS before cross-origin mutating calls. app.ts handles
preflight both via the cors() middleware and an explicit app.options("*")
handler returning 204.
sequenceDiagram
participant FE as Browser
participant CORS as cors and options handler
FE->>CORS: OPTIONS /api/tokens/purchase Origin header
alt origin allowed or NODE_ENV development
CORS-->>FE: 204 with Allow-Origin Allow-Methods Allow-Headers
else origin not allowed
CORS-->>FE: error CORS not allowed
end
Note over CORS: allowed headers include Authorization X-Platform X-Webhook-Secret
Background jobs & async¶
The API contract intersects async work in two notable ways:
- Fire-and-forget side effects in handlers. Some handlers return success
immediately and run work in the background. Example:
signUpresponds200with"OTP (if email configured) is being sent."while the OTP send runs as a detached promise (src/controllers/auth.controller.ts). The client therefore must not treat a200as proof the email was sent. - BullMQ queues (emailQueue, databaseQueue, cleanupQueue, kpiQueue,
resumeAnalysisQueue) — handlers enqueue jobs via
QueueServiceand respond before the job runs. Queue health is observable at the Bull Board UI mounted at/admin/queues(routes/bullBoard.routes.ts). - Async access logging in
vendorAuthMiddlewarehappens onres.on('finish')inside asetImmediate, so it never delays the response and its failures are swallowed with aconsole.warn.
There are no API-design-specific webhooks emitted by this service; inbound webhooks (Exotel, Razorpay) follow the shared-secret / signature patterns documented per integration.
External integrations¶
Relevant to the API/error layer:
| Integration | Used by API layer for | Env vars | Failure / fallback |
|---|---|---|---|
| JWT (jsonwebtoken) | Bearer auth, HS256 verify | JWT_SECRET |
Missing secret → 500 Server configuration error. Expired → 401 TOKEN_EXPIRED. |
| Redis | TOTP rate limiting | REDIS_HOST, REDIS_PORT |
totpRateLimit fails open — if Redis errors, the request proceeds (no 429). |
| PostgreSQL (TypeORM) | every data read/write | DB_* |
Errors surface as 500 with error.message in the envelope. |
| Exotel | webhook token verify | EXOTEL_WEBHOOK_TOKEN |
Bad token → 403; feature self-disables when unset. |
| Razorpay / S3 / LLM gateway | downstream in services | per-integration | Wrapped in controller try/catch → 500 envelope. |
CORS allow-list is data-driven: CORS_ALLOWED_ORIGINS (a JSON array) is merged
with a hard-coded default list, and all origins are allowed when
NODE_ENV=development (src/app.ts).
Status lifecycles¶
The HTTP request itself has a meaningful lifecycle through the middleware/handler stack:
stateDiagram-v2
[*] --> CorsCheck
CorsCheck --> Rejected: origin not allowed
CorsCheck --> BodyParse: allowed
BodyParse --> RouteMatch
RouteMatch --> NotFound: no route
RouteMatch --> Guard: route matched
Guard --> Unauthorized: 401 missing or invalid auth
Guard --> Forbidden: 403 wrong role or permission
Guard --> RateLimited: 429 totp limit exceeded
Guard --> Handler: passed
Handler --> Success: 2xx envelope
Handler --> ClientError: 400 or 404 or 409 caught
Handler --> ServerError: 500 caught in try catch
Handler --> GlobalHandler: uncaught sync throw
GlobalHandler --> ServerError
Success --> [*]
ClientError --> [*]
ServerError --> [*]
NotFound --> [*]
Unauthorized --> [*]
Forbidden --> [*]
RateLimited --> [*]
Rejected --> [*]
The VendorApiKey status lifecycle gates the vendor tier:
stateDiagram-v2
[*] --> ACTIVE: key issued
ACTIVE --> DISABLED: admin disables
DISABLED --> ACTIVE: admin re-enables
ACTIVE --> REVOKED: admin revokes
DISABLED --> REVOKED: admin revokes
REVOKED --> [*]
note right of ACTIVE
only ACTIVE passes vendorAuthMiddleware
DISABLED returns 403 disabled
REVOKED returns 403 revoked
end note
Status-code table¶
| Code | When it is used | Source examples |
|---|---|---|
200 OK |
Successful read/update; also used by signUp and many mutations |
user.controller.ts, auth.controller.ts |
201 Created |
Resource created (createUser, vendor lead submit) |
user.controller.ts |
204 No Content |
CORS preflight response | app.ts options("*") |
400 Bad Request |
Missing/invalid input, invalid OTP, signup failure | controllers validate then 400 |
401 Unauthorized |
Missing/invalid/expired JWT, malformed/invalid vendor key, password-changed, session-revoked | auth.middleware.ts, vendorAuth.middleware.ts |
403 Forbidden |
Blocked account, wrong role (admin/finance/expert), disabled/revoked vendor key, bad webhook token | admin.middleware.ts, finance.middleware.ts, vendorAuth.middleware.ts, exotelWebhook.routes.ts |
404 Not Found |
Resource missing, or no route matched | user.controller.ts, app.ts 404 handler |
409 Conflict |
Duplicate unique resource (email already exists) | user.controller.ts createUser |
429 Too Many Requests |
TOTP attempts exceeded (5 per 15 min per IP) | totpRateLimit.middleware.ts |
500 Internal Server Error |
Caught service error, missing JWT_SECRET, vendor auth crash, global handler backstop |
controllers, app.ts global handler |
Error-response table¶
| Scenario | Status | Body | Source |
|---|---|---|---|
| No auth token | 401 | { "message": "Authorization token missing" } |
auth.middleware.ts |
| Expired JWT | 401 | { "message": "Token expired", "errorType": "TOKEN_EXPIRED" } |
auth.middleware.ts |
| Invalid JWT | 401 | { "message": "Invalid or expired token", "errorType": "INVALID_TOKEN" } |
auth.middleware.ts |
| User deleted after token issued | 401 | { "message": "User no longer exists" } |
auth.middleware.ts |
| Password changed since token | 401 | { "message": "Your password was changed. Please login again.", "reason": "PASSWORD_CHANGED" } |
auth.middleware.ts |
| Session revoked elsewhere | 401 | { "message": "This session was signed out…", "reason": "SESSION_REVOKED" } |
auth.middleware.ts |
| Blocked account | 403 | { "message": "Your account has been blocked. Please contact support." } |
auth.middleware.ts |
| Not admin | 403 | { "message": "Access denied. Admin role required." } |
admin.middleware.ts |
| Not finance/admin | 403 | { "success": false, "message": "Finance access required" } |
finance.middleware.ts |
| Malformed vendor key | 401 | { "success": false, "message": "Missing or malformed API key. Expected X-API-Key: vk_live_xxxx.yyyy" } |
vendorAuth.middleware.ts |
| Invalid vendor key | 401 | { "success": false, "message": "Invalid API key" } |
vendorAuth.middleware.ts |
| Vendor key lacks permission | 403 | { "success": false, "message": "This key does not have submit permission" } |
vendorAuth.middleware.ts |
| Bad webhook token | 403 | { "success": false, "message": "Invalid webhook token" } |
exotelWebhook.routes.ts |
| TOTP rate limit | 429 | { "message": "Too many failed TOTP attempts. Try again later." } |
totpRateLimit.middleware.ts |
| Missing required body field | 400 | { "success": false, "message": "fullName and email are required" } |
user.controller.ts |
| Duplicate resource | 409 | { "success": false, "message": "Email already exists" } |
user.controller.ts |
| Unhandled service error | 500 | { "success": false, "message": "Failed to …", "error": "<detail>" } |
controllers |
| Route not found | 404 | { "success": false, "message": "Route not found", "path": "<url>" } |
app.ts |
| Backstop uncaught throw | 500 | { "success": false, "message": "Internal Server Error", "error?": "<detail in dev only>" } |
app.ts global handler |
Edge cases, limits & gotchas¶
- No automatic async error forwarding. Express 4 + no
express-async-errorsmeans a rejected promise in anasynchandler does not reach the global error handler. Every handler must wrap its body intry/catchor the request will hang until timeout. This is the single most important rule when adding endpoints. - The global error handler leaks
error.messageonly in development. Most controllers, however, put the raw message in theerrorfield unconditionally — be careful not to leak internals via that path. - Envelope is not enforced. Three shapes coexist: full
{ success, message, data }, bare{ message }(auth/legacy), and{ success, message }(guards). New code should use the full envelope. - Auth re-hits the DB on every request.
authMiddlewaredoes afindOneonUser(and sometimesRefreshToken) per request — it does not trust JWT claims alone. This enables instant block/logout-everywhere/session-revoke at the cost of a DB read per call. - Token also accepted via cookie.
Authorization: Beareris preferred, buttoken/authTokencookies are honored, and login setstoken,refreshToken, and a non-httpOnlyusercookie. Thetokencookie ishttpOnly:falseby design (read by the frontend). x-platformis read ad-hoc, not via middleware. Only a handful of controllers (auth,issues,voiceInterview,missOzone) inspect it. Default ismy-analytics-school;mr-mentorandmr-hireare the other known values. There is no central platform-resolution middleware on this branch (inferred — aPlatformentity /MasGatewayexist but are not wired into a request guard here).- Rate limiting fails open.
totpRateLimitlets requests through if Redis is unavailable — availability is favored over strict throttling. It is the only rate limiter in the stack; there is no global request throttle. /api/v1/vendoris the only versioned namespace. All other routes are unversioned; breaking changes to/api/*must be coordinated across consumers (see Cross-Repo Dependencies).- No idempotency keys. Mutating endpoints are not idempotent and do not honor
an
Idempotency-Keyheader; retries on POST can double-write. Razorpay/order flows guard against this at the service layer, not the API layer. req.ipis unreliable.trust proxyis not configured, so client-IP logic (vendor logging, TOTP keys) readsX-Forwarded-For/CF-Connecting-IPmanually. Behind a new proxy hop this needs review.- Body limit is 20mb for both JSON and urlencoded — large uploads use S3 presigned URLs instead of posting bytes through this API.
/api/test-datais a public debug probe that reveals a seed user's name and role; it is a known leftover (TODO: gate or remove).
Related docs¶
- Request Lifecycle & Middleware — the full middleware stack, CORS, body parsing, Socket.IO auth.
- Identity & Access — JWT issuance, refresh-token rotation, roles, session revoke, master TOTP.
- Vendor API Platform — vendor key issuance, permissions, access summaries, lead ingestion.
- Root navigation guide — repo map and cross-repo API consumers.