应用结构 #

一、项目目录结构 #

1.1 标准目录结构 #

text
my-fastify-app/
├── src/
│   ├── app.js              ├── server.js             ├── config/
│   │   ├── index.js        │   ├── development.js    │   ├── production.js      │   └── test.js            ├── plugins/
│   │   ├── database.js     │   ├── redis.js          │   ├── auth.js            │   └── logger.js          ├── routes/
│   │   ├── index.js        │   ├── api/
│   │   │   ├── v1/
│   │   │   │   ├── index.js
│   │   │   │   ├── users.js
│   │   │   │   └── products.js
│   │   └── web/
│   │       ├── index.js
│   │       └── pages.js
│   ├── schemas/
│   │   ├── user.js         │   └── product.js        ├── services/
│   │   ├── user.service.js │   └── product.service.js├── models/
│   │   ├── user.model.js   │   └── product.model.js  ├── utils/
│   │   ├── helpers.js      │   ├── errors.js         │   └── constants.js       └── middleware/
│       ├── auth.js         └── validation.js         ├── test/
│   ├── app.test.js         ├── routes/
│   │   └── users.test.js   └── helpers/
│       └── test-helper.js  ├── .env
├── .env.example
├── package.json
└── README.md

1.2 最小目录结构 #

text
my-fastify-app/
├── app.js
├── routes/
│   ├── index.js
│   └── users.js
├── plugins/
│   └── support.js
├── test/
│   └── app.test.js
├── package.json
└── .env

二、入口文件组织 #

2.1 app.js - 应用配置 #

javascript
const path = require('path')
const autoLoad = require('@fastify/autoload')

async function app(fastify, opts) {
  fastify.register(autoLoad, {
    dir: path.join(__dirname, 'plugins'),
    options: opts
  })

  fastify.register(autoLoad, {
    dir: path.join(__dirname, 'routes'),
    options: opts
  })
}

module.exports = app
module.exports.options = {
  logger: {
    level: process.env.LOG_LEVEL || 'info'
  }
}

2.2 server.js - 服务器启动 #

javascript
require('dotenv').config()

const Fastify = require('fastify')
const app = require('./app')

const fastify = Fastify({
  logger: {
    level: process.env.LOG_LEVEL || 'info',
    transport: process.env.NODE_ENV === 'development' 
      ? { target: 'pino-pretty' }
      : undefined
  }
})

fastify.register(app)

const start = async () => {
  try {
    await fastify.ready()
    await fastify.listen({
      port: process.env.PORT || 3000,
      host: process.env.HOST || '0.0.0.0'
    })
    console.log(`Server listening on ${fastify.server.address().port}`)
  } catch (err) {
    fastify.log.error(err)
    process.exit(1)
  }
}

start()

三、路由组织 #

3.1 路由自动加载 #

使用 @fastify/autoload 自动加载路由:

javascript
const path = require('path')
const autoLoad = require('@fastify/autoload')

const fastify = require('fastify')()

fastify.register(autoLoad, {
  dir: path.join(__dirname, 'routes')
})

3.2 路由文件结构 #

routes/index.js

javascript
module.exports = async function (fastify, opts) {
  fastify.get('/', async (request, reply) => {
    return { message: 'Welcome to API' }
  })
}

routes/users.js

javascript
module.exports = async function (fastify, opts) {
  fastify.get('/users', async (request, reply) => {
    return { users: [] }
  })

  fastify.post('/users', async (request, reply) => {
    return { created: true }
  })
}

3.3 路由前缀 #

routes/api/v1/index.js

javascript
module.exports = async function (fastify, opts) {
  fastify.register(require('./users'), { prefix: '/users' })
  fastify.register(require('./products'), { prefix: '/products' })
}

routes/api/v1/users.js

javascript
module.exports = async function (fastify, opts) {
  fastify.get('/', async (request, reply) => {
    return { users: [] }
  })

  fastify.get('/:id', async (request, reply) => {
    return { id: request.params.id }
  })
}

3.4 带Schema的路由 #

routes/users.js

javascript
const userSchema = require('../schemas/user')

module.exports = async function (fastify, opts) {
  fastify.get('/users', {
    schema: {
      response: {
        200: userSchema.list
      }
    }
  }, async (request, reply) => {
    return { users: [] }
  })

  fastify.post('/users', {
    schema: {
      body: userSchema.create,
      response: {
        201: userSchema.item
      }
    }
  }, async (request, reply) => {
    reply.code(201)
    return request.body
  })
}

四、插件组织 #

4.1 插件目录结构 #

text
plugins/
├── database.js
├── redis.js
├── auth.js
├── cors.js
└── sensible.js

4.2 数据库插件 #

plugins/database.js

javascript
const fp = require('fastify-plugin')
const { MongoClient } = require('mongodb')

async function databasePlugin(fastify, opts) {
  const client = new MongoClient(opts.url)
  
  await client.connect()
  
  const db = client.db(opts.dbName)
  
  fastify.decorate('db', db)
  fastify.decorate('mongo', client)
  
  fastify.addHook('onClose', async (instance) => {
    await client.close()
  })
}

module.exports = fp(databasePlugin, {
  name: 'database'
})

4.3 认证插件 #

plugins/auth.js

javascript
const fp = require('fastify-plugin')
const jwt = require('@fastify/jwt')

async function authPlugin(fastify, opts) {
  fastify.register(jwt, {
    secret: opts.secret
  })

  fastify.decorate('authenticate', async function (request, reply) {
    try {
      await request.jwtVerify()
    } catch (err) {
      reply.code(401).send({ error: 'Unauthorized' })
    }
  })
}

module.exports = fp(authPlugin, {
  name: 'auth'
})

4.4 插件自动加载 #

app.js

javascript
const path = require('path')
const autoLoad = require('@fastify/autoload')

async function app(fastify, opts) {
  fastify.register(autoLoad, {
    dir: path.join(__dirname, 'plugins'),
    ignorePattern: /.*\.test\.js$/,
    options: {
      database: {
        url: process.env.MONGODB_URL,
        dbName: process.env.MONGODB_DB
      },
      auth: {
        secret: process.env.JWT_SECRET
      }
    }
  })
}

module.exports = app

五、Schema组织 #

5.1 Schema文件结构 #

schemas/user.js

javascript
const userBase = {
  type: 'object',
  properties: {
    id: { type: 'integer' },
    name: { type: 'string' },
    email: { type: 'string', format: 'email' },
    createdAt: { type: 'string', format: 'date-time' }
  }
}

const createBody = {
  type: 'object',
  required: ['name', 'email'],
  properties: {
    name: { type: 'string', minLength: 2, maxLength: 100 },
    email: { type: 'string', format: 'email' },
    password: { type: 'string', minLength: 8 }
  }
}

const updateBody = {
  type: 'object',
  properties: {
    name: { type: 'string', minLength: 2, maxLength: 100 },
    email: { type: 'string', format: 'email' }
  }
}

module.exports = {
  base: userBase,
  item: {
    ...userBase,
    required: ['id', 'name', 'email']
  },
  list: {
    type: 'object',
    properties: {
      users: {
        type: 'array',
        items: userBase
      },
      total: { type: 'integer' }
    }
  },
  create: createBody,
  update: updateBody,
  params: {
    type: 'object',
    properties: {
      id: { type: 'integer' }
    }
  }
}

5.2 共享Schema #

schemas/index.js

javascript
const userSchema = require('./user')
const productSchema = require('./product')

module.exports = {
  user: userSchema,
  product: productSchema
}

app.js

javascript
const schemas = require('./schemas')

async function app(fastify, opts) {
  for (const [name, schema] of Object.entries(schemas)) {
    fastify.addSchema({ ...schema.base, $id: name })
  }
}

六、配置管理 #

6.1 环境变量 #

.env

env
NODE_ENV=development
PORT=3000
HOST=0.0.0.0
LOG_LEVEL=debug

MONGODB_URL=mongodb://localhost:27017
MONGODB_DB=myapp

JWT_SECRET=your-secret-key
JWT_EXPIRES_IN=7d

REDIS_URL=redis://localhost:6379

6.2 配置文件 #

config/index.js

javascript
const env = process.env.NODE_ENV || 'development'

const config = {
  development: {
    port: 3000,
    logLevel: 'debug',
    database: {
      url: 'mongodb://localhost:27017',
      dbName: 'myapp_dev'
    }
  },
  production: {
    port: process.env.PORT || 3000,
    logLevel: 'info',
    database: {
      url: process.env.MONGODB_URL,
      dbName: process.env.MONGODB_DB
    }
  },
  test: {
    port: 0,
    logLevel: 'error',
    database: {
      url: 'mongodb://localhost:27017',
      dbName: 'myapp_test'
    }
  }
}

module.exports = config[env]

6.3 使用配置 #

javascript
const config = require('./config')

const fastify = require('fastify')({
  logger: {
    level: config.logLevel
  }
})

fastify.register(require('./plugins/database'), config.database)

七、服务层组织 #

7.1 服务文件 #

services/user.service.js

javascript
class UserService {
  constructor(fastify) {
    this.fastify = fastify
    this.collection = fastify.db.collection('users')
  }

  async findAll(query = {}) {
    return this.collection.find(query).toArray()
  }

  async findById(id) {
    return this.collection.findOne({ _id: id })
  }

  async create(data) {
    const result = await this.collection.insertOne({
      ...data,
      createdAt: new Date()
    })
    return this.findById(result.insertedId)
  }

  async update(id, data) {
    await this.collection.updateOne(
      { _id: id },
      { $set: { ...data, updatedAt: new Date() } }
    )
    return this.findById(id)
  }

  async delete(id) {
    const result = await this.collection.deleteOne({ _id: id })
    return result.deletedCount > 0
  }
}

module.exports = UserService

7.2 服务插件 #

plugins/services.js

javascript
const fp = require('fastify-plugin')
const UserService = require('../services/user.service')
const ProductService = require('../services/product.service')

async function servicesPlugin(fastify, opts) {
  fastify.decorate('services', {
    user: new UserService(fastify),
    product: new ProductService(fastify)
  })
}

module.exports = fp(servicesPlugin, {
  name: 'services',
  dependencies: ['database']
})

7.3 在路由中使用 #

javascript
module.exports = async function (fastify, opts) {
  fastify.get('/users', async (request, reply) => {
    const users = await fastify.services.user.findAll()
    return { users }
  })

  fastify.post('/users', async (request, reply) => {
    const user = await fastify.services.user.create(request.body)
    reply.code(201)
    return user
  })
}

八、错误处理组织 #

8.1 自定义错误 #

utils/errors.js

javascript
class AppError extends Error {
  constructor(message, statusCode = 500) {
    super(message)
    this.statusCode = statusCode
    this.isOperational = true
    Error.captureStackTrace(this, this.constructor)
  }
}

class NotFoundError extends AppError {
  constructor(message = 'Resource not found') {
    super(message, 404)
  }
}

class ValidationError extends AppError {
  constructor(message = 'Validation failed') {
    super(message, 400)
  }
}

class UnauthorizedError extends AppError {
  constructor(message = 'Unauthorized') {
    super(message, 401)
  }
}

class ForbiddenError extends AppError {
  constructor(message = 'Forbidden') {
    super(message, 403)
  }
}

module.exports = {
  AppError,
  NotFoundError,
  ValidationError,
  UnauthorizedError,
  ForbiddenError
}

8.2 全局错误处理器 #

plugins/error-handler.js

javascript
const fp = require('fastify-plugin')
const { AppError } = require('../utils/errors')

async function errorHandlerPlugin(fastify, opts) {
  fastify.setErrorHandler((error, request, reply) => {
    if (error.validation) {
      reply.code(400).send({
        statusCode: 400,
        error: 'Validation Error',
        message: error.message,
        details: error.validation
      })
      return
    }

    if (error instanceof AppError) {
      reply.code(error.statusCode).send({
        statusCode: error.statusCode,
        error: error.name,
        message: error.message
      })
      return
    }

    fastify.log.error(error)
    
    reply.code(500).send({
      statusCode: 500,
      error: 'Internal Server Error',
      message: process.env.NODE_ENV === 'production' 
        ? 'Something went wrong'
        : error.message
    })
  })
}

module.exports = fp(errorHandlerPlugin, {
  name: 'error-handler'
})

九、测试组织 #

9.1 测试文件结构 #

text
test/
├── app.test.js
├── routes/
│   ├── users.test.js
│   └── products.test.js
├── plugins/
│   └── database.test.js
└── helpers/
    └── test-helper.js

9.2 测试助手 #

test/helpers/test-helper.js

javascript
const Fastify = require('fastify')
const app = require('../../src/app')

async function build(t) {
  const fastify = Fastify()
  await fastify.register(app, {
    database: {
      url: 'mongodb://localhost:27017',
      dbName: 'test_db'
    }
  })
  
  await fastify.ready()
  
  t.after(async () => {
    await fastify.close()
  })
  
  return fastify
}

module.exports = { build }

9.3 路由测试 #

test/routes/users.test.js

javascript
const { test } = require('node:test')
const assert = require('node:assert')
const { build } = require('../helpers/test-helper')

test('GET /users', async (t) => {
  const fastify = await build(t)
  
  const response = await fastify.inject({
    method: 'GET',
    url: '/users'
  })
  
  assert.strictEqual(response.statusCode, 200)
  assert.ok(Array.isArray(JSON.parse(response.payload).users))
})

test('POST /users', async (t) => {
  const fastify = await build(t)
  
  const response = await fastify.inject({
    method: 'POST',
    url: '/users',
    payload: {
      name: 'Test User',
      email: 'test@example.com'
    }
  })
  
  assert.strictEqual(response.statusCode, 201)
})

十、最佳实践 #

10.1 项目结构原则 #

  • 关注点分离:路由、服务、模型分离
  • 模块化:使用插件组织功能
  • 可测试:便于编写单元测试
  • 可扩展:易于添加新功能

10.2 命名规范 #

类型 命名规范 示例
文件 kebab-case user-service.js
PascalCase UserService
函数 camelCase findById
常量 UPPER_SNAKE_CASE MAX_RETRIES
路由 kebab-case /user-profiles

10.3 代码组织建议 #

javascript
// 1. 导入依赖
const fp = require('fastify-plugin')
const { MongoClient } = require('mongodb')

// 2. 定义常量
const DEFAULT_OPTIONS = {
  poolSize: 10
}

// 3. 定义类/函数
async function databasePlugin(fastify, opts) {
  // 实现
}

// 4. 导出
module.exports = fp(databasePlugin)

十一、总结 #

本章我们学习了:

  1. 目录结构:标准和最小目录结构
  2. 入口文件:app.js和server.js分离
  3. 路由组织:自动加载、前缀、Schema
  4. 插件组织:数据库、认证等插件
  5. 配置管理:环境变量、配置文件
  6. 服务层:业务逻辑分离
  7. 错误处理:自定义错误、全局处理
  8. 测试组织:测试助手、路由测试

接下来让我们深入学习路由系统!

最后更新:2026-03-28