Node.js REST API 完整示例

一个中型网站的后端往往从一个简单的 API 开始:前端要数据,后端给 JSON。本文用 Express 加 SQLite 搭一个带完整增删改查的博客接口,不引入重型框架,代码量小、依赖少,适合中小站点快速落地。SQLite 无需单独部署数据库服务,对早期项目尤其友好。假设你在给一个内容站做后台,编辑想用一套接口管理文章:列表、详情、新增、改标题、删旧文——下面这段代码就是为此准备的。

初始化项目

mkdir my-api && cd my-api
npm init -y
npm install express cors helmet better-sqlite3

helmet 用于设置常见安全响应头,cors 解决跨域,better-sqlite3 是同步 API 的 SQLite 驱动,写起来比异步驱动直观很多。

参考:Express 官方文档 https://expressjs.com/

主服务器与建表

const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const Database = require('better-sqlite3');

const app = express();
const db = new Database('app.db');

app.use(helmet());
app.use(cors());
app.use(express.json());

db.exec(`CREATE TABLE IF NOT EXISTS articles (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  title TEXT NOT NULL,
  content TEXT,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`);

建表语句尽量保持精简,因为 SQLite 不像 MySQL 那样支持复杂的 ALTER 操作,字段最好在初期规划好,后面再改表结构会比较麻烦。

CRUD 路由

// 查询列表
app.get('/api/articles', (req, res) => {
  const articles = db.prepare('SELECT * FROM articles ORDER BY created_at DESC').all();
  res.json(articles);
});

// 查询单条
app.get('/api/articles/:id', (req, res) => {
  const article = db.prepare('SELECT * FROM articles WHERE id = ?').get(req.params.id);
  if (!article) return res.status(404).json({ error: 'Article not found' });
  res.json(article);
});

// 新增
app.post('/api/articles', (req, res) => {
  const { title, content } = req.body;
  if (!title) return res.status(400).json({ error: 'Title is required' });
  const result = db.prepare('INSERT INTO articles (title, content) VALUES (?, ?)').run(title, content);
  res.status(201).json({ id: result.lastInsertRowid, title, content });
});

// 更新
app.put('/api/articles/:id', (req, res) => {
  const { title, content } = req.body;
  const result = db.prepare('UPDATE articles SET title = ?, content = ? WHERE id = ?').run(title, content, req.params.id);
  if (result.changes === 0) return res.status(404).json({ error: 'Article not found' });
  res.json({ message: 'Updated successfully' });
});

// 删除
app.delete('/api/articles/:id', (req, res) => {
  const result = db.prepare('DELETE FROM articles WHERE id = ?').run(req.params.id);
  if (result.changes === 0) return res.status(404).json({ error: 'Article not found' });
  res.json({ message: 'Deleted successfully' });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`API server running on port ${PORT}`);
});

目录与代码结构

项目保持极简:一个 server.js(或 index.js)装下全部路由,一个 app.db 放 SQLite 数据。小项目这样没问题,等路由超过十来个再考虑拆成 controllers 目录。初始化时 npm init -y 会生成默认 package.json,记得把 "main" 改成入口文件名,并在 scripts 里加 "start": "node server.js",这样部署时 npm start 就能跑。

用 curl 验证接口

curl -X POST http://localhost:3000/api/articles \
  -H "Content-Type: application/json" \
  -d '{"title":"第一篇","content":"hello"}'

curl http://localhost:3000/api/articles
curl -X DELETE http://localhost:3000/api/articles/1

路由与状态码约定

方法 路径 作用 成功状态码 失败状态码
GET /api/articles 列表 200
GET /api/articles/:id 单条 200 404
POST /api/articles 新增 201 400
PUT /api/articles/:id 更新 200 404
DELETE /api/articles/:id 删除 200 404

状态码符合 REST 惯例:创建返回 201,参数错误返回 400,资源不存在返回 404。更多设计规范可以参考REST API 设计指南。

接口返回结构约定

前端对接时最怕每个接口返回结构都不一样。可以在项目里约定一套统一格式,比如成功时 { "code": 0, "data": ... },失败时 { "code": 4001, "message": "参数错误" }。这样前端只用判断 code 就能统一处理错误,不用在每处接口里各写一套判断。上面的示例为了简洁直接返回裸数据,实际项目中套一层结构通常更省事。

常用中间件

Express 的中间件机制让公共逻辑可以复用。除了示例里已经用到的 helmetcorsexpress.json(),还有几个常见场景:记录请求日志的 morgan,统一限流的 express-rate-limit,以及校验请求体的 express-validator。中间件的顺序也有讲究——放在路由之前注册的会先执行,所以鉴权和限流通常放在最前面。

兜底错误处理

生产环境一定要有兜底错误处理中间件,否则某个路由抛异常时,Express 默认返回 HTML 错误页,前端解析起来很别扭。在所有路由之后注册一个专门处理错误的中间件,统一返回 JSON:

app.use((err, req, res, next) => {
  console.error(err);
  res.status(500).json({ error: 'Internal Server Error' });
});

配合上面的统一返回结构,前端就能在拿到非零 code 时展示友好提示。

列表接口的分页

现在 GET /api/articles 会一次性返回全部数据,文章多了响应会越来越大。给列表接口加分页是常见需求:从 query 里读 pagepageSize,用 LIMIT ? OFFSET ? 查询,并在响应里带上 total 字段,前端就能渲染页码。这段逻辑和上面的 CRUD 思路一致,改起来很快。

常见问题

为什么用 better-sqlite3 而不是 sqlite3? better-sqlite3 是同步 API,不需要回调或 Promise,读起来接近普通代码,出错也更容易定位;代价是它在 Node 主线程上同步执行,适合中小流量。POST 返回 400 但前端没传 content? 接口只校验了 title,content 是可选的,这是有意为之;如果业务要求两者都必填,把校验改成 if (!title || !content) 即可。端口被占用怎么办?PORT=4000 node server.js 覆盖环境变量,或者通过 .env 管理端口。JSON 请求体太大被拒? express.json() 默认限制 100kb,用 express.json({ limit: '2mb' }) 调整即可。上线后接口 500 怎么排查? 先看 PM2 或 systemd 的日志,再检查 SQLite 文件权限;兜底错误处理中间件里打日志能帮忙快速定位。

上线前的补充

上面是最小可用版本,真正上线前通常还要补几件事:

  • 输入校验。目前只有标题的非空检查,建议用校验库限制长度与格式,避免脏数据入库。
  • 统一错误处理。把 404 和 500 的处理集中到中间件,接口报错时返回一致的 JSON 结构,参考API 错误处理。
  • 鉴权。如果接口要写入数据,先加上登录与权限控制,具体可看API 安全与 JWT。
  • 进程管理。用PM2守护进程并设置开机自启,避免服务器重启后接口失联。
  • 敏感配置。端口、数据库路径等参数用环境变量管理,见环境变量与密钥管理。

完整的 Express 项目搭建流程,可以看Node.js Express 指南;如果要做更复杂的后端对接,后端对接分类下有更多资料。