应用结构 #
一、项目目录结构 #
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)
十一、总结 #
本章我们学习了:
- 目录结构:标准和最小目录结构
- 入口文件:app.js和server.js分离
- 路由组织:自动加载、前缀、Schema
- 插件组织:数据库、认证等插件
- 配置管理:环境变量、配置文件
- 服务层:业务逻辑分离
- 错误处理:自定义错误、全局处理
- 测试组织:测试助手、路由测试
接下来让我们深入学习路由系统!
最后更新:2026-03-28