Fastify

Setup & Basic Server

import Fastify from 'fastify'

const app = Fastify({ logger: true })

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

app.listen({ port: 3000, host: '0.0.0.0' })

Routes

app.get('/users', getUsers)
app.post('/users', createUser)
app.put('/users/:id', updateUser)
app.delete('/users/:id', deleteUser)
app.patch('/users/:id', patchUser)
app.all('/health', healthHandler)

app.register(async (instance) => {
  instance.get('/inner', innerHandler)
}, { prefix: '/api' })

Path Parameters & Query

app.get('/users/:id', async (request) => {
  const { id } = request.params
  return { id }
})

app.get('/search', async (request) => {
  const { q, page, limit } = request.query
  return { q, page: page || 1, limit: limit || 10 }
})

app.get('/files/*', async (request) => {
  const { '*': wildcard } = request.params
  return { path: wildcard }
})

Request Body & Validation

app.post('/users', {
  handler: async (request, reply) => {
    return request.body
  },
  schema: {
    body: {
      type: 'object',
      required: ['name', 'email'],
      properties: {
        name: { type: 'string', minLength: 2 },
        email: { type: 'string', format: 'email' },
        age: { type: 'integer', minimum: 0 }
      }
    }
  }
}, async (request, reply) => {})

JSON Schema Validation

const schema = {
  params: {
    type: 'object',
    properties: { id: { type: 'integer' } }
  },
  querystring: {
    type: 'object',
    properties: {
      page: { type: 'integer', default: 1 },
      limit: { type: 'integer', default: 20 }
    }
  },
  headers: {
    type: 'object',
    required: ['authorization'],
    properties: {
      authorization: { type: 'string' }
    }
  },
  response: {
    200: {
      type: 'object',
      properties: {
        id: { type: 'integer' },
        name: { type: 'string' }
      }
    }
  }
}

Hooks

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

app.addHook('preHandler', async (request, reply) => {
  if (!request.headers.authorization) {
    reply.code(401).send({ error: 'Unauthorized' })
  }
})

app.addHook('onResponse', async (request, reply) => {
  const elapsed = Date.now() - request.startTime
  app.log.info({ elapsed })
})

app.addHook('onSend', async (request, reply, payload) => {
  return payload
})

app.addHook('onError', async (request, reply, error) => {
  app.log.error(error)
})

Plugins

import fp from 'fastify-plugin'

const dbPlugin = fp(async (app, options) => {
  const db = await connectDB(options.url)
  app.decorate('db', db)
  app.addHook('onClose', async () => db.close())
})

app.register(dbPlugin, { url: 'mongodb://localhost:27017' })

app.register(import('@fastify/swagger'), {
  swagger: { title: 'API', version: '1.0.0' }
})

app.register(import('@fastify/cors'), { origin: true })

Middleware

app.register(import('@fastify/rate-limit'), {
  max: 100,
  timeWindow: '1 minute'
})

app.register(import('@fastify/helmet'))

app.register(import('@fastify/compress'))

app.register(import('@fastify/csrf-protection'))

app.addHook('onRequest', async (request, reply) => {
  reply.header('X-Custom', 'value')
})

Error Handling

app.setErrorHandler((error, request, reply) => {
  if (error.validation) {
    reply.code(400).send({ error: error.message, details: error.validation })
    return
  }
  if (error.statusCode === 404) {
    reply.code(404).send({ error: 'Not found' })
    return
  }
  reply.code(error.statusCode || 500).send({ error: 'Internal error' })
})

app.setNotFoundHandler((request, reply) => {
  reply.code(404).send({ error: 'Route not found' })
})

throw app.httpErrors.unauthorized()
throw app.httpErrors.notFound()
throw app.httpErrors.badRequest('invalid input')

Authentication (JWT)

import jwt from '@fastify/jwt'

app.register(jwt, { secret: 'supersecret' })

app.post('/login', async (request, reply) => {
  const token = app.jwt.sign({ id: 1, role: 'admin' })
  return { token }
})

app.get('/protected', {
  preHandler: [app.authenticate]
}, async (request) => {
  return request.user
})

app.decorate('authenticate', async (request, reply) => {
  try {
    await request.jwtVerify()
  } catch (err) {
    reply.code(401).send({ error: 'Invalid token' })
  }
})

File Upload

import multipart from '@fastify/multipart'

app.register(multipart, { limits: { fileSize: 10 * 1024 * 1024 } })

app.post('/upload', async (request, reply) => {
  const data = await request.file()
  const buffer = await data.toBuffer()
  await fs.writeFile(`./uploads/${data.filename}`, buffer)
  return { filename: data.filename, mimetype: data.mimetype }
})

app.post('/multi-upload', async (request, reply) => {
  const files = request.files()
  const results = []
  for await (const data of files) {
    results.push(data.filename)
  }
  return { files: results }
})

Static Files

import fastifyStatic from '@fastify/static'
import path from 'path'

app.register(fastifyStatic, {
  root: path.join(import.meta.dirname, 'public'),
  prefix: '/assets/',
  index: ['index.html']
})

app.get('/download', async (request, reply) => {
  return reply.download('./files/report.pdf', 'report.pdf')
})

Logging

const app = Fastify({
  logger: {
    level: 'info',
    transport: {
      target: 'pino-pretty',
      options: { colorize: true }
    }
  }
})

app.log.info('server started')
app.log.warn({ event: 'slow_query' }, 'query took 5s')
app.log.error({ err }, 'request failed')

app.addHook('onResponse', async (request, reply) => {
  request.log.info({ statusCode: reply.statusCode }, 'response sent')
})

Testing

import { test } from 'node:test'
import assert from 'node:assert'
import build from './app.js'

test('GET /', async (t) => {
  const app = build()

  t.after(() => app.close())

  const response = await app.inject({
    method: 'GET',
    url: '/'
  })

  assert.equal(response.statusCode, 200)
  assert.deepEqual(response.json(), { hello: 'world' })
})

test('POST /users', async (t) => {
  const app = build()
  t.after(() => app.close())

  const response = await app.inject({
    method: 'POST',
    url: '/users',
    payload: { name: 'Alice', email: 'alice@test.com' }
  })

  assert.equal(response.statusCode, 200)
})

Decorators

app.decorate('db', null)
app.decorate('getUser', function (id) {
  return this.db.findUser(id)
})

app.decorateRequest('user', null)
app.addHook('preHandler', async (request) => {
  if (request.headers.authorization) {
    request.user = await verifyToken(request.headers.authorization)
  }
})

app.decorateReply('success', function (data) {
  return this.code(200).send({ success: true, data })
})

if (!app.hasDecorator('db')) {
  app.decorate('db', createConnection())
}

初始化与基本服务

import Fastify from 'fastify'

const app = Fastify({ logger: true })

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

app.listen({ port: 3000, host: '0.0.0.0' })

路由

app.get('/users', getUsers)
app.post('/users', createUser)
app.put('/users/:id', updateUser)
app.delete('/users/:id', deleteUser)
app.patch('/users/:id', patchUser)
app.all('/health', healthHandler)

app.register(async (instance) => {
  instance.get('/inner', innerHandler)
}, { prefix: '/api' })

路径参数与查询

app.get('/users/:id', async (request) => {
  const { id } = request.params
  return { id }
})

app.get('/search', async (request) => {
  const { q, page, limit } = request.query
  return { q, page: page || 1, limit: limit || 10 }
})

app.get('/files/*', async (request) => {
  const { '*': wildcard } = request.params
  return { path: wildcard }
})

请求体与验证

app.post('/users', {
  handler: async (request, reply) => {
    return request.body
  },
  schema: {
    body: {
      type: 'object',
      required: ['name', 'email'],
      properties: {
        name: { type: 'string', minLength: 2 },
        email: { type: 'string', format: 'email' },
        age: { type: 'integer', minimum: 0 }
      }
    }
  }
}, async (request, reply) => {})

JSON Schema 验证

const schema = {
  params: {
    type: 'object',
    properties: { id: { type: 'integer' } }
  },
  querystring: {
    type: 'object',
    properties: {
      page: { type: 'integer', default: 1 },
      limit: { type: 'integer', default: 20 }
    }
  },
  headers: {
    type: 'object',
    required: ['authorization'],
    properties: {
      authorization: { type: 'string' }
    }
  },
  response: {
    200: {
      type: 'object',
      properties: {
        id: { type: 'integer' },
        name: { type: 'string' }
      }
    }
  }
}

钩子

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

app.addHook('preHandler', async (request, reply) => {
  if (!request.headers.authorization) {
    reply.code(401).send({ error: 'Unauthorized' })
  }
})

app.addHook('onResponse', async (request, reply) => {
  const elapsed = Date.now() - request.startTime
  app.log.info({ elapsed })
})

app.addHook('onSend', async (request, reply, payload) => {
  return payload
})

app.addHook('onError', async (request, reply, error) => {
  app.log.error(error)
})

插件

import fp from 'fastify-plugin'

const dbPlugin = fp(async (app, options) => {
  const db = await connectDB(options.url)
  app.decorate('db', db)
  app.addHook('onClose', async () => db.close())
})

app.register(dbPlugin, { url: 'mongodb://localhost:27017' })

app.register(import('@fastify/swagger'), {
  swagger: { title: 'API', version: '1.0.0' }
})

app.register(import('@fastify/cors'), { origin: true })

中间件

app.register(import('@fastify/rate-limit'), {
  max: 100,
  timeWindow: '1 minute'
})

app.register(import('@fastify/helmet'))

app.register(import('@fastify/compress'))

app.register(import('@fastify/csrf-protection'))

app.addHook('onRequest', async (request, reply) => {
  reply.header('X-Custom', 'value')
})

错误处理

app.setErrorHandler((error, request, reply) => {
  if (error.validation) {
    reply.code(400).send({ error: error.message, details: error.validation })
    return
  }
  if (error.statusCode === 404) {
    reply.code(404).send({ error: 'Not found' })
    return
  }
  reply.code(error.statusCode || 500).send({ error: 'Internal error' })
})

app.setNotFoundHandler((request, reply) => {
  reply.code(404).send({ error: 'Route not found' })
})

throw app.httpErrors.unauthorized()
throw app.httpErrors.notFound()
throw app.httpErrors.badRequest('invalid input')

JWT 认证

import jwt from '@fastify/jwt'

app.register(jwt, { secret: 'supersecret' })

app.post('/login', async (request, reply) => {
  const token = app.jwt.sign({ id: 1, role: 'admin' })
  return { token }
})

app.get('/protected', {
  preHandler: [app.authenticate]
}, async (request) => {
  return request.user
})

app.decorate('authenticate', async (request, reply) => {
  try {
    await request.jwtVerify()
  } catch (err) {
    reply.code(401).send({ error: 'Invalid token' })
  }
})

文件上传

import multipart from '@fastify/multipart'

app.register(multipart, { limits: { fileSize: 10 * 1024 * 1024 } })

app.post('/upload', async (request, reply) => {
  const data = await request.file()
  const buffer = await data.toBuffer()
  await fs.writeFile(`./uploads/${data.filename}`, buffer)
  return { filename: data.filename, mimetype: data.mimetype }
})

app.post('/multi-upload', async (request, reply) => {
  const files = request.files()
  const results = []
  for await (const data of files) {
    results.push(data.filename)
  }
  return { files: results }
})

静态文件

import fastifyStatic from '@fastify/static'
import path from 'path'

app.register(fastifyStatic, {
  root: path.join(import.meta.dirname, 'public'),
  prefix: '/assets/',
  index: ['index.html']
})

app.get('/download', async (request, reply) => {
  return reply.download('./files/report.pdf', 'report.pdf')
})

日志

const app = Fastify({
  logger: {
    level: 'info',
    transport: {
      target: 'pino-pretty',
      options: { colorize: true }
    }
  }
})

app.log.info('服务已启动')
app.log.warn({ event: 'slow_query' }, '查询耗时 5s')
app.log.error({ err }, '请求失败')

app.addHook('onResponse', async (request, reply) => {
  request.log.info({ statusCode: reply.statusCode }, '已发送响应')
})

测试

import { test } from 'node:test'
import assert from 'node:assert'
import build from './app.js'

test('GET /', async (t) => {
  const app = build()

  t.after(() => app.close())

  const response = await app.inject({
    method: 'GET',
    url: '/'
  })

  assert.equal(response.statusCode, 200)
  assert.deepEqual(response.json(), { hello: 'world' })
})

test('POST /users', async (t) => {
  const app = build()
  t.after(() => app.close())

  const response = await app.inject({
    method: 'POST',
    url: '/users',
    payload: { name: 'Alice', email: 'alice@test.com' }
  })

  assert.equal(response.statusCode, 200)
})

装饰器

app.decorate('db', null)
app.decorate('getUser', function (id) {
  return this.db.findUser(id)
})

app.decorateRequest('user', null)
app.addHook('preHandler', async (request) => {
  if (request.headers.authorization) {
    request.user = await verifyToken(request.headers.authorization)
  }
})

app.decorateReply('success', function (data) {
  return this.code(200).send({ success: true, data })
})

if (!app.hasDecorator('db')) {
  app.decorate('db', createConnection())
}