Appearance
前端开发者的 Express CRUD 接口实战手册
更新: 7/11/2026 字数: 0 字 时长: 0 分钟
你已经会写 Koa、用过 Node、能撸简单接口。这份文档不讲"什么是后端",而是带你把 Express 的整套 CRUD(增删改查)从零搭到能跑、能扛、能交付。所有代码复制就能运行,每一步都标了前端人容易踩的坑。

一、先用一句话理解 Express
Express 干的事就一件:收到一个 HTTP 请求,挑一段函数来处理它,吐回一个响应。
如果你写过前端的请求拦截器,那 Express 的"中间件"你秒懂——它就是一串按顺序排队的函数,请求像传送带上的包裹一样从第一个函数流到最后一个,每个函数都能:
- 看一眼这个请求(读
req) - 给它盖个章、塞点东西(改
req/res) - 决定放行到下一个(调
next()),还是直接拦下返回(调res.send())
请求 → [日志中间件] → [解析body] → [校验] → [业务处理] → 响应
↓ next() ↓ next() ↓ next() ↓ res.json()和 Koa 的区别就一个核心点:Koa 是洋葱模型(进去再出来),Express 是直线流水线(从头走到尾)。Express 里没有 await next() 那种"出来"的概念,next() 只管往后传。这点后面踩坑章节会重点说。
二、环境搭建 + 第一个接口(3 步跑起来)

第 1 步:建项目装依赖
bash
mkdir express-crud && cd express-crud
npm init -y
npm install express sequelize sqlite3 express-validatorexpress:框架本体sequelize+sqlite3:ORM + 一个零配置的本地数据库(SQLite 不用装服务,一个文件就是一个库,学习阶段最省心)express-validator:参数校验(基于 Express 的中间件写法,比手写 if 判断舒服)
第 2 步:开启 ESM(让你能用 import,和前端写法一致)
打开 package.json,加一行:
json
{
"type": "module"
}第 3 步:写第一个接口 app.js
js
import express from 'express';
const app = express();
app.use(express.json()); // 关键:让 Express 能解析 JSON 请求体
app.get('/hello', (req, res) => {
res.json({ msg: '你好,Express 跑起来了' });
});
app.listen(3000, () => {
console.log('服务已启动: http://localhost:3000');
});运行:
bash
node app.js浏览器打开 http://localhost:3000/hello,看到 JSON 就成功了。
前端易踩坑 ①:不写
app.use(express.json()),你的req.body永远是undefined。前端发Content-Type: application/json的 POST 请求,后端必须先"开"这个解析开关才能读到。Koa 里靠koa-bodyparser,Express 4.16+ 直接内置了express.json(),但默认是关的,必须手动 use。
三、路由注册:把请求分发给正确的处理函数

小项目把路由全写在 app.js 里没问题,但接口一多就乱。正规做法是用 express.Router() 把一类资源(比如"用户")的路由单独拆成一个文件,像前端按页面拆组件一样。
新建 routes/users.js:
js
import { Router } from 'express';
const router = Router();
// 这里先写空壳,下一节填业务逻辑
router.get('/', (req, res) => res.json([])); // 查列表
router.get('/:id', (req, res) => res.json({})); // 查单个
router.post('/', (req, res) => res.json({})); // 新增
router.put('/:id', (req, res) => res.json({})); // 更新
router.delete('/:id', (req, res) => res.json({})); // 删除
export default router;在 app.js 里挂载它:
js
import usersRouter from './routes/users.js';
app.use('/users', usersRouter); // 所有 /users 开头的请求都交给这个 router挂载后路径会拼接:router.get('/:id') + 挂载前缀 /users = 实际接口 /users/:id。
REST 风格的对应关系记一下,前端调接口时也是这套:
| 操作 | HTTP 方法 | 路径 | 含义 |
|---|---|---|---|
| 查列表 | GET | /users | 拿所有用户 |
| 查单个 | GET | /users/123 | 拿 id=123 的用户 |
| 新增 | POST | /users | 创建用户 |
| 更新 | PUT | /users/123 | 改 id=123 的用户 |
| 删除 | DELETE | /users/123 | 删 id=123 的用户 |
前端易踩坑 ②:
/:id里的id通过req.params.id取,类型永远是字符串。你拿它跟数据库数字主键比较、或做计算前,记得Number(req.params.id),否则会出现"123" !== 123的诡异 bug。
四、数据库操作:用 ORM 优雅地增删改查

直接写 SQL 字符串又累又危险(SQL 注入)。ORM 让你用 JS 对象的方式操作数据库,User.create({...}) 就是插一条,User.findAll() 就是查列表,完全是前端熟悉的对象思维。
新建 db.js,定义数据库连接和模型:
js
import { Sequelize, DataTypes } from 'sequelize';
// 连接 SQLite,数据存到本地 database.sqlite 文件
export const sequelize = new Sequelize({
dialect: 'sqlite',
storage: './database.sqlite',
logging: false, // 关掉控制台 SQL 日志,清爽点
});
// 定义 User 模型(相当于一张表的结构)
export const User = sequelize.define('User', {
name: {
type: DataTypes.STRING,
allowNull: false, // 不允许为空
},
email: {
type: DataTypes.STRING,
allowNull: false,
unique: true, // 邮箱不能重复
},
});
// 同步:表不存在就自动建表
await sequelize.sync();回到 routes/users.js,把空壳填成真正的数据库操作:
js
import { Router } from 'express';
import { User } from '../db.js';
const router = Router();
// 查列表
router.get('/', async (req, res, next) => {
try {
const users = await User.findAll();
res.json(users);
} catch (err) {
next(err); // 出错往后传给错误中间件
}
});
// 查单个
router.get('/:id', async (req, res, next) => {
try {
const user = await User.findByPk(req.params.id);
if (!user) return res.status(404).json({ error: '用户不存在' });
res.json(user);
} catch (err) {
next(err);
}
});
// 新增
router.post('/', async (req, res, next) => {
try {
const user = await User.create(req.body);
res.status(201).json(user); // 201 = 创建成功
} catch (err) {
next(err);
}
});
// 更新
router.put('/:id', async (req, res, next) => {
try {
const user = await User.findByPk(req.params.id);
if (!user) return res.status(404).json({ error: '用户不存在' });
await user.update(req.body);
res.json(user);
} catch (err) {
next(err);
}
});
// 删除
router.delete('/:id', async (req, res, next) => {
try {
const user = await User.findByPk(req.params.id);
if (!user) return res.status(404).json({ error: '用户不存在' });
await user.destroy();
res.status(204).end(); // 204 = 成功但无返回内容
} catch (err) {
next(err);
}
});
export default router;前端易踩坑 ③:Express 的路由处理函数不会自动帮你 catch 异步错误。如果你在
async函数里不写try/catch,数据库一报错,请求就会卡住直到超时,前端一直转圈。这是 Express 4 最反直觉的地方(Koa 的洋葱模型能统一兜住,Express 不行)。要么每个异步路由都包try/catch + next(err),要么用下面这招省事。
偷懒技巧:封装一个异步包装器,免得每个路由都手写 try/catch:
js
// 放在 routes/users.js 顶部
const wrap = (fn) => (req, res, next) => fn(req, res, next).catch(next);
// 用法:路由直接包一层,内部就不用 try/catch 了
router.get('/', wrap(async (req, res) => {
const users = await User.findAll();
res.json(users);
}));五、参数校验 + 错误处理:接口的两道防线

永远不要相信前端传来的数据(哪怕前端是你自己写的)。校验在前,兜底在后,这是接口健壮性的两道防线。
5.1 参数校验(express-validator)
它本质是一串中间件:先声明规则,再用一个检查函数收集错误。
在 routes/users.js 里引入:
js
import { body, validationResult } from 'express-validator';
// 校验规则(一组中间件)
const userRules = [
body('name').notEmpty().withMessage('name 不能为空'),
body('email').isEmail().withMessage('email 格式不对'),
];
// 统一检查校验结果的中间件
const checkValid = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
};把规则挂到需要校验的路由上(校验中间件排在业务函数前面):
js
router.post('/', userRules, checkValid, wrap(async (req, res) => {
const user = await User.create(req.body);
res.status(201).json(user);
}));
router.put('/:id', userRules, checkValid, wrap(async (req, res) => {
const user = await User.findByPk(req.params.id);
if (!user) return res.status(404).json({ error: '用户不存在' });
await user.update(req.body);
res.json(user);
}));请求进来时,中间件按 userRules → checkValid → 业务函数 顺序执行,校验不过直接在 checkValid 拦下返回 400,根本到不了业务逻辑。
5.2 全局错误处理中间件
这是 Express 里唯一一个有 4 个参数的特殊中间件:(err, req, res, next)。只要某处调了 next(err)(传了参数),Express 就会跳过所有普通中间件,直奔这里。
把它放在 app.js 的最后面(所有路由挂载之后):
js
// 错误处理中间件:必须 4 个参数,必须放最后
app.use((err, req, res, next) => {
console.error('出错了:', err.message);
// Sequelize 唯一约束冲突(比如邮箱重复)
if (err.name === 'SequelizeUniqueConstraintError') {
return res.status(409).json({ error: '邮箱已被注册' });
}
res.status(500).json({ error: '服务器内部错误' });
});前端易踩坑 ④:错误中间件少一个参数都不行。如果你写成
(err, req, res)只有 3 个参数,Express 会把它当成普通中间件,错误根本不会进来。这个签名是 Express 靠"参数个数"识别的硬规则,记死它。前端易踩坑 ⑤:错误中间件必须放在所有路由的后面。Express 是从上到下顺序匹配的,放前面的话请求还没到路由就被它拦了。
六、拼成一个完整能跑的项目

最终目录结构:
express-crud/
├── package.json ("type": "module")
├── app.js (入口 + 全局中间件 + 错误处理)
├── db.js (数据库连接 + User 模型)
└── routes/
└── users.js (用户 CRUD 路由 + 校验)完整 app.js(把前面的拼起来):
js
import express from 'express';
import usersRouter from './routes/users.js';
const app = express();
app.use(express.json()); // 解析 JSON body
app.use('/users', usersRouter); // 挂载用户路由
// 全局错误处理(放最后)
app.use((err, req, res, next) => {
console.error('出错了:', err.message);
if (err.name === 'SequelizeUniqueConstraintError') {
return res.status(409).json({ error: '邮箱已被注册' });
}
res.status(500).json({ error: '服务器内部错误' });
});
app.listen(3000, () => console.log('http://localhost:3000'));完整 routes/users.js:
js
import { Router } from 'express';
import { body, validationResult } from 'express-validator';
import { User } from '../db.js';
const router = Router();
const wrap = (fn) => (req, res, next) => fn(req, res, next).catch(next);
const userRules = [
body('name').notEmpty().withMessage('name 不能为空'),
body('email').isEmail().withMessage('email 格式不对'),
];
const checkValid = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
next();
};
router.get('/', wrap(async (req, res) => {
res.json(await User.findAll());
}));
router.get('/:id', wrap(async (req, res) => {
const user = await User.findByPk(req.params.id);
if (!user) return res.status(404).json({ error: '用户不存在' });
res.json(user);
}));
router.post('/', userRules, checkValid, wrap(async (req, res) => {
const user = await User.create(req.body);
res.status(201).json(user);
}));
router.put('/:id', userRules, checkValid, wrap(async (req, res) => {
const user = await User.findByPk(req.params.id);
if (!user) return res.status(404).json({ error: '用户不存在' });
await user.update(req.body);
res.json(user);
}));
router.delete('/:id', wrap(async (req, res) => {
const user = await User.findByPk(req.params.id);
if (!user) return res.status(404).json({ error: '用户不存在' });
await user.destroy();
res.status(204).end();
}));
export default router;启动后用 curl 或 Postman 测一遍:
bash
# 新增
curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"name":"张三","email":"zhangsan@test.com"}'
# 查列表
curl http://localhost:3000/users
# 更新
curl -X PUT http://localhost:3000/users/1 \
-H "Content-Type: application/json" \
-d '{"name":"李四","email":"lisi@test.com"}'
# 删除
curl -X DELETE http://localhost:3000/users/1
# 测校验:故意传空,应返回 400
curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" -d '{}'七、前端学 Express 高频踩坑速查表

| 坑 | 现象 | 正确做法 |
|---|---|---|
忘了 express.json() | req.body 是 undefined | 入口加 app.use(express.json()) |
| async 不写 try/catch | 报错后请求卡死、前端一直转圈 | 包 try/catch + next(err),或用 wrap 包装器 |
| 错误中间件参数写少了 | 错误进不去,500 兜不住 | 必须 4 个参数 (err, req, res, next) |
| 错误中间件放错位置 | 永远不触发 | 放在所有路由之后 |
req.params.id 当数字用 | "5" === 5 为 false 的怪 bug | 用前先 Number() 转换 |
| 响应发了还继续执行 | Cannot set headers after sent 报错 | res.json() 后要 return,别让代码往下跑 |
中间件忘了 next() | 请求挂起不响应 | 不结束响应就必须调 next() 放行 |
| 路由顺序写反 | /users/new 被 /users/:id 抢先匹配 | 具体路径写在动态路径 :id 前面 |
| 跨域被浏览器拦 | 前端 fetch 报 CORS 错误 | 装 cors 包:app.use(cors()) |
几个适配技巧(从前端思维平滑过渡):
- 中间件 = 请求拦截器:你在 axios 里写的
interceptors.request,在 Express 就是一个app.use((req,res,next)=>{...})。同一套"链式处理"思维。 res.json()= 前端的return:它就是这个接口的"返回值",一个请求只能res一次,发完就结束。- 状态码是接口的"表情":200 成功、201 已创建、400 你传错了、401 没登录、404 找不到、409 冲突、500 我崩了。前端调接口时按状态码分支处理,后端就要准确地"给表情"。
八、学习路径小结
- 先跑通:照第二节把 hello 接口跑起来,建立信心。
- 理解中间件:这是 Express 的灵魂,搞懂"请求像流水线一样穿过一串函数"就通了一大半。
- 拿下 CRUD:照第六节把完整项目敲一遍、用 curl 测一遍,五个接口全绿。
- 补防线:把校验和错误中间件加上,体会"接口健壮性"是怎么来的。
- 进阶方向:JWT 鉴权中间件、分页查询、把 SQLite 换成 MySQL/PostgreSQL(改
db.js一行 dialect 即可)、用morgan加请求日志。
Express 和 Koa 你都摸过之后会发现:框架只是壳,真正可复用的是"路由分发 + 中间件管道 + 校验 + 错误兜底"这套心智模型。换任何后端框架(甚至 Go 的 Gin),这套思路都成立。