How to Migrate from Express.js to Fastify
How do I migrate from Express.js to Fastify?
TL;DR
- Bottom line: Migrate incrementally using
@fastify/expressas a bridge — wrap your Express app in Fastify, then convert routes and middleware to native Fastify plugins one at a time until Express can be removed. - Key tool/command:
npm install fastify @fastify/expressthenawait fastify.register(require('@fastify/express')); fastify.use(expressApp) - Watch out for: Treating Fastify plugins like Express middleware — plugins use encapsulated scope, not a linear chain, so registering a plugin in a child context does not affect sibling or parent routes.
- Works with: Fastify 5.8.5 (current, Apr 2026), Node.js 20+/22+, TypeScript 5.x. Express 4.x/5.x middleware supported via
@fastify/expressv4.0.6 bridge. [src1, src9]
Constraints
- Fastify v5 requires Node.js 20+ — do not attempt migration if production runs Node 18 or earlier. Fastify v4 LTS ended June 30, 2025. [src6]
- Never use
reply.send()ANDreturna value in the same async handler — this causesFST_ERR_PROMISE_NOT_FULFILLED. Pick one:return dataorreply.send(data). [src1] - The
@fastify/expressbridge does NOT support HTTP/2 — migrate all routes to native Fastify before enabling HTTP/2. [src4] - Every JSON Schema must include the top-level
typeproperty (e.g.,type: 'object'). Fastify v5 silently passes validation without it, which means malformed data reaches your handlers unvalidated. [src6] - Do not install
body-parser— Fastify parsesapplication/jsonandtext/plainby default. Double-parsing causes subtle bugs and wasted CPU. [src1] - Plugin encapsulation is NOT middleware — a decorator or hook registered inside a child plugin is invisible to sibling/parent plugins. Use
fastify-plugin(fp()) wrapper to intentionally break encapsulation for shared decorators. [src1, src2]
Quick Reference
| Express Pattern | Fastify Equivalent | Example |
|---|---|---|
app.get('/path', handler) | fastify.get('/path', handler) | fastify.get('/users', async (request, reply) => { return users }) |
(req, res, next) signature | (request, reply) signature | async (request, reply) => { return data } |
res.json(data) | return data (auto-serialized) | return { id: 1, name: 'Alice' } |
res.status(404).json(err) | reply.code(404).send(err) | reply.code(404).send({ error: 'Not found' }) |
app.use(middleware) | fastify.register(plugin) | fastify.register(require('@fastify/cors')) |
express.Router() | Plugin with prefix | fastify.register(userRoutes, { prefix: '/api/users' }) |
express.static('public') | @fastify/static plugin | fastify.register(require('@fastify/static'), { root: path.join(__dirname, 'public') }) |
express.json() body parser | Built-in (automatic) | JSON parsing is on by default, no middleware needed |
app.use((err, req, res, next) => {}) | fastify.setErrorHandler() | fastify.setErrorHandler((error, request, reply) => { reply.code(500).send({ error }) }) |
express-validator / Joi | Built-in JSON Schema | fastify.post('/users', { schema: { body: userSchema } }, handler) |
morgan / winston logging | Built-in Pino logger | const fastify = Fastify({ logger: true }) then request.log.info('message') |
helmet middleware | @fastify/helmet plugin | fastify.register(require('@fastify/helmet')) |
cors middleware | @fastify/cors plugin | fastify.register(require('@fastify/cors'), { origin: '*' }) |
compression middleware | @fastify/compress plugin | fastify.register(require('@fastify/compress')) |
app.listen(3000, callback) | await fastify.listen({ port: 3000 }) | await fastify.listen({ port: 3000, host: '0.0.0.0' }) |
req.connection | request.socket | request.socket.remoteAddress (v5: request.connection removed) |
res.redirect(302, url) | reply.redirect(url, 302) | reply.redirect('/new-path', 301) (v5: reversed args) |
Custom logger: {logger: pinoInstance} | {loggerInstance: pinoInstance} | Fastify({ loggerInstance: pino() }) (v5: logger no longer accepts instances) |
Decision Tree
START
├── Is this a greenfield project or adding Fastify to an existing Express app?
│ ├── GREENFIELD → Start fresh with Fastify, skip the bridge. See Step 1 below.
│ └── EXISTING EXPRESS APP ↓
├── How large is the Express codebase?
│ ├── SMALL (<20 routes, <5 middleware) → Rewrite all routes directly, no bridge needed
│ └── MEDIUM/LARGE ↓
├── Can you freeze feature development during migration?
│ ├── YES → Rewrite route-by-route without the bridge, deploy as a batch
│ └── NO → Use @fastify/express bridge for incremental migration (Step 2)
├── Do you use Express-specific middleware with no Fastify equivalent?
│ ├── YES → Keep them running via @fastify/express, migrate last
│ └── NO ↓
├── Do you use custom Express error middleware (4-arg functions)?
│ ├── YES → Convert to fastify.setErrorHandler() early (Step 6)
│ └── NO ↓
├── Are you targeting HTTP/2?
│ ├── YES → Cannot use @fastify/express bridge; migrate all routes to native Fastify first
│ └── NO ↓
└── DEFAULT → Migrate routes one-by-one, replace middleware with @fastify/* plugins, add JSON Schema validation, remove Express when done
Decision Logic
Structured rules an agent can apply directly when advising a migration. Use the inputs_needed answers to select the path.
If production runs Node.js 18 or earlier
→ Do NOT start a Fastify v5 migration. Upgrade Node to 20+ first (or 22 LTS); Fastify v5 hard-requires Node 20+ and v4 reached EOL on 2025-06-30. [src1, src6]
If the codebase is small (<20 routes, <5 middleware)
→ Skip the @fastify/express bridge. Rewrite all routes directly to native Fastify and deploy as one batch — the bridge overhead is not worth it at this size. [src2, src3]
If the codebase is medium/large and feature development cannot freeze
→ Use the @fastify/express v4.0.6 bridge for incremental, route-by-route migration; the first PR only adds Fastify and wraps the Express app, each later PR moves a few routes. [src2, src5, src9]
If you must enable HTTP/2
→ Migrate every route to native Fastify before turning HTTP/2 on. @fastify/express does not support HTTP/2 because Express cannot use Node's core HTTP/2 module. [src4]
If the app uses express-validator, Joi, or celebrate for validation
→ Replace them with Fastify's built-in JSON Schema (Ajv) per route; there is no automated Joi-to-JSON-Schema converter, so budget rewrite time, and always include the top-level type property. [src1, src6]
If you need per-route request timeouts
→ Use Fastify v5.8.0+ first-class handler-level timeouts instead of porting Express timeout middleware. [src9]
If the app relies on Express-only middleware with no Fastify equivalent (e.g., Passport strategies)
→ Keep those routes running through @fastify/express and migrate them last; do not block the rest of the migration on them. [src2, src4]
Step-by-Step Guide
1. Install Fastify alongside Express
Add Fastify and the Express compatibility bridge to your existing project. Both frameworks can coexist on the same server during migration. [src2, src4]
npm install fastify @fastify/express
Verify: node -e "const f = require('fastify'); console.log(f().version)" → prints Fastify version (e.g., 5.8.5)
2. Wrap Express app with Fastify bridge
Create a Fastify instance that proxies all requests to your existing Express app. This lets Fastify handle the server while Express routes still work. [src4, src5]
// server.js — bridge setup
const Fastify = require('fastify');
const expressApp = require('./app'); // your existing Express app
const fastify = Fastify({ logger: true });
async function start() {
// Register the Express compatibility layer
await fastify.register(require('@fastify/express'));
// Mount your entire Express app under Fastify
fastify.use(expressApp);
await fastify.listen({ port: 3000, host: '0.0.0.0' });
console.log(`Server running on ${fastify.server.address().port}`);
}
start();
Verify: curl http://localhost:3000/your-existing-route → same response as before. All Express routes still work.
3. Migrate routes from Express to native Fastify
Convert Express route handlers one at a time. Change (req, res) to (request, reply) and return data instead of calling res.json(). [src2, src3]
// BEFORE: Express route
app.get('/api/users/:id', async (req, res) => {
try {
const user = await db.findUser(req.params.id);
if (!user) return res.status(404).json({ error: 'Not found' });
res.json(user);
} catch (err) {
res.status(500).json({ error: 'Internal server error' });
}
});
// AFTER: Fastify route (register as a plugin)
async function userRoutes(fastify, options) {
fastify.get('/api/users/:id', {
schema: {
params: {
type: 'object',
properties: { id: { type: 'string' } },
required: ['id']
}
}
}, async (request, reply) => {
const user = await db.findUser(request.params.id);
if (!user) {
reply.code(404);
return { error: 'Not found' };
}
return user; // auto-serialized to JSON
});
}
fastify.register(userRoutes);
Verify: curl http://localhost:3000/api/users/123 → same JSON response. Remove the Express version of the route.
4. Replace Express middleware with Fastify plugins
Convert common middleware to their Fastify equivalents. Each Express app.use() becomes a fastify.register() call. [src2, src4]
npm install @fastify/cors @fastify/helmet @fastify/compress @fastify/cookie @fastify/session
// BEFORE: Express middleware stack
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const compression = require('compression');
const app = express();
app.use(cors({ origin: '*' }));
app.use(helmet());
app.use(compression());
app.use(express.json());
// AFTER: Fastify plugin registration
const Fastify = require('fastify');
const fastify = Fastify({ logger: true }); // JSON body parsing is built-in
fastify.register(require('@fastify/cors'), { origin: '*' });
fastify.register(require('@fastify/helmet'));
fastify.register(require('@fastify/compress'));
// No need for body parser — Fastify parses JSON by default
Verify: Check response headers for CORS, security headers, and compression. curl -v http://localhost:3000/api/users should show the same headers.
5. Add JSON Schema validation to replace validation middleware
Replace express-validator, Joi, or celebrate with Fastify's built-in JSON Schema validation. Schemas are declared per-route and validated automatically before the handler runs. [src1, src2]
// BEFORE: Express with express-validator
const { body, validationResult } = require('express-validator');
app.post('/api/users', [
body('email').isEmail(),
body('name').isLength({ min: 2 }),
], (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
// ... create user
});
// AFTER: Fastify with built-in JSON Schema
fastify.post('/api/users', {
schema: {
body: {
type: 'object',
required: ['email', 'name'],
properties: {
email: { type: 'string', format: 'email' },
name: { type: 'string', minLength: 2 }
}
},
response: {
201: {
type: 'object',
properties: {
id: { type: 'string' },
email: { type: 'string' },
name: { type: 'string' }
}
}
}
}
}, async (request, reply) => {
// request.body is already validated — no manual checks needed
const user = await db.createUser(request.body);
reply.code(201);
return user;
});
Verify: curl -X POST -H 'Content-Type: application/json' -d '{"email":"bad"}' http://localhost:3000/api/users → 400 with validation error. Schema-driven responses also boost serialization speed by ~2x. [src7]
6. Convert error handling
Replace Express's 4-argument error middleware with Fastify's setErrorHandler(). Fastify catches async errors automatically — no try/catch wrappers needed in route handlers. [src2, src4]
// BEFORE: Express error middleware
app.use((err, req, res, next) => {
console.error(err.stack);
if (err.name === 'ValidationError') {
return res.status(400).json({ error: err.message });
}
res.status(err.statusCode || 500).json({ error: 'Internal server error' });
});
// AFTER: Fastify error handler
fastify.setErrorHandler((error, request, reply) => {
request.log.error(error);
if (error.validation) {
return reply.code(400).send({
error: 'Validation Error',
details: error.validation
});
}
const statusCode = error.statusCode || 500;
reply.code(statusCode).send({
error: statusCode >= 500 ? 'Internal server error' : error.message
});
});
Verify: Throw an error in any route handler — Fastify catches it automatically and passes it to setErrorHandler(). No next(err) pattern needed.
7. Remove Express and clean up
Once all routes, middleware, and error handling are migrated, uninstall Express and related packages. [src5]
npm uninstall express cors helmet compression morgan express-validator body-parser
# Also remove the @fastify/express bridge
npm uninstall @fastify/express
Verify: grep -rn "require('express')" --include='*.js' --include='*.ts' → zero results. npm ls express → not found. Run full test suite.
Code Examples
JavaScript: Complete Express-to-Fastify server migration
Full script: javascript-complete-express-to-fastify-server-migr.js (44 lines)
// Input: An Express server with routes, middleware, and error handling
// Output: Equivalent Fastify server with plugins and JSON Schema validation
const Fastify = require('fastify');
// Create Fastify instance (replaces const app = express())
const fastify = Fastify({
# ... (see full script)
TypeScript: Fastify route plugin with full type safety
Full script: typescript-fastify-route-plugin-with-full-type-saf.ts (76 lines)
// Input: Express route file with TypeScript
// Output: Fastify plugin with typed schema, request, and reply
import { FastifyPluginAsync } from 'fastify';
interface UserParams {
id: string;
# ... (see full script)
JavaScript: Hooks as middleware replacement
Full script: javascript-hooks-as-middleware-replacement.js (34 lines)
// Input: Express middleware chain for auth + rate limiting
// Output: Fastify hooks and decorators achieving the same flow
const fp = require('fastify-plugin');
// Authentication plugin (replaces app.use(authMiddleware))
const authPlugin = fp(async function (fastify, opts) {
# ... (see full script)
Anti-Patterns
Wrong: Using Express-style callback error handling
// BAD — wrapping every route handler in try/catch like Express
fastify.get('/api/users/:id', async (request, reply) => {
try {
const user = await db.findUser(request.params.id);
if (!user) {
reply.code(404).send({ error: 'Not found' });
return;
}
reply.send(user);
} catch (err) {
reply.code(500).send({ error: 'Internal server error' });
}
});
Correct: Let Fastify handle errors automatically
// GOOD — Fastify catches rejected promises and routes them to setErrorHandler
fastify.get('/api/users/:id', async (request, reply) => {
const user = await db.findUser(request.params.id);
if (!user) {
reply.code(404);
return { error: 'Not found' };
}
return user; // auto-serialized, no reply.send() needed
});
Wrong: Registering middleware globally like Express
// BAD — treating Fastify plugins like Express middleware
fastify.register(require('@fastify/cors'));
fastify.register(require('@fastify/auth'));
// Every route gets auth, even public ones
fastify.get('/api/health', async () => ({ status: 'ok' }));
fastify.get('/api/protected', async (request) => ({ user: request.user }));
Correct: Use plugin encapsulation for scoped middleware
// GOOD — encapsulate auth to only the routes that need it
fastify.register(require('@fastify/cors')); // global: fine
// Public routes
fastify.get('/api/health', async () => ({ status: 'ok' }));
// Protected routes in their own scope
fastify.register(async function (fastify) {
fastify.addHook('preHandler', fastify.authenticate);
fastify.get('/api/protected', async (request) => ({ user: request.user }));
});
Wrong: Skipping JSON Schema validation
// BAD — manual validation like in Express, missing Fastify's core advantage
fastify.post('/api/users', async (request, reply) => {
const { email, name } = request.body;
if (!email || !email.includes('@')) {
reply.code(400);
return { error: 'Invalid email' };
}
if (!name || name.length < 2) {
reply.code(400);
return { error: 'Name too short' };
}
return await db.createUser({ email, name });
});
Correct: Declare JSON Schema for validation and serialization
// GOOD — schema validated before handler runs, response serialized faster
fastify.post('/api/users', {
schema: {
body: {
type: 'object',
required: ['email', 'name'],
properties: {
email: { type: 'string', format: 'email' },
name: { type: 'string', minLength: 2 }
}
}
}
}, async (request, reply) => {
reply.code(201);
return await db.createUser(request.body); // body is guaranteed valid
});
Wrong: Mixing reply.send() with return values
// BAD — calling reply.send() AND returning a value causes FST_ERR_PROMISE_NOT_FULFILLED
fastify.get('/api/data', async (request, reply) => {
const data = await fetchData();
reply.send(data);
return data; // double response — Fastify throws an error
});
Correct: Use either return OR reply.send(), never both
// GOOD — return data for automatic serialization
fastify.get('/api/data', async (request, reply) => {
const data = await fetchData();
return data;
});
// Also valid: explicit reply.send() without returning
fastify.get('/api/data', async (request, reply) => {
const data = await fetchData();
reply.send(data);
});
Wrong: Using old logger and listen() syntax from Fastify v4
// BAD — these patterns break in Fastify v5
const pino = require('pino')();
const fastify = Fastify({ logger: pino }); // v5: logger no longer accepts instances
fastify.listen(3000, '0.0.0.0', callback); // v5: variadic listen() removed
const time = reply.getResponseTime(); // v5: removed
Correct: Use v5 API for logger, listen, and response time
// GOOD — Fastify v5 correct syntax
const pino = require('pino')();
const fastify = Fastify({ loggerInstance: pino }); // v5: use loggerInstance
await fastify.listen({ port: 3000, host: '0.0.0.0' }); // v5: options object only
const time = reply.elapsedTime; // v5: replaces getResponseTime()
Common Pitfalls
- Plugin encapsulation confusion: Fastify plugins create isolated scopes. A decorator or hook registered inside a plugin is NOT available to sibling or parent plugins. Fix: Use
fastify-plugin(fp()) wrapper to break encapsulation when you need shared decorators. [src1, src2] - Forgetting that body parsing is built-in: Installing
body-parseror callingfastify.use(express.json())is unnecessary and can cause double-parsing. Fix: Remove body-parser — Fastify parsesapplication/jsonandtext/plainby default. [src1] - Using
reply.send()withreturnin async handlers: This triggersFST_ERR_PROMISE_NOT_FULFILLEDbecause Fastify tries to send the response twice. Fix: Usereturn datafor auto-serialization, orreply.send(data)without returning. Never both. [src1] - Not converting
listen()call syntax: Express'sapp.listen(3000, callback)won't work. Fastify v5 removed variadic arguments. Fix: Useawait fastify.listen({ port: 3000, host: '0.0.0.0' })with the options object. [src6] - Expecting middleware execution order like Express: Express middleware runs in registration order for ALL routes. Fastify hooks run only within their encapsulated scope. Fix: Register hooks in the correct plugin scope, or use
fastify-pluginfor global hooks. [src2] - Missing the
typeproperty in JSON Schema: Fastify v5 requires full JSON Schema — every schema must includetype: 'object'. Omitting it causes validation to silently pass everything. Fix: Always includetypeat the top level of every schema definition. [src6] - Not handling the
@fastify/expressbridge removal: The bridge adds overhead. Leaving it in production after migration defeats the performance gains. Fix: Remove@fastify/expressandnpm uninstall expressonce all routes are converted. [src4, src5] - Ignoring Fastify's lifecycle hooks: Express has a simple request → middleware → response flow. Fastify has
onRequest → preParsing → preValidation → preHandler → handler → preSerialization → onSend → onResponse. Fix: Map your middleware logic to the correct hook — auth goes inpreHandler, logging inonRequest, response transforms inpreSerialization. [src1] - Using
loggeroption with a custom Pino instance in v5: Fastify v5 no longer accepts a custom logger via theloggeroption. Fix: UseloggerInstanceinstead —Fastify({ loggerInstance: pinoInstance }). [src6] - Using
reply.redirect(code, url)in v5: The argument order reversed in v5 — it is nowreply.redirect(url, code). Fix: Swap the arguments or use the 1-arg formreply.redirect(url)for 302. [src6]
Diagnostic Commands
# Verify Fastify version
node -e "console.log(require('fastify/package.json').version)"
# Check for remaining Express imports in the codebase
grep -rn "require('express')\|from 'express'" --include='*.js' --include='*.ts' --include='*.mjs'
# Verify no Express dependencies remain
npm ls express body-parser cors helmet morgan express-validator
# Check Fastify plugin compatibility
npx fastify-print-routes # prints all registered routes and their schemas
# Test JSON Schema validation
curl -X POST -H 'Content-Type: application/json' -d '{"bad":"data"}' http://localhost:3000/api/users
# Should return 400 with validation errors
# Benchmark before and after migration
npx autocannon -c 100 -d 10 http://localhost:3000/api/users
# Check Fastify server health and loaded plugins
curl http://localhost:3000/health
# Verify Node.js version meets Fastify v5 minimum
node -e "const [major] = process.versions.node.split('.'); console.log(major >= 20 ? 'OK: Node ' + process.version : 'FAIL: Fastify v5 requires Node 20+');"
# Check for deprecated Fastify v4 patterns
grep -rn "getResponseTime\|reply\.redirect([0-9]\|logger:.*require('pino')" --include='*.js' --include='*.ts'
Version History & Compatibility
| Version | Status | Breaking Changes | Migration Notes |
|---|---|---|---|
| Fastify 5.8.5 (Apr 2026) | Current | Full JSON Schema required; listen() requires options object; reply.redirect(url, code) reversed args; loggerInstance replaces logger for custom loggers; request.connection removed (use request.socket); semicolon query delimiters off by default; parameters object loses prototype chain | Patches CVE-2026-33806 (5.8.5, port parsing), CVE-2026-3635 (5.8.3), CVE-2026-3419 (5.8.1, content-type validation bypass), CVE-2026-25224 (5.7.3). v5.8.0 added first-class handler-level timeouts. Upgrade from v4: fix all deprecation warnings first, update schemas to include type [src9] |
| Fastify 5.7.x (Feb 2026) | Maintenance | Stricter RFC 9110 content-type header parsing (5.7.2) — non-standard headers may now be rejected | Bump to 5.8.5 for the latest security patches [src9] |
| Fastify 5.0 (Sep 2024) | Stable | 20+ breaking changes — see V5 Migration Guide. Minimum Node.js 20. Non-standard HTTP methods removed | Major release via OpenJS Foundation. Performance 5-10% faster than v4 [src8] |
| Fastify 4.x (2022–2025) | EOL (June 30, 2025) | reply.send() no longer returns promise; content type parser changes | Was LTS; most migration guides still reference v4 patterns |
| Fastify 3.x (2020–2022) | EOL | Middleware removed from core (requires @fastify/express or @fastify/middie) | Last version with built-in middleware support |
| Express 5.x (2024) | Current | req.host → req.hostname; regex route changes; removed several deprecated methods | If on Express 5, migration patterns to Fastify are similar |
| Express 4.x (2014–present) | Maintenance | Minimal — stable for years | Source framework for migration; no active feature development |
When to Use / When Not to Use
| Use When | Don't Use When | Use Instead |
|---|---|---|
| API-heavy app needing >10K req/sec throughput | Simple static file server with minimal routing | Express or serve-static |
| You want built-in validation, serialization, logging | Rapid prototype where Express knowledge saves time | Stay with Express |
| TypeScript-first codebase needing type-safe routes | App relies heavily on Express-only middleware (e.g., Passport strategies without Fastify adapter) | Express + specific middleware |
| Microservices architecture with plugin encapsulation | Team is unfamiliar with Fastify and has no time to learn | Keep Express, optimize later |
| Need native OpenAPI/Swagger spec generation | Serverless functions (Cloudflare Workers, Vercel Edge) | Hono or framework-agnostic handlers |
| Moving to Node.js 20+/22+ and want modern framework | Stuck on Node.js 18 or earlier in production | Stay on Express or use Fastify v4 (EOL) |
Important Caveats
- Fastify v5 requires Node.js 20+. If your production environment runs Node 18, stay on Fastify v4.x (but note: v4 went EOL June 30, 2025 — plan your Node upgrade). [src6]
- The
@fastify/expressbridge does NOT support HTTP/2. If you need HTTP/2, migrate all routes to native Fastify before enabling it. [src4] - Fastify benchmarks show ~3-5x throughput vs Express (≈70K–80K req/sec vs ≈20K–30K req/sec in 2026), but real-world gains depend on your I/O patterns. Database-bound apps may see 20-40% improvement rather than 5x. [src7, src10]
- JSON Schema validation runs via Ajv. If your Express app uses Joi or Yup schemas, you must rewrite them as JSON Schema — there is no automated converter.
- Plugin encapsulation means
fastify.decorate()inside a child plugin is NOT visible to sibling plugins. Usefastify-pluginwrapper to share decorators across the app. [src1] - Stay current on patches: Fastify 5.8.5 (Apr 2026) fixes CVE-2026-33806 (port parsing), and 5.8.x earlier patched CVE-2026-3635 (5.8.3) and CVE-2026-3419 (5.8.1, a content-type validation bypass via missing end-anchor in
subtypeNameReg). Run at least 5.8.5 in production. [src9] - The
@fastify/expressbridge is a temporary tool only (current v4.0.6, requires Fastify ^5.x). It does not support async middleware and is not a long-term solution — remove it once all routes are native Fastify. [src9] - Fastify v5 removed support for non-standard HTTP methods (PROPFIND, TRACE, SEARCH, etc.). If your Express app handles WebDAV or similar, you will need a workaround. [src6]
- Fastify v5 query string parsing no longer supports semicolon delimiters by default (per RFC 3986). If your Express app uses semicolons in query strings, set
useSemicolonDelimiter: true. [src6]