Skip to content

前端开发者的 Express CRUD 接口实战手册

更新: 7/11/2026 字数: 0 字 时长: 0 分钟

你已经会写 Koa、用过 Node、能撸简单接口。这份文档不讲"什么是后端",而是带你把 Express 的整套 CRUD(增删改查)从零搭到能跑、能扛、能交付。所有代码复制就能运行,每一步都标了前端人容易踩的坑。

Express 入门

一、先用一句话理解 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-validator
  • express:框架本体
  • 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 优雅地增删改查

数据库操作 CRUD

直接写 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.bodyundefined入口加 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 我崩了。前端调接口时按状态码分支处理,后端就要准确地"给表情"。

八、学习路径小结

  1. 先跑通:照第二节把 hello 接口跑起来,建立信心。
  2. 理解中间件:这是 Express 的灵魂,搞懂"请求像流水线一样穿过一串函数"就通了一大半。
  3. 拿下 CRUD:照第六节把完整项目敲一遍、用 curl 测一遍,五个接口全绿。
  4. 补防线:把校验和错误中间件加上,体会"接口健壮性"是怎么来的。
  5. 进阶方向:JWT 鉴权中间件、分页查询、把 SQLite 换成 MySQL/PostgreSQL(改 db.js 一行 dialect 即可)、用 morgan 加请求日志。

Express 和 Koa 你都摸过之后会发现:框架只是壳,真正可复用的是"路由分发 + 中间件管道 + 校验 + 错误兜底"这套心智模型。换任何后端框架(甚至 Go 的 Gin),这套思路都成立。