---
# === IDENTITY ===
id: software/migrations/express-to-fastify/2026
canonical_question: "How do I migrate from Express.js to Fastify?"
aliases:
  - "Express to Fastify migration"
  - "convert Express to Fastify"
  - "replace Express with Fastify"
  - "Express.js to Fastify guide"
  - "migrate Express middleware to Fastify plugins"
  - "Express to Fastify route migration"
entity_type: software_reference
domain: software > migrations > express_to_fastify
region: global
jurisdiction: global
temporal_scope: 2024-2026

# === VERIFICATION ===
last_verified: 2026-05-29
confidence: 0.90
version: 2.1
first_published: 2026-02-17

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: evolving
  last_breaking_change: "Fastify 5.0 (2024-09)"
  next_review: 2026-11-25
  change_sensitivity: medium

# === CONSTRAINTS ===
constraints:
  - "Fastify v5 requires Node.js 20+ — do not attempt migration if production runs Node 18 or earlier"
  - "Never use reply.send() AND return a value in the same async handler — causes FST_ERR_PROMISE_NOT_FULFILLED"
  - "The @fastify/express bridge does NOT support HTTP/2 — migrate all routes to native Fastify before enabling HTTP/2"
  - "Every JSON Schema must include the top-level type property (e.g., type: 'object') — Fastify v5 silently passes validation without it"
  - "Do not install body-parser — Fastify parses application/json and text/plain by default; double-parsing causes subtle bugs"
  - "Plugin encapsulation is NOT middleware — a decorator or hook inside a child plugin is invisible to siblings; use fastify-plugin (fp()) to break encapsulation intentionally"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "User wants to migrate to NestJS (wants opinionated framework with DI)"
    use_instead: "software/migrations/express-to-nestjs/2026"
  - condition: "User wants to migrate to Hono for a serverless/edge target (Cloudflare Workers, Vercel Edge)"
    use_instead: "Use a framework-agnostic edge handler or Hono — Fastify is Node.js-runtime oriented and not the right target for edge"
  - condition: "User is upgrading between Fastify versions (v4 to v5), not migrating from Express"
    use_instead: "Follow the official Fastify V5 Migration Guide at https://fastify.dev/docs/latest/Guides/Migration-Guide-V5/"

# === AGENT HINTS ===
inputs_needed:
  - key: codebase_size
    question: "How large is the Express codebase?"
    type: choice
    options: ["Small (<20 routes, <5 middleware)", "Medium (20-100 routes)", "Large (>100 routes, complex middleware)"]
  - key: migration_timeline
    question: "Can you freeze feature development during migration?"
    type: choice
    options: ["Yes — full rewrite ok", "No — must migrate incrementally"]
  - key: express_specific_deps
    question: "Does the app use Express-only middleware without Fastify equivalents (e.g., Passport strategies, custom 4-arg error middleware)?"
    type: choice
    options: ["Yes — must bridge them", "No — all have Fastify equivalents", "Not sure"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/migrations/express-to-fastify/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-05-29)"

# === RELATED UNITS ===
related_kos:
  related_to:
    - id: "software/migrations/javascript-to-typescript/2026"
      label: "JavaScript to TypeScript Migration"
    - id: "software/migrations/rest-to-graphql/2026"
      label: "REST to GraphQL Migration"
    - id: "software/migrations/mongoose-to-prisma/2026"
      label: "Mongoose to Prisma Migration"
  alternative_to:
    - id: "software/migrations/express-to-nestjs/2026"
      label: "Express to NestJS Migration (opinionated DI)"
  often_confused_with:
    - id: "software/migrations/express-to-nestjs/2026"
      label: "Express to NestJS Migration (NestJS can run on a Fastify adapter)"

# === SOURCES (10 authoritative sources) ===
# Types: official_docs, technical_blog, rfc_spec, academic_paper, community_resource, industry_report
# Reliability: high, moderate_high, moderate, moderate_low, low, authoritative
sources:
  - id: src1
    title: "Fastify Documentation — Getting Started"
    author: Fastify
    url: https://fastify.dev/docs/latest/
    type: official_docs
    published: 2026-05-01
    reliability: authoritative
  - id: src2
    title: "Migrating from Express to Fastify: A Complete Guide"
    author: Better Stack
    url: https://betterstack.com/community/guides/scaling-nodejs/migrating-from-express-to-fastify/
    type: technical_blog
    published: 2025-06-15
    reliability: high
  - id: src3
    title: "How to Migrate Your App from Express to Fastify"
    author: SitePoint
    url: https://www.sitepoint.com/express-to-fastify-migrate/
    type: technical_blog
    published: 2024-09-10
    reliability: high
  - id: src4
    title: "Migrate Your Express Application to Fastify"
    author: AppSignal
    url: https://blog.appsignal.com/2023/06/28/migrate-your-express-application-to-fastify.html
    type: technical_blog
    published: 2023-06-28
    reliability: high
  - id: src5
    title: "Moving from Express to Fastify"
    author: Val Town
    url: https://blog.val.town/blog/fastify/
    type: technical_blog
    published: 2025-03-15
    reliability: high
  - id: src6
    title: "V5 Migration Guide"
    author: Fastify
    url: https://fastify.dev/docs/latest/Guides/Migration-Guide-V5/
    type: official_docs
    published: 2024-09-01
    reliability: authoritative
  - id: src7
    title: "Fastify Benchmarks"
    author: Fastify
    url: https://fastify.dev/benchmarks/
    type: official_docs
    published: 2026-01-01
    reliability: authoritative
  - id: src8
    title: "Fastify v5 is Now Officially Released"
    author: OpenJS Foundation
    url: https://openjsf.org/blog/fastifys-growth-and-success
    type: community_resource
    published: 2024-09-15
    reliability: high
  - id: src9
    title: "Releases — fastify/fastify (5.7.x–5.8.x security fixes)"
    author: Fastify
    url: https://github.com/fastify/fastify/releases
    type: official_docs
    published: 2026-04-14
    reliability: authoritative
  - id: src10
    title: "Express vs Fastify: Node.js Framework Choice 2026"
    author: PkgPulse
    url: https://www.pkgpulse.com/guides/express-vs-fastify-2026
    type: technical_blog
    published: 2026-03-01
    reliability: moderate_high
---

# 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/express` as 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/express` then `await 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/express` v4.0.6 bridge. [src1, src9]

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- 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()` AND `return` a value in the same async handler — this causes `FST_ERR_PROMISE_NOT_FULFILLED`. Pick one: `return data` or `reply.send(data)`. [src1]
- The `@fastify/express` bridge does NOT support HTTP/2 — migrate all routes to native Fastify before enabling HTTP/2. [src4]
- Every JSON Schema must include the top-level `type` property (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 parses `application/json` and `text/plain` by 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]

```bash
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]

```javascript
// 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]

```javascript
// 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]

```bash
npm install @fastify/cors @fastify/helmet @fastify/compress @fastify/cookie @fastify/session
```

```javascript
// 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]

```javascript
// 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]

```javascript
// 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]

```bash
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](scripts/javascript-complete-express-to-fastify-server-migr.js) (44 lines)

```javascript
// 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](scripts/typescript-fastify-route-plugin-with-full-type-saf.ts) (76 lines)

```typescript
// 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](scripts/javascript-hooks-as-middleware-replacement.js) (34 lines)

```javascript
// 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

```javascript
// 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

```javascript
// 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

```javascript
// 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

```javascript
// 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

```javascript
// 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

```javascript
// 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

```javascript
// 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

```javascript
// 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

```javascript
// 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

```javascript
// 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-parser` or calling `fastify.use(express.json())` is unnecessary and can cause double-parsing. Fix: Remove body-parser — Fastify parses `application/json` and `text/plain` by default. [src1]
- **Using `reply.send()` with `return` in async handlers**: This triggers `FST_ERR_PROMISE_NOT_FULFILLED` because Fastify tries to send the response twice. Fix: Use `return data` for auto-serialization, or `reply.send(data)` without returning. Never both. [src1]
- **Not converting `listen()` call syntax**: Express's `app.listen(3000, callback)` won't work. Fastify v5 removed variadic arguments. Fix: Use `await 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-plugin` for global hooks. [src2]
- **Missing the `type` property in JSON Schema**: Fastify v5 requires full JSON Schema — every schema must include `type: 'object'`. Omitting it causes validation to silently pass everything. Fix: Always include `type` at the top level of every schema definition. [src6]
- **Not handling the `@fastify/express` bridge removal**: The bridge adds overhead. Leaving it in production after migration defeats the performance gains. Fix: Remove `@fastify/express` and `npm uninstall express` once 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 in `preHandler`, logging in `onRequest`, response transforms in `preSerialization`. [src1]
- **Using `logger` option with a custom Pino instance in v5**: Fastify v5 no longer accepts a custom logger via the `logger` option. Fix: Use `loggerInstance` instead — `Fastify({ loggerInstance: pinoInstance })`. [src6]
- **Using `reply.redirect(code, url)` in v5**: The argument order reversed in v5 — it is now `reply.redirect(url, code)`. Fix: Swap the arguments or use the 1-arg form `reply.redirect(url)` for 302. [src6]

## Diagnostic Commands

```bash
# 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/express` bridge 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. Use `fastify-plugin` wrapper 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/express` bridge 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]

## Related Units
<!-- Generated from related_kos frontmatter -->

- [JavaScript to TypeScript Migration](/software/migrations/javascript-to-typescript/2026)
- [REST to GraphQL Migration](/software/migrations/rest-to-graphql/2026)
- [Mongoose to Prisma Migration](/software/migrations/mongoose-to-prisma/2026)
- [Express to NestJS Migration (opinionated DI)](/software/migrations/express-to-nestjs/2026)
