六维教程

Node.js Express 入门

用 Node 自带的 http 模块写接口,每个路由都要手动解析 URL 再 if 判断,接口一多代码就乱成一团。这篇介绍 Express,npm 生态里最流行的 Node Web 框架,它把路由、参数解析、响应发送这些重复劳动封装好,我们只写业务代码。Node.js 事件循环 里已经见识过 http 模块的写法,现在看 Express 在它之上做了什么。

为什么用 Express

Express 是构建在 http 模块之上的轻量级 Web 框架,不改变 Node 的运行方式,只是把常用操作封装成更顺手的 API。

场景 http 模块 Express
定义路由 手动解析 req.url 再 if 判断 app.get(“/users”, handler) 一行搞定
取查询参数 手写 URL 解析 req.query.page 直接读
返回 JSON JSON.stringify 再拼响应头 res.json(data)

框架本身只做路由和中间件两件事,数据库、模板、验证等能力全部靠第三方插件扩展,用多少装多少,所以它够轻量,npm 下载量常年第一。

安装 Express

先初始化项目再安装依赖,npm 的完整流程在 Node.js npm 入门 里讲过,这里直接操作。

# 在项目目录初始化 package.json
npm init -y

# 安装 express 并写入 dependencies
npm install express

安装完成后打开 package.json,dependencies 里能看到 express 的版本号。这一系列文章用的都是 Node 20 LTS,包管理器 npm 已经内置,不需要单独安装。

最简服务器

// server.js
const express = require("express");

const app = express();

// 定义根路径路由,浏览器访问 http://localhost:3000 会看到返回的文本
app.get("/", (req, res) => {
  res.send("Hello Express");
});

// 启动服务器并监听 3000 端口
app.listen(3000, () => {
  console.log("服务器已启动,访问 http://localhost:3000");
});
node server.js

app.get 的第一个参数是路径,第二个参数是处理函数。处理函数接收 req 和 res 两个对象,req 携带请求信息,res 负责发送响应。app.listen(3000) 启动服务器,第二个回调在启动成功时执行。

路由参数

路径参数 req.params

/users/123 这类地址里的 123 是路径参数,路由里用冒号声明变量名。

app.get("/users/:id", (req, res) => {
  res.send(`用户 ID 是 ${req.params.id}`);
});

访问 /users/7 时 req.params.id 的值是 7。冒号后的名字可以任意起,取值时用同名属性即可。

查询参数 req.query

/users?age=18 问号后面的部分是查询参数,Express 自动解析成对象挂在 req.query 上。

app.get("/users", (req, res) => {
  res.send(`筛选年龄 ${req.query.age}`);
});

查询参数适合表达筛选和分页,比如 ?age=18 和 ?page=2&size=10,多个参数用 & 连接。

请求体 req.body

POST 请求的数据放在请求体里,Express 默认不解析,需要注册 express.json() 中间件。先记住这个固定写法,中间件的原理在下一篇 Node.js Express 中间件 详细讲。

app.use(express.json());

app.post("/users", (req, res) => {
  res.send(`收到 ${req.body.name}`);
});

路由方法

Express 为每种 HTTP 方法提供了对应的声明函数。

方法 声明函数 典型用途
GET app.get 查询数据
POST app.post 创建数据
PUT app.put 整体更新数据
DELETE app.delete 删除数据
app.get("/users", (req, res) => res.send("查询用户列表"));
app.post("/users", (req, res) => res.send("创建用户"));
app.put("/users/:id", (req, res) => res.send(`更新用户 ${req.params.id}`));
app.delete("/users/:id", (req, res) => res.send(`删除用户 ${req.params.id}`));

同一个路径搭配不同方法,就是四个相互独立的路由。浏览器地址栏只能发 GET 请求,测试其他方法用 curl 或者 Postman。

# 用 curl 发送 POST 请求
curl -X POST http://localhost:3000/users

响应 API

res 对象提供几个常用的发送响应方法。

app.get("/text", (req, res) => {
  res.send("纯文本"); // 发送文本,自动设置 Content-Type
});

app.get("/json", (req, res) => {
  res.json({ name: "张三", age: 18 }); // 发送 JSON
});

app.get("/status", (req, res) => {
  res.status(201).json({ ok: true }); // 先设状态码再发 JSON,链式调用
});

res.send 会按内容自动识别类型,传字符串返回文本,传对象也会转成 JSON。res.json 专门发送 JSON,接口场景优先用它。res.status 设置状态码,可以与其他方法链式调用。

常见坑

路由顺序影响匹配结果

Express 按注册顺序匹配路由,先注册的先命中。如果 /users/:id 写在 /users/new 前面,访问 /users/new 时 new 会被当成 id 参数。

// 错误写法,/users/new 永远匹配不到
app.get("/users/:id", (req, res) => res.send("详情"));
app.get("/users/new", (req, res) => res.send("新建页"));

// 正确写法,固定路径放在动态路径之前
app.get("/users/new", (req, res) => res.send("新建页"));
app.get("/users/:id", (req, res) => res.send("详情"));

res.json 和 res.send 不要混用

一个请求只能发送一次响应,同一个处理函数里重复调用会抛 ERR_HTTP_HEADERS_SENT 错误。接口统一用 res.json,简单文本用 res.send,别在同一个处理函数里两个都用。

实践

用 Express 重写 http 模块版的用户服务器,支持路径参数和查询参数,顺便把四种方法全部用上。

// server.js
const express = require("express");

const app = express();
app.use(express.json());

// 模拟用户数据,先用内存数组,后面的数据库篇会替换成真数据库
const users = [
  { id: 1, name: "张三", age: 20 },
  { id: 2, name: "李四", age: 18 },
];

// 用户列表,支持 ?age=18 按年龄筛选
app.get("/users", (req, res) => {
  const { age } = req.query;
  if (age) {
    return res.json(users.filter((u) => u.age === Number(age)));
  }
  res.json(users);
});

// 单个用户详情,id 来自路径参数
app.get("/users/:id", (req, res) => {
  const user = users.find((u) => u.id === Number(req.params.id));
  if (!user) {
    return res.status(404).json({ message: "用户不存在" });
  }
  res.json(user);
});

// 创建用户,数据来自请求体
app.post("/users", (req, res) => {
  const user = { id: users.length + 1, ...req.body };
  users.push(user);
  res.status(201).json(user);
});

// 更新用户
app.put("/users/:id", (req, res) => {
  const id = Number(req.params.id);
  const index = users.findIndex((u) => u.id === id);
  if (index === -1) {
    return res.status(404).json({ message: "用户不存在" });
  }
  users[index] = { id, ...req.body };
  res.json(users[index]);
});

// 删除用户
app.delete("/users/:id", (req, res) => {
  const id = Number(req.params.id);
  const index = users.findIndex((u) => u.id === id);
  if (index === -1) {
    return res.status(404).json({ message: "用户不存在" });
  }
  users.splice(index, 1);
  res.json({ message: "删除成功" });
});

app.listen(3000, () => {
  console.log("服务器已启动,访问 http://localhost:3000");
});
node server.js
# 另开一个终端依次测试
curl http://localhost:3000/users
curl "http://localhost:3000/users?age=18"
curl http://localhost:3000/users/1
curl -X POST -H "Content-Type: application/json" -d "{\"name\":\"王五\",\"age\":25}" http://localhost:3000/users
curl -X PUT -H "Content-Type: application/json" -d "{\"name\":\"王五\",\"age\":26}" http://localhost:3000/users/3
curl -X DELETE http://localhost:3000/users/2

路由和参数的写法到这里就齐了,下一篇 Node.js Express 中间件 讲 express.json() 这类中间件到底做了什么,以及如何做全局错误处理。

上一篇
Node.js 事件循环
下一篇
Node.js Express 中间件