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

十一、总结 #

本章我们学习了:

  1. 项目结构:合理的目录组织
  2. 配置管理:环境变量和配置文件
  3. 插件开发:数据库和安全插件
  4. Schema定义:请求和响应验证
  5. 服务层:业务逻辑封装
  6. 路由开发:RESTful API实现
  7. 测试:API测试编写

接下来让我们学习用户认证系统!

最后更新:2026-03-28