Overview
One versioned REST API — 77 paths under /api/v1 — speaking a single response dialect. Every endpoint validates input with zod, returns the same envelope, paginates the same way (or streams application/pdf), and fails with typed, predictable errors. The API contract lives twice: hand-maintained markdown in docs/api/** (authoritative) and a mirrored OpenAPI 3.1 spec (openapi/openapi.yaml) for Apidog/Postman import.
Response Envelope
Every response, success or failure:
{ "success": true, "data": { } }
{ "success": false, "message": "Validation error", "errors": { "email": "…" } }
Lists add pagination meta: { page, limit, total, totalPages, hasNext, hasPrev } — offset-based with page/limit, capped at 100 per page.
Endpoint Map
| Area | Endpoints | Auth |
|---|---|---|
| Auth | POST /auth/register, POST /auth/login (rate-limited + 10/15 min lockout), GET/DELETE /auth/session, GET /auth/sessions + per-device revoke + revoke-others, POST /auth/email-verification/*, POST /auth/password-reset + OTP verify + token verify, GET /auth/csrf-token | Public / customer |
| Account | GET/PATCH/DELETE /users/me, PATCH /users/me/password, POST /users/me/email + /verify, POST /users/me/phone-number + /verify, GET/POST/PATCH/DELETE /users/me/addresses (default-flag invariants, ownership-scoped) | Customer |
| Catalog | GET /products (search/brand/sort/paginated, customer visibility = has ACTIVE variant), GET /products/{id} (images ordered, is_primary, variants with final_price), GET /categories, GET /categories/{id}, GET /categories/{id}/products | Public |
| Cart | GET /cart (404 if never created), POST /cart/items, PATCH /cart/items/{variant}, DELETE /cart/items/{variant}, DELETE /cart (all four mutations advisory-locked) | Customer |
| Orders | POST /orders (checkout: advisory-locked, coupon + shipping + mock payment + snapshots + cart clear), GET /orders + GET /orders/{id} (immutable snapshots, status-filtered) | Customer |
| Reviews | GET /products/{id}/reviews (public, paginated), POST/PATCH/DELETE /reviews, GET /users/me/reviews, GET /reviews/{id} — purchase-gate flag off, ImageKit provenance validated | Mixed |
| Reports | GET /admin/reports/pnl, /expenses, /revenue (+ .pdf aliases) by `month | quarter |
| Uploads | GET /uploads/imagekit-auth and GET /admin/products/uploads/imagekit-auth (short-lived HMAC params) | Customer/Admin |
Admin surface (/admin/*)
Full catalog CRUD including nested variants and images, category management with product assignment, inventory adjustments + reservations, order status transitions, review moderation, coupon management with usage history, user management (role changes are SUPER_ADMIN only), admin account management, stats, audit log, analytics (overview / coupons / expenses — SUPER_ADMIN) and financial PDF reports (P&L / Expenses / Revenue by month|quarter|year|custom — SUPER_ADMIN, attachment|inline PDF or ?format=json).
Validation and Errors
- One zod schema per endpoint validates
body,query, andparamstogether (includingpublic_idprefix patternsusr_,prd_,var_,adr_, …); failures return400with per-fielderrors. - A typed error taxonomy (
BadRequest400,Unauthorized401,Forbidden403,NotFound404,Conflict409,Gone410,TooManyRequests429, …) flows to one central handler;prismaErrorMappermapsP2002→ 409,P2025→ 404, Postgres22001(string-too-long via cause chain) → 400 so internals never leak; stack is only included outside production. - Unknown
/api/*paths return the JSON envelope too — never an HTML 404. Health probes/health//health/readylive outside the global rate limiter.
Security Per Request
| Mechanism | Detail |
|---|---|
| Rate limits | Global default (RATE_LIMIT_MAX) plus stricter dedicated limiters on login, register, email verification/change, phone change, password reset; global limiter skipped in test, route limiters stay active |
| Login lockout | Per-email tracker: 10 failed attempts → locked for 15 minutes; failed admin logins audited, unknown-email probes logged |
| CSRF | csrf-csrf double-submit: x-csrf-token header + __Host-csrf cookie bound to the session cookie; anonymous (no session) skipped; fail-closed in production (ENABLE_CSRF boot guard) |
| Cookies | session cookie: httpOnly, Secure in production, SameSite=Lax; peppered SHA-256(session + SESSION_SECRET) hash stored server-side with 14-day idle timeout |
| Trust proxy | TRUST_PROXY env drives app.set("trust proxy", …) — correct IP for rate limits/logs/session records behind Render/Neon |
Contract Documentation
The API is documented twice by design:
- Markdown contracts per domain under
docs/api/— the declared source of truth. - An OpenAPI YAML spec with shared envelope/pagination components, configured for Redocly linting.
Known gap
The OpenAPI spec is not yet linted in CI against the markdown contracts — drift between them is a tracked work item.
Design Rules in One Glance
- URLs use prefixed public IDs (
ord_…,prd_…) — internal autoincrement IDs never leave the database. - Money is
Decimal(10,2)serialized as fixed strings — never floats. - Every write that matters is validated, transactional, and audited.
- Pagination, sorting, and error shapes are identical across all fifteen modules.
Result
A client developer can guess any endpoint's behavior after learning three conventions: the envelope, the pagination meta, and the public-ID pattern.