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 的中间件机制让公共逻辑可以复用。除了示例里已经用到的 helmet、cors、express.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 里读 page 和 pageSize,用 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 指南;如果要做更复杂的后端对接,后端对接分类下有更多资料。