The two projects answer the same requests with the same payloads. Run them both and curl 3,008 cannot tell which one is listening, which is the fairest way to compare what each framework costs.
| Concern | Express 24,430 (The Bookshelf API) | Nest (this section) |
|---|---|---|
| Wiring | createApp() mounts routers in order | @Module arrays, resolved by the container |
| Route | r.post('/', guard, validate, c.create) | @Post() @UseGuards(...) create(@Body() dto) |
| Validation | Zod 44,027 schema + validate middleware | DTO class + global ValidationPipe |
| Errors and auth | AppError, readToken, requireRole | exceptions, JwtAuthGuard, RolesGuard |
| Docs | zod-to-openapi 1,623 registry, by hand | generated from the DTOs and decorators |
| Swapping the store | edit one line in data/index.js | one provider: { provide: Store, useClass } |
Nest won the parts nobody enjoys writing. The OpenAPI document generated itself from DTOs that already existed, swapping the data layer for a test became overrideProvider(Store).useValue(stub), and failures moved to boot time: a missing provider is a startup crash naming the class, where the Express equivalent — a router handed an undefined store — is a TypeError on the first request.
Express won everything about the loop. node --watch src/server.js starts in milliseconds; nest build && node dist/main.js does not, and the Express project has no decorators, no reflect-metadata, no compile step and no framework vocabulary.
Run this project with npm 2,036 install, a .env holding a 32-character ACCESS_SECRET, then npx nest build and node dist/main.js. MongoDB replaces the store with MongoDB 1,815 for both projects, and Next.js calls whichever one you kept from Next.js 10,514 — the envelope and the /api/v1 paths are identical either way.