API server
How the Express API handles a request — middleware, modules, errors, uploads, email and pricing.
The API (apps/api) is an Express 5 server written in TypeScript, using
MongoDB through Mongoose 9. Every client goes through it for products,
carts, orders, users and content. This page explains how a request moves
through the server. The full list of endpoints is in
API endpoints.
At a glance
Section titled “At a glance”| Entry point | apps/api/server.ts → createApp() in src/app.ts |
| Port | PORT, default 8000 |
| Endpoints | 297, under 41 base paths |
| Models | 31 Mongoose models in apps/api/models (Data models) |
| Validation | Zod schemas plus validate.middleware |
| Tests | Vitest + Supertest + mongodb-memory-server, 7 files |
A request, step by step
Section titled “A request, step by step”- 1helmetsecurity headers
- 2requestIdx-request-id
- 3CORSorigin allowlist
- 4Stripe webhookraw body, before JSON
- 5JSON parser10 MB limit
- 6Sanitizestrip $ operators
- 7Rate limits300/min, auth 10/min
- 8Routers/api/*
- 9Errors404 + error handler
- helmet sets secure headers. In production
trust proxyis set to1so the real client IP is used for rate limiting. - requestId gives each request an id, which is returned in the
x-request-idheader and in error responses. - CORS allows
ADMIN_URL,CLIENT_URL, a few hosted domains, and the local ports 3000, 5173 and 8081. WhenNODE_ENV=developmentevery origin is allowed. - The Stripe webhook (
POST /api/payments/webhook) is mounted before the JSON parser, because the signature check needs the exact raw bytes. - express.json / urlencoded parse bodies up to 10 MB.
- express-mongo-sanitize replaces
$and.keys in the body and params, so{"$gt": ""}-style query injection can’t get through. - Rate limits: 300 requests per minute per IP overall, and 10 per minute
on login, register, OAuth and password endpoints. Image search and the
order
webhook-statusroute have their own limits. - Routers handle
/api/..., thennotFoundHandleranderrorHandlerrun.
Modules and legacy controllers
Section titled “Modules and legacy controllers”New code lives in src/modules/<domain>/ and is split into
*.routes.ts, *.controller.ts, *.service.ts and *.repository.ts. Nine
older areas still run on the original handlers in apps/api/controllers.
Those areas are products, orders, order workflow, the four blog routers,
salaries and cash collections. They are mounted through thin module shells,
so their URLs stay the same. This is the “strangler fig” migration
described in LEGACY_MIGRATION.md.
Errors
Section titled “Errors”Every error goes through one handler. It turns Zod errors, bad ObjectIds,
Mongoose validation errors, duplicate keys (E11000) and JWT errors into an
AppError with an HTTP status, and always responds with the same shape:
{ "success": false, "error": { "code": "BAD_REQUEST", "message": "…", "details": [] }, "message": "…", "meta": { "requestId": "…" }}In production, the messages of 5xx errors are hidden.
apps/api/src/middleware/error.middleware.ts› errorHandlerHealth checks and API docs
Section titled “Health checks and API docs”/health
GET/health/live
GET/health/ready
/health/ready also checks the MongoDB connection, which makes it a good
target for a load balancer. Outside production, Swagger UI is served at
/api/docs.
Uploads and email
Section titled “Uploads and email”- Images.
multerkeeps uploads in memory, accepts images only and caps each file at 5 MB. They are then sent to Cloudinary (the default, folderbabyshop) or to AWS S3, depending onDEFAULT_UPLOAD_PROVIDER. S3 presigned URLs last one hour. - Email.
nodemailersends over SMTP (SMTP_HOST, defaultsmtp.gmail.com:587). It sends order confirmations, invoices, password-reset links and admin messages.
Order pricing
Section titled “Order pricing”When a customer places an order, the API prices it again from the database. It never trusts the client’s numbers:
- each line uses the
pricestored on theProduct; - shipping is a flat 15 when the subtotal is at or below
FREE_DELIVERY_THRESHOLD(default 999), and 0 above it; - tax is
TAX_RATE × subtotal(default0).
Variable names and defaults are listed in Environment variables.
pnpm --filter @babyshop/api test runs Vitest against an in-memory MongoDB.
Nodemailer and Cloudinary are mocked. The 7 test files cover auth, orders and
pricing, payments and webhooks, users and roles, uploads and vendors, a smoke
suite, and utility unit tests.