Skip to main content
Advertisement

13.3 Fastify + TypeScript — Schema-Based Type Inference

Introduction to Fastify

Fastify is a Node.js web framework much faster than Express, featuring JSON schema-based type inference. Its TypeScript integration is excellent.

npm install fastify
npm install --save-dev @types/node typescript

Basic Setup

// src/index.ts
import Fastify from 'fastify'

const fastify = Fastify({
logger: true, // Built-in Pino logger
})

// Register plugins
await fastify.register(import('./plugins/auth'))
await fastify.register(import('./routes/user'), { prefix: '/api/users' })

await fastify.listen({ port: 3000 })

Schema-Based Type Inference

Fastify automatically infers TypeScript types when you define request/response types with JSON Schema.

import { FastifyPluginAsync } from 'fastify'

interface UserParams {
id: string
}

interface CreateUserBody {
name: string
email: string
age: number
}

interface UserResponse {
id: string
name: string
email: string
createdAt: string
}

const userRoutes: FastifyPluginAsync = async (fastify) => {
// Use JSON Schema + TypeScript types simultaneously
fastify.get<{
Params: UserParams
Reply: UserResponse
}>(
'/:id',
{
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' },
},
required: ['id'],
},
response: {
200: {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
email: { type: 'string' },
createdAt: { type: 'string', format: 'date-time' },
},
},
},
},
},
async (request, reply) => {
const { id } = request.params // UserParams type automatically applied
const user = await getUserById(id)
return reply.send(user)
}
)

fastify.post<{
Body: CreateUserBody
Reply: { 201: UserResponse }
}>(
'/',
{
schema: {
body: {
type: 'object',
properties: {
name: { type: 'string', minLength: 1 },
email: { type: 'string', format: 'email' },
age: { type: 'number', minimum: 0 },
},
required: ['name', 'email', 'age'],
},
},
},
async (request, reply) => {
const { name, email, age } = request.body // CreateUserBody type
const user = await createUser({ name, email, age })
return reply.status(201).send(user)
}
)
}

export default userRoutes

Unified Types and Schemas with TypeBox

TypeBox generates JSON Schema and TypeScript types simultaneously.

npm install @sinclair/typebox
import { Type, Static } from '@sinclair/typebox'
import { FastifyPluginAsync } from 'fastify'

// Define Schema and TypeScript types at the same time
const CreateUserSchema = Type.Object({
name: Type.String({ minLength: 1, maxLength: 100 }),
email: Type.String({ format: 'email' }),
age: Type.Integer({ minimum: 0, maximum: 150 }),
role: Type.Optional(Type.Union([
Type.Literal('admin'),
Type.Literal('user'),
])),
})

const UserResponseSchema = Type.Object({
id: Type.String(),
name: Type.String(),
email: Type.String(),
age: Type.Integer(),
role: Type.String(),
createdAt: Type.String({ format: 'date-time' }),
})

// Extract TypeScript types
type CreateUserInput = Static<typeof CreateUserSchema>
type UserResponse = Static<typeof UserResponseSchema>

const userRoutes: FastifyPluginAsync = async (fastify) => {
fastify.post<{
Body: CreateUserInput
Reply: UserResponse
}>(
'/',
{
schema: {
body: CreateUserSchema,
response: { 201: UserResponseSchema },
},
},
async (request, reply) => {
// request.body: CreateUserInput (type-safe)
const user = await createUser(request.body)
return reply.status(201).send(user)
}
)
}

Plugin Type System

Fastify plugins use fastify-plugin to decorate instances.

// plugins/auth.ts
import fp from 'fastify-plugin'
import { FastifyPluginAsync, FastifyRequest } from 'fastify'
import jwt from '@fastify/jwt'

// Declare types added by the plugin
declare module '@fastify/jwt' {
interface FastifyJWT {
payload: { userId: string; role: string }
user: { id: string; email: string; role: string }
}
}

const authPlugin: FastifyPluginAsync = async (fastify) => {
// Register JWT plugin
await fastify.register(jwt, {
secret: process.env.JWT_SECRET!,
})

// Add custom decorator
fastify.decorate('authenticate', async (request: FastifyRequest) => {
try {
await request.jwtVerify()
} catch (err) {
throw fastify.httpErrors.unauthorized('Authentication required.')
}
})
}

// Wrapping with fp() extends the plugin scope to parent
export default fp(authPlugin)

// Extend Fastify instance type
declare module 'fastify' {
interface FastifyInstance {
authenticate: (request: FastifyRequest) => Promise<void>
}
}

Protecting Routes with preHandler

fastify.get(
'/protected',
{
preHandler: [fastify.authenticate], // Authentication check
},
async (request, reply) => {
// request.user: { id, email, role } (type-safe)
return { user: request.user }
}
)

Pro Tips

1. Error handling

fastify.setErrorHandler((error, request, reply) => {
if (error.validation) {
return reply.status(422).send({
error: 'Validation failed',
details: error.validation,
})
}

fastify.log.error(error)
return reply.status(500).send({ error: 'Server error' })
})

2. Serialization performance

// fast-json-stringify built-in — 3x faster serialization when response schema defined
{
schema: {
response: {
200: UserResponseSchema // Auto-optimized when schema present
}
}
}
Advertisement