feat: migrate HTTP platform from Express to Fastify#3190
Conversation
This comment has been minimized.
This comment has been minimized.
Replace @nestjs/platform-express with @nestjs/platform-fastify and swap supporting middleware: cookie-parser -> @fastify/cookie, plus @fastify/static for static assets. Key changes: - Build the Fastify adapter from configuration at bootstrap (trustProxy and JSON body limit), since Fastify requires these at construction time, before the DI container exists. - Abstract request access behind src/routes/common/http/http-request.utils.ts (getHeader / getRoutePath / getRouteParam / getRouteUrl). - Response API: res.json() -> res.send(), res.cookie() -> res.setCookie(), res.set()/.header() -> reply.header(), res.headersSent -> reply.sent, res.redirect(code, url) -> res.redirect(url, code). - Cookie maxAge is now expressed in seconds (Fastify) instead of milliseconds (Express). - Tests: createTestApplication / initTestApplication call Fastify's ready() after init() so supertest can reach the server; all boot specs use them. - Inline the production bootstrap into main.ts and remove the AppProvider / DefaultAppProvider abstraction; TestAppProvider is now standalone. Behavioural parity intended; unit suite green (5363 passing). Integration and e2e suites require live Redis/Postgres/AMQP to verify. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@claude review |
|
Claude finished @PooyaRaki's task in 4m 37s —— View job Claude PR Review — Express → Fastify migration
Overall, this is a clean, well-scoped migration. Below are the things worth tightening before merge. 🐛 Potential bugs
🔐 Security / robustness
🧪 Test coverage
🧹 Quality / nits
✅ What looks good
Nothing here is blocking on its own; #1 and #2 are the two I'd want addressed (or explicitly accepted) before merging. · Branch: |
The Express->Fastify platform swap left several parity gaps that failed
the integration-tests CI job:
- Re-enable trailing-slash matching (Fastify defaults it off) via
routerOptions.ignoreTrailingSlash, fixing 404s on `/path/` routes.
- Raise routerOptions.maxParamLength to 1024 (Fastify defaults to 100) so
composite ids like `multisig_<address>_<safeTxHash>` (~118 chars) match
instead of 404ing.
- Preserve the Set-Cookie contract: derive `Expires` from `maxAge` on set
and clear without `Max-Age=0`, via getSetCookieOptions/getClearCookieOptions
(auth + oidc controllers).
- Pass an explicit 302 to the oidc redirects; Fastify reuses the
NestJS-set 200 status when no code is supplied.
- Add NullResponseInterceptor so a top-level `null` controller return
serializes to an empty body (Express behaviour) instead of literal `null`.
- Enable versioning in createTestApplication so the v1/v2 `/chains`
controllers no longer collide ("GET already declared for route") under
Fastify, and fix hooks.controller spec to the real `/v1/hooks/events` path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- guard the OIDC state cookie against a sub-second TTL collapsing to Max-Age=0 (Math.max(1, floor(stateMaxAge/1000))) - restore body-parser parity by accepting tb/pb units in parseBodyLimit - share FASTIFY_ROUTER_OPTIONS so createTestApplication matches production route matching (trailing slash, long composite ids) - type the 404 middleware request as the shared RequestLike - refresh the stale jsonLimit docstring (parser, units, default, namespace) - add runnable unit specs for auth cookie options and parseBodyLimit Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@claude review |
|
Claude finished @PooyaRaki's task in 3m 34s —— View job Claude PR Review — Express → Fastify migration
Most of the prior review feedback has been addressed (sub-second TTL guard at 🐛 Bugs / correctness
🔐 Security / robustness
🧪 Test coverage
🧹 Quality / nits
✅ What looks good
· Branch: |
- Reject sub-second AUTH_STATE_TTL_MILLISECONDS (min 1s) so a misconfigured OIDC state cookie fails fast instead of collapsing to a 1s TTL - Send no body on the targeted-messaging 204 branch instead of an empty object (Fastify strips 204 payloads either way) - Mark the deferred express.* config rename with TODO(fastify-rename) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@claude review |
|
All prior review feedback has been addressed. Latest commit (49ef8cb) covers the remaining items:
Items not actioned (with rationale):
|
|
@claude review |
|
Claude finished @PooyaRaki's task in 4m 1s —— View job Claude PR Review — Express → Fastify migration
Prior review rounds have addressed the bulk of issues. The migration is in good shape — the remaining items are minor and none are blocking. 🐛 Bugs / correctness
🔐 Security / robustness
🧪 Test coverage
🧹 Quality / nits
✅ What looks good
Nothing here blocks merge; #1 ( · Branch: |
- Tighten maxParamLength from 1024 to 256 (~2× the longest real composite id, ~118 chars) to avoid admitting kilobyte-long path segments through ValidationPipe/zod on every request - Return 'unknown' sentinel from getRoutePath when no route template is available (unmatched/404 requests) instead of the actual URL, preventing log cardinality explosion in Datadog - Document request.method ?? 'UNKNOWN' fallback as a type-required sentinel (unreachable at runtime under both FastifyRequest and raw Node IncomingMessage) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Reduced maxParamLength from The longest real composite id in the API is |
|
@claude do a final review, all the previous remarks are fixed. |
|
@claude review all the remarks are fixed. |
|
Claude finished @PooyaRaki's task in 1m 57s —— View job Claude PR Review — Express → Fastify migration (final pass)
Confirmed the latest changes against prior review rounds. The migration is in a good state to merge — nothing blocking remains. ✅ Prior remarks addressed
🟡 Items intentionally deferred (rationale accepted)
🧹 Minor nits (non-blocking, optional)
✅ What looks good
LGTM — no further changes needed from my side. Suggest confirming · Branch: |

What
Migrates the gateway's HTTP platform from Express to Fastify: swaps
@nestjs/platform-express→@nestjs/platform-fastify, replacescookie-parserwith@fastify/cookie, and adds@fastify/staticfor static assets.Why
Fastify offers lower per-request overhead and a more modern plugin model. This is an internal infrastructure change intended to be behaviour-preserving for clients.
Key changes
Adapter built from config at bootstrap. Fastify needs
trustProxyand the JSONbodyLimitat construction time — before the DI container exists — somain.tsreads the raw configuration and builds the adapter directly (createFastifyAdapterFromConfiguration). This replaces the old Expressapp.set('trust proxy')/app.use(json(...))post-construction setup. - Request access abstracted behindsrc/routes/common/http/http-request.utils.ts(getHeader/getRoutePath/getRouteParam/getRouteUrl), so guards, interceptors, filters and decorators no longer depend on Express types.Response API migrated:
res.json()→res.send(),res.cookie()→res.setCookie(),res.set()/.header()→reply.header(),res.headersSent→reply.sent,res.redirect(code, url)→res.redirect(url, code).Cookie
maxAgeis now in seconds (Fastify/@fastify/cookie) rather than milliseconds (Express) — auth and OIDC flows updated accordingly (getSecondsUntil, ms→s conversions).Test harness:
createTestApplication/initTestApplicationcall Fastify'sready()afterinit()sosupertestcan reach the server; all boot specs (integration + e2e) use them.Removed the
AppProvider/DefaultAppProviderabstraction — the production bootstrap is inlined intomain.ts, andTestAppProvideris now a standalone test helper (its publicprovide()API is unchanged, so consumer specs were untouched).Testing
✅ Unit suite: 5363 passing, 0 failing.
✅ Production build typecheck (
tsconfig.build.json): clean.✅ Biome: clean.
⏳ Integration & e2e require live Redis/Postgres/AMQP and must be verified in CI / locally.
Reviewer notes
yarn test:integrationandyarn test:e2eare green in CI.yarn startsmoke test is worthwhile to confirm Fastify boots with Swagger (/api), static assets, and cookie auth.express.*(jsonLimit,trustProxy) — left as-is to avoid touching deployed env vars; can be renamed in a follow-up.🤖 Generated with Claude Code