首页 / Node.js 教程 / RESTful API 设计与实战

Node.js 教程

RESTful API 设计与实战

本教程共 76 篇 · 第 42 篇 · 更新于 2026-07-25 · 约 6 分钟阅读

Node.jsRESTAPICRUDExpress

42. RESTful API 设计与实战

本节目标:REST 原则、状态码、统一响应结构和完整 CRUD 实现。

REST(Representational State Transfer)不是什么协议,而是一种设计 Web API 的风格。它的核心思想很简单:URL 表示资源,HTTP 方法表示对资源的操作,响应状态码表示结果。听起来像废话?但做到位和随便写,差距非常大。

这节我们先讲设计原则,再手搓一套完整的 CRUD 接口,代码可以直接跑。

REST 的核心原则

1. 资源即名词

URL 里不要用动词。/getUsers/createUser 是 RPC 风格,REST 风格是:

GET    /users       // 获取用户列表
GET    /users/42    // 获取单个用户
POST   /users       // 创建用户
PUT    /users/42    // 完整更新
PATCH  /users/42    // 部分更新
DELETE /users/42    // 删除用户

2. 无状态

服务器不保存客户端的上下文。每次请求必须自带全部信息(比如通过 JWT Token 或 Session ID)。这样横向扩容时随便加机器,不需要考虑会话粘滞。

3. 正确使用状态码

状态码场景
200 OKGET、PUT、PATCH、DELETE 成功
201 CreatedPOST 创建成功
204 No Content删除成功,不返回 body
400 Bad Request参数校验失败
401 Unauthorized未登录
403 Forbidden无权限
404 Not Found资源不存在
500 Internal Server Error服务端异常

4. 返回一致的响应结构

前后端对接最烦的就是「这个接口返回数组,那个接口返回对象,出错时又变成字符串」。建议统一结构:

{
  "code": 0,
  "message": "ok",
  "data": { ... }
}

项目结构

rest-demo/
├── app.js
├── routes/
│   └── users.js
├── controllers/
│   └── userController.js
└── package.json

实际代码量不大,拆分是为了演示真实项目的组织方式。你可以把逻辑都塞一个文件,但千万别养成这个习惯。

完整 CRUD 代码

app.js

import express from 'express'
import userRoutes from './routes/users.js'

const app = express()
const port = 3000

app.use(express.json())

// 统一响应封装
app.use((req, res, next) => {
  res.sendSuccess = (data, message = 'ok') => {
    res.json({ code: 0, message, data })
  }
  res.sendError = (message, statusCode = 500, code = statusCode) => {
    res.status(statusCode).json({ code, message, data: null })
  }
  next()
})

// 路由
app.use('/users', userRoutes)

// 404
app.use((req, res) => {
  res.sendError('Not Found', 404)
})

// 错误处理(Express 5 自动捕获异步错误)
app.use((err, req, res, next) => {
  console.error(err)
  res.sendError(err.message || 'Internal Server Error', err.status || 500)
})

app.listen(port, () => {
  console.log(`API server at http://localhost:${port}`)
})

controllers/userController.js

// 内存存储,实际项目换成数据库
let users = [
  { id: 1, name: 'Tom', email: 'tom@example.com' },
  { id: 2, name: 'Jerry', email: 'jerry@example.com' }
]
let nextId = 3

export const getUsers = (req, res) => {
  res.sendSuccess(users)
}

export const getUser = (req, res) => {
  const id = Number(req.params.id)
  const user = users.find(u => u.id === id)
  if (!user) {
    return res.sendError('User not found', 404)
  }
  res.sendSuccess(user)
}

export const createUser = (req, res) => {
  const { name, email } = req.body
  if (!name || !email) {
    return res.sendError('Name and email are required', 400)
  }
  const user = { id: nextId++, name, email }
  users.push(user)
  res.status(201).sendSuccess(user, 'Created')
}

export const updateUser = (req, res) => {
  const id = Number(req.params.id)
  const user = users.find(u => u.id === id)
  if (!user) {
    return res.sendError('User not found', 404)
  }
  const { name, email } = req.body
  if (name) user.name = name
  if (email) user.email = email
  res.sendSuccess(user)
}

export const deleteUser = (req, res) => {
  const id = Number(req.params.id)
  const index = users.findIndex(u => u.id === id)
  if (index === -1) {
    return res.sendError('User not found', 404)
  }
  users.splice(index, 1)
  res.status(204).send()
}

routes/users.js

import express from 'express'
import {
  getUsers, getUser, createUser, updateUser, deleteUser
} from '../controllers/userController.js'

const router = express.Router()

router.get('/', getUsers)
router.get('/:id', getUser)
router.post('/', createUser)
router.put('/:id', updateUser)
router.delete('/:id', deleteUser)

export default router

运行:

node app.js

测试接口

curl 或者 Postman 都可以,这里用 curl 演示:

# 获取所有用户
curl http://localhost:3000/users

# 获取单个用户
curl http://localhost:3000/users/1

# 创建用户
curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"name":"Spike","email":"spike@example.com"}'

# 更新用户
curl -X PUT http://localhost:3000/users/1 \
  -H "Content-Type: application/json" \
  -d '{"name":"Tommy"}'

# 删除用户
curl -X DELETE http://localhost:3000/users/2

也可以用 Node.js 内置的 fetch(自 v18 起全局可用)写测试脚本:

const base = 'http://localhost:3000'

async function test() {
  const create = await fetch(`${base}/users`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name: 'Tyke', email: 'tyke@example.com' })
  })
  console.log('POST', await create.json())

  const list = await fetch(`${base}/users`)
  console.log('GET', await list.json())
}

test()

分页与过滤

列表接口默认返回全部数据,量大了会拖垮服务。分页是最基本的优化:

export const getUsers = (req, res) => {
  const page = Number(req.query.page) || 1
  const limit = Number(req.query.limit) || 10
  const keyword = req.query.keyword || ''

  let result = users
  if (keyword) {
    result = result.filter(u => u.name.includes(keyword))
  }

  const start = (page - 1) * limit
  const end = start + limit
  const data = result.slice(start, end)

  res.sendSuccess({
    list: data,
    total: result.length,
    page,
    limit
  })
}

请求 /users?page=2&limit=5&keyword=Tom 就能拿到第二页、每页 5 条、名字包含 Tom 的数据。

Tip

实际项目里分页和过滤应该在数据库层做(SQL 的 LIMIT/OFFSET 或 MongoDB 的 skip/limit),而不是把数据全捞到内存里再过滤。这里用内存数组只是为了演示接口形态。

版本控制

API 一旦上线,就不能随便改字段名或删接口,否则下游会炸。给 URL 加版本号是常见做法:

import v1UserRoutes from './routes/v1/users.js'
import v2UserRoutes from './routes/v2/users.js'

app.use('/api/v1/users', v1UserRoutes)
app.use('/api/v2/users', v2UserRoutes)

v2 可以复用 v1 的控制器,只改需要变化的部分。大版本变更才开新路径,小改动尽量保持兼容。