RESTful API #
一、项目概述 #
本章将构建一个完整的用户管理RESTful API。
1.1 项目结构 #
text
user-api/
├── src/
│ ├── app.js
│ ├── server.js
│ ├── config/
│ │ └── index.js
│ ├── plugins/
│ │ ├── database.js
│ │ └── auth.js
│ ├── routes/
│ │ ├── index.js
│ │ └── users.js
│ ├── schemas/
│ │ └── user.js
│ ├── services/
│ │ └── user.service.js
│ └── utils/
│ └── errors.js
├── test/
│ └── users.test.js
├── package.json
└── .env
1.2 API设计 #
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/users | 获取用户列表 |
| GET | /api/users/:id | 获取单个用户 |
| POST | /api/users | 创建用户 |
| PUT | /api/users/:id | 更新用户 |
| DELETE | /api/users/:id | 删除用户 |
二、项目初始化 #
2.1 安装依赖 #
bash
mkdir user-api && cd user-api
npm init -y
npm install fastify @fastify/mongodb @fastify/cors @fastify/helmet dotenv
npm install -D nodemon
2.2 package.json #
json
{
"name": "user-api",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node src/server.js",
"dev": "nodemon src/server.js",
"test": "node --test"
},
"dependencies": {
"fastify": "^4.26.0",
"@fastify/mongodb": "^8.0.0",
"@fastify/cors": "^9.0.0",
"@fastify/helmet": "^11.0.0",
"dotenv": "^16.4.0"
},
"devDependencies": {
"nodemon": "^3.0.0"
}
}
2.3 环境变量 #
.env:
env
NODE_ENV=development
PORT=3000
HOST=0.0.0.0
LOG_LEVEL=debug
MONGODB_URL=mongodb://localhost:27017
MONGODB_DB=user_api
三、配置文件 #
3.1 配置模块 #
src/config/index.js:
javascript
import dotenv from 'dotenv'
dotenv.config()
export default {
env: process.env.NODE_ENV || 'development',
port: parseInt(process.env.PORT, 10) || 3000,
host: process.env.HOST || '0.0.0.0',
logLevel: process.env.LOG_LEVEL || 'info',
mongodb: {
url: process.env.MONGODB_URL || 'mongodb://localhost:27017',
database: process.env.MONGODB_DB || 'user_api'
}
}
四、插件开发 #
4.1 数据库插件 #
src/plugins/database.js:
javascript
import fp from 'fastify-plugin'
async function databasePlugin(fastify, opts) {
await fastify.register(import('@fastify/mongodb'), {
url: `${opts.url}/${opts.database}`
})
fastify.decorate('db', fastify.mongo.db)
fastify.addHook('onClose', async (instance) => {
await instance.mongo.client.close()
})
}
export default fp(databasePlugin, {
name: 'database'
})
4.2 安全插件 #
src/plugins/security.js:
javascript
import fp from 'fastify-plugin'
import cors from '@fastify/cors'
import helmet from '@fastify/helmet'
async function securityPlugin(fastify, opts) {
await fastify.register(cors, {
origin: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']
})
await fastify.register(helmet)
}
export default fp(securityPlugin, {
name: 'security'
})
五、Schema定义 #
5.1 用户Schema #
src/schemas/user.js:
javascript
const userSchema = {
$id: 'user',
type: 'object',
properties: {
_id: { type: 'string' },
name: { type: 'string' },
email: { type: 'string', format: 'email' },
age: { type: 'integer', minimum: 0 },
createdAt: { type: 'string', format: 'date-time' },
updatedAt: { type: 'string', format: 'date-time' }
}
}
const createUserSchema = {
$id: 'createUser',
type: 'object',
required: ['name', 'email'],
properties: {
name: { type: 'string', minLength: 2, maxLength: 100 },
email: { type: 'string', format: 'email' },
age: { type: 'integer', minimum: 0, maximum: 150 }
}
}
const updateUserSchema = {
$id: 'updateUser',
type: 'object',
properties: {
name: { type: 'string', minLength: 2, maxLength: 100 },
email: { type: 'string', format: 'email' },
age: { type: 'integer', minimum: 0, maximum: 150 }
}
}
const userListSchema = {
$id: 'userList',
type: 'object',
properties: {
users: {
type: 'array',
items: { $ref: 'user#' }
},
pagination: {
type: 'object',
properties: {
page: { type: 'integer' },
limit: { type: 'integer' },
total: { type: 'integer' },
totalPages: { type: 'integer' }
}
}
}
}
export {
userSchema,
createUserSchema,
updateUserSchema,
userListSchema
}
六、服务层 #
6.1 用户服务 #
src/services/user.service.js:
javascript
class UserService {
constructor(db) {
this.collection = db.collection('users')
}
async findAll(query = {}, options = {}) {
const { page = 1, limit = 10 } = options
const skip = (page - 1) * limit
const [users, total] = await Promise.all([
this.collection.find(query).skip(skip).limit(limit).toArray(),
this.collection.countDocuments(query)
])
return {
users,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit)
}
}
}
async findById(id) {
const { ObjectId } = await import('mongodb')
return this.collection.findOne({ _id: new ObjectId(id) })
}
async findByEmail(email) {
return this.collection.findOne({ email })
}
async create(data) {
const user = {
...data,
createdAt: new Date(),
updatedAt: new Date()
}
const result = await this.collection.insertOne(user)
return this.findById(result.insertedId)
}
async update(id, data) {
const { ObjectId } = await import('mongodb')
await this.collection.updateOne(
{ _id: new ObjectId(id) },
{
$set: {
...data,
updatedAt: new Date()
}
}
)
return this.findById(id)
}
async delete(id) {
const { ObjectId } = await import('mongodb')
const result = await this.collection.deleteOne({
_id: new ObjectId(id)
})
return result.deletedCount > 0
}
}
export default UserService
七、路由开发 #
7.1 用户路由 #
src/routes/users.js:
javascript
import { ObjectId } from 'mongodb'
export default async function (fastify, opts) {
const { userService } = fastify.services
fastify.get('/', {
schema: {
querystring: {
type: 'object',
properties: {
page: { type: 'integer', minimum: 1, default: 1 },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
search: { type: 'string' }
}
},
response: {
200: { $ref: 'userList#' }
}
}
}, async (request, reply) => {
const { page, limit, search } = request.query
const query = search ? {
$or: [
{ name: { $regex: search, $options: 'i' } },
{ email: { $regex: search, $options: 'i' } }
]
} : {}
return userService.findAll(query, { page, limit })
})
fastify.get('/:id', {
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' }
}
},
response: {
200: { $ref: 'user#' },
404: {
type: 'object',
properties: {
error: { type: 'string' },
message: { type: 'string' }
}
}
}
}
}, async (request, reply) => {
const user = await userService.findById(request.params.id)
if (!user) {
reply.code(404)
return { error: 'Not Found', message: 'User not found' }
}
return user
})
fastify.post('/', {
schema: {
body: { $ref: 'createUser#' },
response: {
201: { $ref: 'user#' },
409: {
type: 'object',
properties: {
error: { type: 'string' },
message: { type: 'string' }
}
}
}
}
}, async (request, reply) => {
const existing = await userService.findByEmail(request.body.email)
if (existing) {
reply.code(409)
return { error: 'Conflict', message: 'Email already exists' }
}
const user = await userService.create(request.body)
reply.code(201)
return user
})
fastify.put('/:id', {
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' }
}
},
body: { $ref: 'updateUser#' },
response: {
200: { $ref: 'user#' },
404: {
type: 'object',
properties: {
error: { type: 'string' },
message: { type: 'string' }
}
}
}
}
}, async (request, reply) => {
const existing = await userService.findById(request.params.id)
if (!existing) {
reply.code(404)
return { error: 'Not Found', message: 'User not found' }
}
if (request.body.email && request.body.email !== existing.email) {
const emailExists = await userService.findByEmail(request.body.email)
if (emailExists) {
reply.code(409)
return { error: 'Conflict', message: 'Email already exists' }
}
}
return userService.update(request.params.id, request.body)
})
fastify.delete('/:id', {
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' }
}
},
response: {
204: { type: 'null' },
404: {
type: 'object',
properties: {
error: { type: 'string' },
message: { type: 'string' }
}
}
}
}
}, async (request, reply) => {
const deleted = await userService.delete(request.params.id)
if (!deleted) {
reply.code(404)
return { error: 'Not Found', message: 'User not found' }
}
reply.code(204).send()
})
}
7.2 路由索引 #
src/routes/index.js:
javascript
export default async function (fastify, opts) {
fastify.register(import('./users.js'), { prefix: '/api/users' })
}
八、应用入口 #
8.1 应用配置 #
src/app.js:
javascript
import fp from 'fastify-plugin'
import config from './config/index.js'
import databasePlugin from './plugins/database.js'
import securityPlugin from './plugins/security.js'
import UserService from './services/user.service.js'
import {
userSchema,
createUserSchema,
updateUserSchema,
userListSchema
} from './schemas/user.js'
async function app(fastify, opts) {
fastify.addSchema(userSchema)
fastify.addSchema(createUserSchema)
fastify.addSchema(updateUserSchema)
fastify.addSchema(userListSchema)
await fastify.register(databasePlugin, config.mongodb)
await fastify.register(securityPlugin)
fastify.decorate('services', {
userService: new UserService(fastify.db)
})
fastify.register(import('./routes/index.js'))
}
export default fp(app)
8.2 服务器启动 #
src/server.js:
javascript
import Fastify from 'fastify'
import config from './config/index.js'
import app from './app.js'
const fastify = Fastify({
logger: {
level: config.logLevel,
transport: config.env === 'development'
? { target: 'pino-pretty' }
: undefined
}
})
fastify.register(app)
const start = async () => {
try {
await fastify.listen({
port: config.port,
host: config.host
})
console.log(`Server running at http://${config.host}:${config.port}`)
} catch (err) {
fastify.log.error(err)
process.exit(1)
}
}
start()
九、测试 #
9.1 API测试 #
test/users.test.js:
javascript
import { test } from 'node:test'
import assert from 'node:assert'
import Fastify from 'fastify'
import app from '../src/app.js'
test('GET /api/users', async (t) => {
const fastify = Fastify()
await fastify.register(app)
const response = await fastify.inject({
method: 'GET',
url: '/api/users'
})
assert.strictEqual(response.statusCode, 200)
const body = JSON.parse(response.payload)
assert.ok(Array.isArray(body.users))
await fastify.close()
})
test('POST /api/users', async (t) => {
const fastify = Fastify()
await fastify.register(app)
const response = await fastify.inject({
method: 'POST',
url: '/api/users',
payload: {
name: 'Test User',
email: 'test@example.com'
}
})
assert.strictEqual(response.statusCode, 201)
const body = JSON.parse(response.payload)
assert.strictEqual(body.name, 'Test User')
await fastify.close()
})
十、运行项目 #
10.1 启动服务 #
bash
npm run dev
10.2 测试API #
bash
curl http://localhost:3000/api/users
curl -X POST -H "Content-Type: application/json" \
-d '{"name":"John","email":"john@example.com"}' \
http://localhost:3000/api/users
curl http://localhost:3000/api/users/USER_ID
curl -X PUT -H "Content-Type: application/json" \
-d '{"name":"John Updated"}' \
http://localhost:3000/api/users/USER_ID
curl -X DELETE http://localhost:3000/api/users/USER_ID
十一、总结 #
本章我们学习了:
- 项目结构:合理的目录组织
- 配置管理:环境变量和配置文件
- 插件开发:数据库和安全插件
- Schema定义:请求和响应验证
- 服务层:业务逻辑封装
- 路由开发:RESTful API实现
- 测试:API测试编写
接下来让我们学习用户认证系统!
最后更新:2026-03-28