First strangler migration (L1): /api/weather is served by a NestJS module.
- @trek/shared/weather Zod contract; Nest controller byte-identical to the legacy Express route (paths, query params, status codes, { error } bodies, lang default, ApiError/500 passthrough). Service reuses getWeather/getDetailedWeather (+ shared cache; MCP tools unchanged).
- Strangler routes /api/weather to Nest by default; the legacy Express route + its migration-time parity test were decommissioned in this PR.
- Frontend (FE2): weatherApi typed against the @trek/shared WeatherResult contract.
- Harness: reusable Nest-vs-Express parity harness, e2e harness (temp SQLite + seed/cookie helpers, real JwtAuthGuard), src/nest coverage gate raised to >=80%, src/nest test guide.
- Verified end-to-end on a prod mirror (dev1): 401/400/200 via Nest with real Open-Meteo data, Express route gone.
NestJS migration layer — module & test guide
This folder holds the co-hosted NestJS app that incrementally strangles the legacy
Express API (see the "Brownfield Rewrite" board). Until a prefix is migrated, the
top-level dispatcher in src/index.ts routes it to the legacy app; migrated
prefixes go to Nest. Weather (weather/) is the reference implementation — copy
its shape when migrating a new domain.
Module layout (per domain)
shared/src/<domain>/<domain>.schema.ts(.spec.ts) # Zod contract — single source of truth
server/src/nest/<domain>/<domain>.service.ts # business logic (ported 1:1 from the Express service)
server/src/nest/<domain>/<domain>.controller.ts # same routes/verbs/params/status codes as Express
server/src/nest/<domain>/<domain>.module.ts # registered in app.module.ts
Add the prefix to DEFAULT_NEST_PREFIXES in strangler.ts to route it to Nest
(operators can override at runtime via the NEST_PREFIXES env var — instant
rollback, no redeploy).
Parity is law
A migrated route must be byte-identical for the client: same URL, method,
query/body, HTTP status, Set-Cookie, and JSON body — including bespoke error
strings. Where the legacy route returns a hand-written error (e.g. weather's
{ error: 'Latitude and longitude are required' }), reproduce that exact body in
the controller rather than relying on the generic ZodValidationPipe envelope.
How to write the tests
Every module ships three kinds of tests; the coverage gate (vitest.config.ts,
scoped to src/nest/**) requires ≥80%.
-
Service / controller unit spec —
tests/unit/nest/<domain>.controller.test.ts. Instantiate the controller with a mocked service; assert status codes, the exact{ error }bodies, and that inputs are forwarded correctly (defaults, coercion). Seeweather.controller.test.ts. -
Parity test —
tests/parity/<domain>.parity.test.ts. Mock the shared service identically for both apps, then fire the same request at the Express route and the Nest controller with theexpectParity()harness (tests/parity/parity.ts) and assert identical status + body. This is the gate before flipping the toggle. Seeweather.parity.test.ts. -
e2e —
tests/e2e/<domain>.e2e.test.ts. Boot the Nest module against a temp in-memory SQLite db via the shared harness (tests/e2e/harness.ts:createTempDb/seedUser/sessionCookie), exercising the realJwtAuthGuardend-to-end (401 without cookie, 200 with a signed session). Mock external I/O (HTTP/etc.). Seeweather.e2e.test.ts.
Definition of Done (per module)
Contract in @trek/shared → service ported 1:1 → controller with identical routes →
validation/error parity → unit + parity + e2e tests over the gate → prefix toggled to
Nest → parity verified on the demo DB → then decommission the old Express
route/service (separate step, after the toggle is confirmed in prod) → frontend points
at the typed contract (Frontend Track).