前置知识
将 npm/pnpm 的源更换为国内镜像,可以提供更快、更稳定的下载体验。如果你有科学上网的能力,还是建议使用官方源,避免有些包在国内无法下载的问题。
bash
# 查看当前源
npm config get registry
pnpm config get registry
# 更换为国内源
npm config set registry https://registry.npmmirror.com
pnpm config set registry https://registry.npmmirror.com项目初始化
bash
# 创建项目目录
mkdir nodejs-express-api
cd nodejs-express-api
# 初始化项目
pnpm init
# 安装依赖
pnpm install express # 核心包
pnpm install @dotenvx/dotenvx nodemon -D # 使用环境变量和热更新修改 package.json 文件,添加以下内容
json
{
// 启用 ESM 模块化
"type": "module",
"scripts": {
"dev": "dotenvx run -- nodemon ./src/server.js"
}
}根据 Best Practices for Structuring an Express.js Project 创建项目结构
📁 nodejs-express-api
├── 📁 src
│ ├── 📁 config # 配置文件 (例如: 数据库, 环境变量)
│ ├── 📁 controllers # 业务逻辑 (处理请求/响应)
│ ├── 📁 models # 数据库模型 & 模式
│ ├── 📁 routes # API 路由定义
│ ├── 📁 middlewares # 自定义中间件 (身份验证, 日志记录, 错误处理)
│ ├── 📁 services # 业务逻辑或外部API交互
│ ├── 📁 utils # 辅助函数和工具
│ ├── app.js # 应用入口
│ └── server.js # 服务器初始化
├── .env # 环境变量
├── .gitignore # 忽略版本控制的文件
├── package.json # 项目依赖和脚本
└── README.md # 项目说明简单使用 Expresss
先了解下 Express 的用法,实现简单的 CRUD 接口。
- src 目录下创建一个
mock-database.json文件,模拟数据库 - 在
server.js文件下编写 API 接口
注意
如果需要 body 传参,需要使用 body-parser 中间件。 见 server.js 文件中第 8 行代码
js
import { readFile, writeFile } from "node:fs/promises";
import express from "express";
import bodyParser from "body-parser";
const PORT = 3000;
const app = express();
// parse application/json
app.use(bodyParser.json());
// 构建相对于当前模块的文件路径,而不是依赖运行时的工作目录
const mockDatabasePath = new URL("./mock-database.json", import.meta.url);
async function getTodosData() {
try {
const todosData = await readFile(mockDatabasePath, "utf-8");
const todos = JSON.parse(todosData);
return todos;
} catch (error) {
return [];
}
}
// 查询全部数据
app.get("/todos", async (_req, res) => {
try {
const todos = await getTodosData();
return res.status(200).json(todos);
} catch (error) {
return res.status(500).json({ error: "Failed to fetch todos" });
}
});
// 根据 id 查询数据
app.get("/todos/:todoId", async (req, res) => {
const todos = await getTodosData();
try {
const todo = todos.find((todo) => todo.id === parseInt(req.params.todoId));
if (!todo) {
throw new Error("todo id is not valid");
}
return res.status(200).json(todo);
} catch (error) {
return res.status(404).json({ error: error.message });
}
});
// 根据 id 删除数据
app.delete("/todos/:todoId", async (req, res) => {
debugger;
const todos = await getTodosData();
try {
const updatedTodos = todos.filter((todo) => todo.id !== parseInt(req.params.todoId));
if (updatedTodos.listen === todos.length) {
throw new Error("delete todo failed, todo id is not valid");
}
await writeFile(mockDatabasePath, JSON.stringify(updatedTodos, null, 2));
return res.status(200).json({ message: "delete todo success" });
} catch (error) {
return res.status(404).json({ error: error.message });
}
});
// 添加
app.post("/addTodo", async (req, res) => {
const todos = await getTodosData();
try {
const newTodo = {
id: todos.length + 1,
...req.body,
};
const newTodos = [...todos, newTodo];
await writeFile(mockDatabasePath, JSON.stringify(newTodos, null, 2));
return res.status(200).json(newTodo);
} catch (error) {
return res.status(500).json({ error: "add todo failed" });
}
});
// 修改
app.put("/updateTodo", async (req, res) => {
const todos = await getTodosData();
try {
const { id, ...updates } = req.body;
const todo = todos.find((todo) => todo.id === parseInt(id));
if (!todo) {
throw new Error("update todo failed, todo id is not valid");
}
const updatedTodo = { ...todo, ...updates };
const newTodos = todos.map((todo) => (todo.id === parseInt(id) ? updatedTodo : todo));
await writeFile(mockDatabasePath, JSON.stringify(newTodos, null, 2));
return res.status(200).json(updatedTodo);
} catch (error) {
return res.status(500).json({ error: error.message });
}
});
app.listen(PORT, () => {
console.log(`Server is running at http://localhost:${PORT}`);
});json
[
{
"id": 1,
"title": "完成项目需求文档",
"description": "与产品经理确认后,撰写并提交 V2.0 版本的需求文档。",
"priority": "high",
"completed": false,
"createdAt": "2025-12-12",
"dueDate": "2025-12-19"
},
{
"id": 2,
"title": "预约牙医",
"description": "打电话预约下周的牙齿检查和洗牙。",
"priority": "medium",
"completed": false,
"createdAt": "2025-12-12",
"dueDate": "2025-12-12"
},
{
"id": 3,
"title": "购买生日礼物",
"description": "为朋友挑选并购买下周生日聚会的礼物。",
"priority": "low",
"completed": false,
"createdAt": "2025-12-12",
"dueDate": "2025-12-12"
},
{
"id": 4,
"title": "整理电脑文件",
"description": "清理桌面和下载文件夹,按项目分类归档。",
"priority": "medium",
"completed": false,
"createdAt": "2025-12-12",
"dueDate": "2025-12-12"
},
{
"id": 5,
"title": "提交月度报销单",
"priority": "medium",
"completed": false,
"createdAt": "2025-12-12",
"dueDate": "2025-12-12"
},
{
"id": 6,
"title": "阅读《原子习惯》第3章",
"priority": "medium",
"completed": false,
"createdAt": "2025-12-12",
"dueDate": "2025-12-12"
},
{
"id": 7,
"title": "健身30分钟",
"priority": "medium",
"completed": false,
"createdAt": "2025-12-12",
"dueDate": "2025-12-12"
},
{
"id": 8,
"title": "回复客户邮件",
"priority": "medium",
"completed": false,
"createdAt": "2025-12-12",
"dueDate": "2025-12-12"
},
{
"id": 9,
"title": "准备周会材料",
"priority": "medium",
"completed": false,
"createdAt": "2025-12-12",
"dueDate": "2025-12-12"
},
{
"id": 10,
"title": "缴纳水电费",
"priority": "medium",
"completed": false,
"createdAt": "2025-12-12",
"dueDate": "2025-12-12"
},
{
"id": 11,
"title": "备份重要数据",
"priority": "medium",
"completed": false,
"createdAt": "2025-12-12",
"dueDate": "2025-12-12"
},
{
"id": 12,
"title": "规划周末短途旅行",
"priority": "medium",
"completed": false,
"createdAt": "2025-12-12",
"dueDate": "2025-12-12"
}
]在 src 目录下执行 node server.js 启动服务器,访问 http://localhost:3000/todos 查看效果。
重构路由
为了提高清晰度、可扩展性和可维护性,需要按照上面文件夹拆分不同的代码模块。
txt
PORT = 3000;js
import { readFile, writeFile } from "node:fs/promises";
// 构建相对于当前模块的文件路径,而不是依赖运行时的工作目录
const mockDatabasePath = new URL("../mock-database.json", import.meta.url);
/**
* 获取 todos 数据
* @returns {Promise<Array>} 返回 todos 数据
*/
async function getTodosData() {
try {
const todosData = await readFile(mockDatabasePath, "utf-8");
const todos = JSON.parse(todosData);
return todos;
} catch (error) {
return [];
}
}
/**
* 获取 todos 数据
* @param {Request} req - 请求对象
* @param {Response} res - 响应对象
* @returns {Promise<Response>} 响应对象
*/
export async function getTodos(_req, res) {
debugger;
try {
const todos = await getTodosData();
return res.status(200).json(todos);
} catch (error) {
return res.status(500).json({ error: "Failed to fetch todos" });
}
}
/**
* 根据 id 获取 todo 数据
* @param {Request} req - 请求对象
* @param {Response} res - 响应对象
* @returns {Promise<Response>} 响应对象
*/
export async function getTodoById(req, res) {
const todos = await getTodosData();
try {
const todo = todos.find((todo) => todo.id === parseInt(req.params.todoId));
if (!todo) {
throw new Error("todo id is not valid");
}
return res.status(200).json(todo);
} catch (error) {
return res.status(404).json({ error: error.message });
}
}
/**
* 根据 id 删除 todo 数据
* @param {Request} req - 请求对象
* @param {Response} res - 响应对象
* @returns {Promise<Response>} 响应对象
*/
export async function deleteTodoById(req, res) {
const todos = await getTodosData();
try {
const updatedTodos = todos.filter((todo) => todo.id !== parseInt(req.params.todoId));
if (updatedTodos.listen === todos.length) {
throw new Error("delete todo failed, todo id is not valid");
}
await writeFile(mockDatabasePath, JSON.stringify(updatedTodos, null, 2));
return res.status(200).json({ message: "delete todo success" });
} catch (error) {
return res.status(404).json({ error: error.message });
}
}
/**
* 添加 todo 数据
* @param {Request} req - 请求对象
* @param {Response} res - 响应对象
* @returns {Promise<Response>} 响应对象
*/
export async function addTodo(req, res) {
const todos = await getTodosData();
try {
const newTodo = {
id: todos.length + 1,
...req.body,
};
const newTodos = [...todos, newTodo];
await writeFile(mockDatabasePath, JSON.stringify(newTodos, null, 2));
return res.status(200).json(newTodo);
} catch (error) {
return res.status(500).json({ error: "add todo failed" });
}
}
/**
* 更新 todo 数据
* @param {Request} req - 请求对象
* @param {Response} res - 响应对象
* @returns {Promise<Response>} 响应对象
*/
export async function updateTodo(req, res) {
const todos = await getTodosData();
try {
const { id, ...updates } = req.body;
const todo = todos.find((todo) => todo.id === parseInt(id));
if (!todo) {
throw new Error("update todo failed, todo id is not valid");
}
const updatedTodo = { ...todo, ...updates };
const newTodos = todos.map((todo) => (todo.id === parseInt(id) ? updatedTodo : todo));
await writeFile(mockDatabasePath, JSON.stringify(newTodos, null, 2));
return res.status(200).json(updatedTodo);
} catch (error) {
return res.status(500).json({ error: error.message });
}
}js
import express from "express";
import {
getTodos,
getTodoById,
deleteTodoById,
addTodo,
updateTodo,
} from "../controllers/todoController.js";
const router = express.Router();
router.get("/todos", getTodos);
router.get("/todos/:todoId", getTodoById);
router.delete("/todos/:todoId", deleteTodoById);
router.post("/todos", addTodo);
router.put("/todos/:todoId", updateTodo);
export default router;js
import express from "express";
import bodyParser from "body-parser";
import todoRouters from "./routes/todoRouters.js";
const app = express();
// parse application/json
app.use(bodyParser.json());
app.use("/api", todoRouters);
export default app;js
import { getLocalIP } from "./utils/getLocalIP.js";
import app from "./app.js";
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server is running at:`);
console.log(`- Local: \x1b[38;2;130;182;200mhttp://localhost:${PORT}\x1b[0m`);
console.log(`- Network: \x1b[38;2;130;182;200mhttp://${getLocalIP()}:${PORT}\x1b[0m`);
});js
import os from "os";
export function getLocalIP() {
const interfaces = os.networkInterfaces();
for (const name of Object.keys(interfaces)) {
for (const iface of interfaces[name]) {
if (iface.family === "IPv4" && !iface.internal) {
return iface.address;
}
}
}
return "127.0.0.1";
}测试: 执行命令 pnpm run dev, 在浏览器打开 http://localhost:3000/api/todos 获取 TODO 列表
Supabase 数据库使用
上面我们都是在使用 mock-database.json 文件来模拟数据库,这种只适合演示demo,生产使用将有诸多问题:
- 不支持并发读写。多个用户/进程同时修改文件时,会导致数据损坏或覆盖
- 每次读写都需要加载整个文件到内存(尤其是写入时通常需重写整个文件)。随着数据量增大,读写速度急剧下降。
- 无法字段限制,无法复杂查询,不方便分析数据等等...
Supabase 是一个基于 PostgreSQL 的开源后端即服务(BaaS),它提供了易于使用的 API 来处理常见的后端任务,如用户认证、数据库操作、文件存储和服务器端函数等
Sequelize ORM 使用
接口限流
接口日志
接口鉴权
接口部署
Nodejs + Express RESTful API接口开发实践https://blog.xkfe.site/posts/2025/1205-nodejs-express
评论 隐私政策