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
| Concept | Purpose |
|---|---|
| Plugin | Encapsulated scope |
| Register | Add a plugin |
| Decorator | Extend core objects |
| Schema | Validate and serialize |
| Hook | Lifecycle integration |
fastify-plugin | Break encapsulation |
Schema Types
| Schema | Validates |
|---|---|
body | Request body |
querystring | Query string |
params | Route parameters |
headers | Request headers |
response | Response serialization |
Decorator Methods
| Method | Extends |
|---|---|
decorate | Server instance |
decorateRequest | Request object |
decorateReply | Reply object |
Lifecycle Hooks
| Hook | Fires |
|---|---|
onRequest | On request arrival |
preParsing | Before body parsing |
preValidation | Before validation |
preHandler | Before handler |
preSerialization | Before serialization |
onSend | Before sending |
onResponse | After response |
onError | On error |
Performance Comparison
| Framework | req/s | Latency |
|---|---|---|
| Fastify | 97,595 | 9.73ms |
| Express | 59,651 | 16.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
| Pitfall | Why It Happens | Fix |
|---|---|---|
this is undefined | Arrow function | Use function |
| Decorator not found | Wrong scope | Use fastify-plugin or register at root |
| Schema not compiled | Registered after listen | Register before |
| Validation not applied | No schema | Add the schema |
| Field leaked | No response schema | Add the response schema |
| Plugin order wrong | Register order | Register 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
| Item | Value |
|---|---|
| Framework | Fastify |
| Install | npm install fastify |
| Plugin | Encapsulated function |
| Register | fastify.register() |
| Decorator | fastify.decorate() |
| Request decorator | fastify.decorateRequest() |
| Schema | JSON Schema for validation |
| Response schema | Serialization schema |
| Hook | Lifecycle integration |
fastify-plugin | Break encapsulation |
| Performance | 1.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-pluginwrapper 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
400before 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 thatJSON.stringifyallows . - The decorators extend the core objects. The
decorateadds the properties to the server instance. ThedecorateRequestand thedecorateReplyadd the properties to the request and reply objects. The decorators are the dependency injection mechanism . - The hooks are the lifecycle integration. The
onRequest, thepreHandler, theonSend, 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!