| |

Node.js 35 🟢 Introduction to Fastify Framework Architecture

Fastify is a web framework for Node.js that was engineered around a single principle: add as little overhead as possible on top of the runtime. Where Express builds its abstraction from a middleware chain that grows with every plugin, Fastify builds a graph of encapsulated plugins, each with its own scope, and it compiles the request schema at startup rather than validating at runtime. The framework’s architecture is not just a different syntax; it is a different model for how a web application is composed, and the model is the reason the framework is fast.

Key point: Fastify is built on three architectural pillars: a plugin system that is encapsulated by default, a schema-based validation and serialization layer that compiles at startup, and a decorator API that extends the core objects without mutating their shape at runtime. The plugin system replaces the middleware chain. The schema layer replaces the runtime validation. The decorators replace the ad-hoc property assignment. Together, they produce the performance and the modularity that distinguish Fastify from Express.


Why Fastify exists

The middleware overhead problem. Express builds its request pipeline by chaining middleware functions. Every middleware runs on every request, whether it applies or not, and the chain grows with each plugin. The overhead is small per middleware, but the aggregate is measurable. Fastify replaces the chain with a plugin graph, and the encapsulation means a plugin’s hooks and decorators apply only to the routes inside that plugin’s scope .

The runtime validation problem. Express leaves the request body as an unparsed stream until a middleware reads it, and the validation is the application’s responsibility. Fastify requires a schema for the route, and the schema is compiled at startup. The validation is a single compiled function, not a chain of checks at runtime .

The serialization problem. Express uses JSON.stringify to serialize the response. The function walks the entire object and includes every property. Fastify compiles the response schema with fast-json-stringify, which knows the shape of the response in advance and serializes only the declared fields. The result is two to five times faster serialization and the prevention of the field leaks that JSON.stringify allows .

The encapsulation problem. Express middleware is global unless it is scoped with a path. A middleware registered with app.use() runs for every request. Fastify’s plugins are encapsulated: a plugin’s hooks and decorators apply only to the routes registered inside that plugin. The encapsulation is the default, and the fastify-plugin wrapper is the escape hatch when the global scope is needed .

The plugin ordering problem. Fastify loads the plugins after .listen(), .inject(), or .ready() is called. The load order is deterministic, and the plugins can be nested. The graph is built, and the routes are registered. The result is a predictable initialization sequence that the framework guarantees .

The type safety problem. The schema is the single source of truth. The validation and the serialization both derive from it. The TypeScript integration uses the schema types, and the route handler receives the typed data. The schema is not just documentation; it is the mechanism that enforces the contract .


a. The plugin system and the encapsulation

Fastify’s plugin system is the framework’s organizational model. A plugin is a function that receives the Fastify instance, an options object, and a callback. It registers routes, decorators, hooks, and other plugins on the instance it receives .

const fastify = require('fastify')({ logger: true })

async function plugin (instance, opts) {
  instance.get('/hello', async () => {
    return { hello: 'world' }
  })
}

fastify.register(plugin)

The plugin receives a scoped instance. The routes registered inside the plugin are visible only to the plugin and its descendants. The encapsulation is the default .

The encapsulation works on the ancestor-descendant axis. A decorator added in a plugin is visible to the routes in that plugin and to the plugins registered inside it. It is not visible to the sibling plugins or to the parent . The fastify-plugin wrapper breaks the encapsulation and makes the plugin’s additions available to the parent .

The plugin graph is built in the order of the register calls. The dependencies are resolved by the order. If a plugin needs a decorator from an earlier plugin, the earlier plugin must be registered first .

The plugin can be asynchronous. An async function is the modern form; the callback form with the done parameter is the alternative . The asynchronous plugin can await a database connection or any other initialization before registering the routes.


b. The schema and the validation

Fastify requires a JSON Schema for each route’s input. The schema is declared in the route options, and the framework compiles it at startup with Ajv .

fastify.post('/users', {
  schema: {
    body: {
      type: 'object',
      required: ['email'],
      properties: {
        email: { type: 'string', format: 'email' },
        name: { type: 'string', minLength: 1 }
      }
    },
    querystring: {
      type: 'object',
      properties: {
        page: { type: 'integer', default: 1 }
      }
    },
    params: {
      type: 'object',
      properties: {
        id: { type: 'string', format: 'uuid' }
      }
    }
  },
  handler: async (request, reply) => {
    // request.body, request.query, request.params are validated
  }
})

The schema declares the body, the querystring, the params, and the headers. The validation rejects the invalid requests with a 400 before the handler runs. The handler receives the validated data .

The Ajv configuration has defaults that are worth knowing. The coerceTypes: true option coerces the string values to the numbers or booleans when the schema declares the type. The removeAdditional: true option strips the extra properties that the schema does not declare. The useDefaults: true option applies the default values .

The schema is compiled at startup. The compilation errors are thrown during .listen() or .ready(), not at the request time. The route cannot be registered with the invalid schema .

The response schema is the serialization schema. Fastify compiles it with fast-json-stringify, and the serialization includes only the declared fields.

fastify.get('/user/:id', {
  schema: {
    response: {
      200: {
        type: 'object',
        properties: {
          id: { type: 'string' },
          email: { type: 'string' }
        }
      }
    }
  },
  handler: async (request, reply) => {
    return { id: '1', email: 'a@b.com', password: 'secret' }
    // 'password' is not in the schema, so it is not serialized
  }
})

The password property is not in the response schema, and it is not serialized. The schema prevents the field leak that JSON.stringify would allow .


c. The decorators and the hooks

The decorator API extends the Fastify core objects without mutating their shape at runtime. The decorate method adds a property to the server instance. The decorateRequest and the decorateReply add the properties to the request and reply objects .

fastify.decorate('db', new Database())

fastify.get('/users', async function (request, reply) {
  const users = await this.db.query('SELECT * FROM users')
  return users
})

The fastify.db is available in the route handler through this. The decorator is the dependency injection mechanism .

The decorateRequest and the decorateReply are the mechanism for the per-request and per-reply properties.

fastify.decorateRequest('user', null)

fastify.addHook('preHandler', async (request) => {
  request.user = await authenticate(request)
})

The decorateRequest defines the shape before the request is instantiated. The hook populates the value. The JavaScript engine can optimize the object because the shape is defined upfront .

The hooks are the lifecycle integration points. The onRequest, the preParsing, the preValidation, the preHandler, the preSerialization, the onSend, the onResponse, and the onError are the hooks that fire at the specific points in the request lifecycle . The hooks are the mechanism that replaces the Express middleware, and they are scoped to the plugin .

The onRoute hook is the special hook that fires when a route is registered. It can read and modify the route options, which is the mechanism for the dynamic route customization .


Complete Example Session

// ============================================
// PART 1: BASIC FASTIFY SERVER
// ============================================
const fastify = require('fastify')({ logger: true })

fastify.get('/', async () => {
  return { hello: 'world' }
})

fastify.listen({ port: 3000 })
// ============================================
// PART 2: PLUGIN WITH ENCAPSULATION
// ============================================
async function userPlugin (fastify, opts) {
  fastify.decorate('users', ['alice', 'bob'])

  fastify.get('/users', async function () {
    return { users: this.users }
  })
}

fastify.register(userPlugin)

fastify.get('/other', async function () {
  // this.users is not available here
  return { ok: true }
})
// ============================================
// PART 3: SCHEMA VALIDATION
// ============================================
fastify.post('/users', {
  schema: {
    body: {
      type: 'object',
      required: ['email'],
      properties: {
        email: { type: 'string', format: 'email' },
        name: { type: 'string', minLength: 1 }
      }
    }
  },
  handler: async (request, reply) => {
    return { received: request.body }
  }
})
// ============================================
// PART 4: RESPONSE SCHEMA
// ============================================
fastify.get('/user', {
  schema: {
    response: {
      200: {
        type: 'object',
        properties: {
          id: { type: 'string' },
          name: { type: 'string' }
        }
      }
    }
  },
  handler: async () => {
    return { id: '1', name: 'alice', secret: 'hidden' }
  }
})
// ============================================
// PART 5: DECORATOR
// ============================================
fastify.decorate('config', { db: 'mydb', port: 5432 })

fastify.get('/config', async function () {
  return this.config
})
// ============================================
// PART 6: REQUEST DECORATOR
// ============================================
fastify.decorateRequest('user', null)

fastify.addHook('preHandler', async (request) => {
  request.user = { id: '1', name: 'alice' }
})

fastify.get('/me', async (request) => {
  return request.user
})
// ============================================
// PART 7: HOOK
// ============================================
fastify.addHook('onRequest', async (request, reply) => {
  request.startTime = Date.now()
})

fastify.addHook('onResponse', async (request, reply) => {
  const duration = Date.now() - request.startTime
  fastify.log.info(`Request took ${duration}ms`)
})
// ============================================
// PART 8: ERROR HANDLER
// ============================================
fastify.setErrorHandler((error, request, reply) => {
  reply.status(error.statusCode || 500).send({
    error: error.message
  })
})
// ============================================
// PART 9: ASYNC PLUGIN
// ============================================
async function dbPlugin (fastify, opts) {
  const db = await connectToDatabase()
  fastify.decorate('db', db)

  fastify.addHook('onClose', async () => {
    await db.close()
  })
}

fastify.register(dbPlugin)
// ============================================
// PART 10: FASTIFY-PLUGIN
// ============================================
const fp = require('fastify-plugin')

async function globalPlugin (fastify, opts) {
  fastify.decorate('globalUtil', () => 'available everywhere')
}

module.exports = fp(globalPlugin)

The ten parts covered the basic server, the plugin with encapsulation, the schema validation, the response schema, the decorator, the request decorator, the hook, the error handler, the async plugin, and the fastify-plugin wrapper.


Quick Reference

Core Concepts

ConceptPurpose
PluginEncapsulated scope
RegisterAdd a plugin
DecoratorExtend core objects
SchemaValidate and serialize
HookLifecycle integration
fastify-pluginBreak encapsulation

Schema Types

SchemaValidates
bodyRequest body
querystringQuery string
paramsRoute parameters
headersRequest headers
responseResponse serialization

Decorator Methods

MethodExtends
decorateServer instance
decorateRequestRequest object
decorateReplyReply object

Lifecycle Hooks

HookFires
onRequestOn request arrival
preParsingBefore body parsing
preValidationBefore validation
preHandlerBefore handler
preSerializationBefore serialization
onSendBefore sending
onResponseAfter response
onErrorOn error

Performance Comparison

Frameworkreq/sLatency
Fastify97,5959.73ms
Express59,65116.25ms

Best Practices

✅ Do This:

// Use the schema for every route
fastify.post('/users', {
  schema: { body: { /* ... */ } },
  handler: async () => { }
})                                                              // ✅

// Use the response schema to prevent leaks
schema: { response: { 200: { /* ... */ } } }                    // ✅

// Use the decorators for the dependencies
fastify.decorate('db', new Database())                          // ✅

// Use the encapsulation for the plugin scopes
fastify.register(userPlugin)                                    // ✅

// Use the async plugins for the initialization
async function dbPlugin (fastify) {
  fastify.decorate('db', await connect())
}                                                              // ✅

// Use the hooks for the cross-cutting concerns
fastify.addHook('preHandler', authenticate)                     // ✅

// Use the fastify-plugin for the global utilities
module.exports = fp(globalPlugin)                               // ✅

❌ Don’t Do This:

// Don't mutate the request object without the decorator
fastify.addHook('preHandler', (req) => {
  req.user = 'Bob'  // deoptimizes the object                    // ⚠️
})

// Don't use the global middleware pattern
// Use the hooks and the encapsulation instead                   // ❌

// Don't forget the response schema
// Without it, JSON.stringify is used and the fields leak        // ⚠️

// Don't use the arrow functions when this is needed
fastify.get('/', () => {
  this.db  // undefined                                          // ❌
})

// Don't register the plugins after the listen
// The plugins must be registered before                               // ❌

// Don't use the schema without the required fields
// The missing required arrays allow the incomplete data         // ⚠️

Common Pitfalls

PitfallWhy It HappensFix
this is undefinedArrow functionUse function
Decorator not foundWrong scopeUse fastify-plugin or register at root
Schema not compiledRegistered after listenRegister before
Validation not appliedNo schemaAdd the schema
Field leakedNo response schemaAdd the response schema
Plugin order wrongRegister orderRegister dependencies first

Real-World Examples

1. Basic Server

const fastify = require('fastify')({ logger: true })
fastify.get('/', async () => ({ hello: 'world' }))
fastify.listen({ port: 3000 })

2. Plugin

async function userPlugin (fastify) {
  fastify.get('/users', async () => ({ users: [] }))
}
fastify.register(userPlugin)

3. Schema

fastify.post('/users', {
  schema: { body: { type: 'object', required: ['email'] } },
  handler: async (request) => ({ received: request.body })
})

4. Decorator

fastify.decorate('db', new Database())
fastify.get('/users', async function () {
  return this.db.query('SELECT * FROM users')
})

5. Request Decorator

fastify.decorateRequest('user', null)
fastify.addHook('preHandler', async (request) => {
  request.user = await authenticate(request)
})

6. Hook

fastify.addHook('onRequest', async (request) => {
  request.startTime = Date.now()
})

7. Error Handler

fastify.setErrorHandler((error, request, reply) => {
  reply.status(error.statusCode || 500).send({ error: error.message })
})

8. Async Plugin

async function dbPlugin (fastify) {
  fastify.decorate('db', await connect())
}
fastify.register(dbPlugin)

9. fastify-plugin

const fp = require('fastify-plugin')
module.exports = fp(async function (fastify) {
  fastify.decorate('util', () => 'global')
})

10. Response Schema

fastify.get('/user', {
  schema: { response: { 200: { type: 'object', properties: { id: { type: 'string' } } } } },
  handler: async () => ({ id: '1', secret: 'hidden' })
})

Visual

Plugin Encapsulation

┌─────────────────────────────────────────────────────────────┐
│  ROOT FASTIFY                                               │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  Plugin A (register)                                │    │
│  │  ┌─────────────────────────────────────────────┐    │    │
│  │  │  decorator: utilA                           │    │    │
│  │  │  route: /a                                  │    │    │
│  │  │  └── can access utilA                       │    │    │
│  │  └─────────────────────────────────────────────┘    │    │
│  └─────────────────────────────────────────────────────┘    │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  Plugin B (register)                                │    │
│  │  ┌─────────────────────────────────────────────┐    │    │
│  │  │  route: /b                                  │    │    │
│  │  │  └── cannot access utilA                    │    │    │
│  │  └─────────────────────────────────────────────┘    │    │
│  └─────────────────────────────────────────────────────┘    │
│                                                             │
│  The encapsulation isolates the plugins.                    │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Schema Validation Flow

┌─────────────────────────────────────────────────────────────┐
│  REQUEST                                                    │
│    │                                                        │
│    ▼                                                        │
│  Schema validation (compiled Ajv)                           │
│    │                                                        │
│    ├── Valid ──▶ Handler                                    │
│    │                                                        │
│    └── Invalid ──▶ 400 error                                │
│                                                             │
│  The schema is compiled at startup.                         │
│  The validation is a single function call.                  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Response Serialization

┌─────────────────────────────────────────────────────────────┐
│  HANDLER returns { id, name, secret }                       │
│    │                                                        │
│    ▼                                                        │
│  Response schema: { id, name }                              │
│    │                                                        │
│    ▼                                                        │
│  fast-json-stringify                                        │
│    │                                                        │
│    ▼                                                        │
│  Serialized: { id, name }                                   │
│                                                             │
│  The 'secret' field is not serialized.                      │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Performance Comparison

┌─────────────────────────────────────────────────────────────┐
│  REQUESTS PER SECOND                                        │
│                                                             │
│  Fastify  ████████████████████████████████████████ 97,595   │
│  Express  █████████████████████████ 59,651                  │
│                                                             │
│  Fastify is 1.6x faster than Express.                       │
│                                                             │
│  LATENCY                                                    │
│                                                             │
│  Fastify  ██████████ 9.73ms                                 │
│  Express  █████████████████ 16.25ms                         │
│                                                             │
│  Fastify has 1.7x lower latency.                            │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Summary

ItemValue
FrameworkFastify
Installnpm install fastify
PluginEncapsulated function
Registerfastify.register()
Decoratorfastify.decorate()
Request decoratorfastify.decorateRequest()
SchemaJSON Schema for validation
Response schemaSerialization schema
HookLifecycle integration
fastify-pluginBreak encapsulation
Performance1.6x Express

Key takeaways:

  • Fastify is built on encapsulation. The plugin system replaces the Express middleware chain. A plugin’s routes, decorators, and hooks apply only to the plugin’s scope. The fastify-plugin wrapper is the escape hatch when the global scope is needed .
  • The schema is the contract. Fastify requires a JSON Schema for the route’s input, and the schema is compiled at startup with Ajv. The validation rejects the invalid requests with a 400 before the handler runs .
  • The response schema is the serialization mechanism. Fastify compiles the response schema with fast-json-stringify, which serializes only the declared fields. The schema prevents the field leaks that JSON.stringify allows .
  • The decorators extend the core objects. The decorate adds the properties to the server instance. The decorateRequest and the decorateReply add the properties to the request and reply objects. The decorators are the dependency injection mechanism .
  • The hooks are the lifecycle integration. The onRequest, the preHandler, the onSend, and the rest fire at the specific points in the request lifecycle. The hooks are scoped to the plugin .
  • The performance is the architectural consequence. Fastify is 1.6x faster than Express in the requests per second, and it has the 1.7x lower latency. The performance comes from the plugin graph, the compiled schema, and the fast-json-stringify .
  • The plugin order is deterministic. Fastify loads the plugins after .listen(), .inject(), or .ready() is called. The plugins can be nested, and the load order is the registration order .

Remember: Fastify is not a different syntax for Express. It is a different model for composing a web application. The plugin graph replaces the middleware chain, the schema replaces the runtime validation, and the decorators replace the ad-hoc property assignment. The model is the reason the framework is fast, and the model is the reason the framework is modular. The encapsulation is the default, and the fastify-plugin is the escape hatch. The schema is the contract, and the compiled functions are the implementation. Understand the three pillars—the plugins, the schema, and the decorators—and the rest of Fastify is the details.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!