Skip to content

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.

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
apps/api/src/app.ts› createApp
  1. 1helmetsecurity headers
  2. 2requestIdx-request-id
  3. 3CORSorigin allowlist
  4. 4Stripe webhookraw body, before JSON
  5. 5JSON parser10 MB limit
  6. 6Sanitizestrip $ operators
  7. 7Rate limits300/min, auth 10/min
  8. 8Routers/api/*
  9. 9Errors404 + error handler
Middleware order in createApp()
  1. helmet sets secure headers. In production trust proxy is set to 1 so the real client IP is used for rate limiting.
  2. requestId gives each request an id, which is returned in the x-request-id header and in error responses.
  3. CORS allows ADMIN_URL, CLIENT_URL, a few hosted domains, and the local ports 3000, 5173 and 8081. When NODE_ENV=development every origin is allowed.
  4. The Stripe webhook (POST /api/payments/webhook) is mounted before the JSON parser, because the signature check needs the exact raw bytes.
  5. express.json / urlencoded parse bodies up to 10 MB.
  6. express-mongo-sanitize replaces $ and . keys in the body and params, so {"$gt": ""}-style query injection can’t get through.
  7. 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-status route have their own limits.
  8. Routers handle /api/..., then notFoundHandler and errorHandler run.

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.

apps/api/src/modules/LEGACY_MIGRATION.md

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› errorHandler GET/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.

  • Images. multer keeps uploads in memory, accepts images only and caps each file at 5 MB. They are then sent to Cloudinary (the default, folder babyshop) or to AWS S3, depending on DEFAULT_UPLOAD_PROVIDER. S3 presigned URLs last one hour.
  • Email. nodemailer sends over SMTP (SMTP_HOST, default smtp.gmail.com:587). It sends order confirmations, invoices, password-reset links and admin messages.
apps/api/config/uploadService.ts apps/api/utils/emailService.ts

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 price stored on the Product;
  • 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 (default 0).
apps/api/src/modules/orders/orderPricing.ts› computeTotals

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.