ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

从零搭建Express+Sequelize+MySQL全栈API实战教程

从零搭建Express+Sequelize+MySQL全栈API实战教程 1. 项目初衷与整体设计思路先说点实在的。很多新手朋友想学全栈一上来就陷入选择困难症前端用 React 还是 Vue后端用 Spring Boot 还是 Python 的 Django数据库用啥部署又咋搞结果折腾一个月项目还是停留在“hello world”阶段。我当初做这个从零搭建 Express Sequelize MySQL 全栈 API 项目目标非常明确用最主流、最经典、资料最多的技术组合把后端 API 从开发到部署的完整链路走一遍。Express 是 Node.js 生态里最老牌、用户量最大的框架Sequelize 是 Node 生态里功能最完整、文档最丰富的 ORM 工具MySQL 是通用性最强的关系型数据库Docker 几乎是现在部署服务的标配。这套组合可能不是性能最强的但一定是最不容易走弯路、最适合当教科书的一套。为什么不用 Spring Boot不是不好而是对于前端同学或者刚学 Node 的同学来说Java 的体系学习成本相对高光是一个 Maven 依赖管理就能劝退不少人。为什么不用 MongoDB在真实业务里MySQL 依然是绝大多数公司的选择事务、联表查询、权限控制这些关系型数据库的“基本功”你迟早要会。用 Sequelize 就是为了把 SQL 语句封装起来让你可以用 JavaScript 的思维去操作数据库降低入门门槛但又不至于像直接用 SQL 一样容易写出漏洞。1.1 项目规划的四种核心思路在动手敲代码之前我先把整个项目需要解决的问题列了一个清单这个清单基本可以复用在你自己的任何后端项目里技术栈选型明确使用 Express 5.x目前最新稳定版、Sequelize 6.x、MySQL 8.0Docker Compose 管理容器编排。目录结构设计不能把所有代码都堆在一个 app.js 里要按 MVC 的思想拆成 models、routes、controllers、config 等目录。核心功能模块用户注册登录含密码加密、文章 CRUD含分页、数据校验、统一的错误处理。部署方案用 Dockerfile 构建应用镜像用 docker-compose 把 MySQL 和应用一起跑起来做到本地跑通即可无缝迁移到服务器。这四步想清楚项目就成功了一半。很多时候项目烂尾不是代码写不出来而是开头没有想清楚“这个项目到底要包含哪些功能”。1.2 为什么 Express Sequelize 更适合新手入门我见过太多一上来就上 NestJS 的同学。NestJS 确实好但它引入了装饰器、依赖注入、模块化这些概念对零基础的人来说理解成本偏高。Express 就四个字简单直接。路由就是函数中间件就是函数连框架本身都只是一个加强版的 HTTP 模块封装。Sequelize 对比直接用 mysql 包来写 SQL优势在于你不需要手动拼接 SQL 字符串避免 SQL 注入风险。模型定义之后增删改查就是对象方法调用比如User.findOne()和User.create()心智负担小很多。迁移migration功能可以在开发阶段帮你同步表结构不用手工去 MySQL 里敲CREATE TABLE。当然 Sequelize 也有一些“坑”比如关联查询的写法比较绕、复杂嵌套查询性能表现一般这些我会在后面的章节里详细讲到实际踩坑记录。2. 环境准备与基础搭建这个项目的第一步不是写代码而是把环境准备好。我见过太多同学环境变量没配好项目代码抄了一遍还是跑不起来最后怀疑人生。其实只要你按步骤来十分钟就能把环境搞定。2.1 本地开发环境的必要组件清单Node.js我用的 18.16.0 LTS 版本。建议去官网下载不要用 apt 或者 brew 装的太旧的版本。MySQL本地开发可以用 Docker 跑一个 MySQL 8.0 实例这样最干净复盘也用得到。Docker DesktopWindows 和 Mac 用户直接装 Docker Desktop 就行安装的时候注意把 WSL2 后端勾上不然性能差很多。Navicat 或 DBeaver数据库可视化工具这个不是必须但调试表结构和数据的时候能省不少事。Postman 或 ApifoxAPI 调试工具推荐直接用 Apifox它支持把接口文档导出成 Markdown方便整理博客内容。2.2 初始化项目并安装依赖mkdir fullstack-api-demo cd fullstack-api-demo npm init -y npm install express sequelize mysql2 dotenv cors npm install -D nodemon我解释一下为什么安装这些包express核心框架处理 HTTP 请求路由。sequelizemysql2Sequelize 依赖 mysql2 来驱动 MySQL 数据库。dotenv读取 .env 文件里的配置数据库密码、端口这类敏感信息不该硬编码到代码里。cors解决跨域问题前后端分离项目必装。nodemon开发时监听文件变化自动重启服务不用每次手动node app.js。装完之后在 package.json 里加两个脚本scripts: { dev: nodemon app.js, start: node app.js }2.3 创建基础目录结构我习惯的分层方式是这样的清晰、可扩展fullstack-api-demo/ ├── app.js # 入口文件创建 Express 实例 ├── config/ │ └── db.js # Sequelize 连接配置 ├── models/ # Sequelize 模型定义 │ ├── index.js │ ├── user.js │ └── article.js ├── routes/ # 路由定义 │ ├── index.js │ ├── auth.js │ └── article.js ├── controllers/ # 业务逻辑处理 │ ├── authController.js │ └── articleController.js ├── middlewares/ # 中间件鉴权、错误处理 │ ├── auth.js │ └── errorHandler.js ├── .env # 环境变量一定加进 .gitignore ├── .env.example # 环境变量模板 ├── Dockerfile # 应用镜像构建 └── docker-compose.yml # 容器编排有的同学一开始觉得 routes 和 controllers 分开很麻烦直接在路由文件里写业务逻辑不是更快吗小项目确实可以但一旦功能多起来路由文件会膨胀到几百行而且多个路由共用逻辑比如查询当前登录用户就没法抽出来复用。分开写有点像是把“餐厅的点菜菜单”和“后厨做菜的流程”分开管理井井有条。3. Sequelize 模型设计与数据库连接这是整个项目的地基。数据库表设计得好不好直接影响你后面写业务代码的幸福感。我在这个项目里设计了两个表用户表users和文章表articles并且建立了“用户对文章的一对多关联关系”。3.1 配置数据库连接池在config/db.js里我这样配置 Sequelize 连接const { Sequelize } require(sequelize); require(dotenv).config(); const sequelize new Sequelize( process.env.DB_NAME, process.env.DB_USER, process.env.DB_PASSWORD, { host: process.env.DB_HOST, port: process.env.DB_PORT, dialect: mysql, timezone: 08:00, pool: { max: 10, min: 0, acquire: 30000, idle: 10000 }, logging: console.log } ); module.exports sequelize;关于连接池我多讲两句。连接池里的max: 10表示同时最多保持 10 个数据库连接acquire: 30000表示如果连接池满了等待 30 秒还没拿到连接就报超时错误。你可能会问一个请求创建一个连接不行吗不行每次都重新建立 TCP 连接 MySQL 握手 鉴权开销非常大高并发下数据库会先崩。连接池本质是“复用已有连接”就像打车平台调度车辆而不是每单都造一辆新车。注意我设置了timezone: 08:00不设置的话时间字段会差 8 个小时这是新手最容易忽略的坑。3.2 定义用户模型与文章模型用户模型const { DataTypes } require(sequelize); const sequelize require(../config/db); const bcrypt require(bcryptjs); const User sequelize.define(User, { id: { type: DataTypes.INTEGER.UNSIGNED, autoIncrement: true, primaryKey: true, allowNull: false }, username: { type: DataTypes.STRING(50), allowNull: false, unique: true, comment: 用户名唯一 }, email: { type: DataTypes.STRING(100), allowNull: false, unique: true, validate: { isEmail: true } }, passwordHash: { type: DataTypes.STRING(255), allowNull: false, field: password_hash, comment: 存储 bcrypt 加密后的密码 }, status: { type: DataTypes.TINYINT, defaultValue: 1, comment: 0: 禁用, 1: 正常 } }, { tableName: users, timestamps: true, underscored: true });文章模型const Article sequelize.define(Article, { id: { type: DataTypes.INTEGER.UNSIGNED, autoIncrement: true, primaryKey: true }, title: { type: DataTypes.STRING(200), allowNull: false, comment: 文章标题 }, content: { type: DataTypes.TEXT, allowNull: false, comment: 文章正文 }, viewCount: { type: DataTypes.INTEGER.UNSIGNED, defaultValue: 0, field: view_count, comment: 浏览量 }, userId: { type: DataTypes.INTEGER.UNSIGNED, allowNull: false, field: user_id, references: { model: User, key: id } } }, { tableName: articles, timestamps: true, underscored: true });有几个细节值得注意密码字段我故意命名成passwordHash并且在模型里用field: password_hash映射到数据库列名这样代码和数据库命名规范都清晰。所有时间戳都用underscored: true让 Sequelize 自动生成created_at和updated_at。时间字段的默认值交给数据库端CURRENT_TIMESTAMP即可代码里不用手工赋值。3.3 model/index.js统一注册模型和关联关系如果你的项目只有一两个表直接在 app.js 里加载模型没问题。但真实项目往往有十几个表所以最好有个统一的入口const sequelize require(../config/db); const User require(./user); const Article require(./article); User.hasMany(Article, { foreignKey: user_id, as: articles }); Article.belongsTo(User, { foreignKey: user_id, as: author }); const sync async () { await sequelize.sync({ alter: false }); }; module.exports { sequelize, User, Article, sync };关于sequelize.sync()我再强调一下开发环境用sync({ alter: true })可以自动把数据库表结构改成和模型一致很爽。但绝对不要在生产环境用 sync你永远不知道它会执行什么 ALTER 语句生产环境表结构变更必须走 migration 工具。4. Express API 路由与业务逻辑实现模型有了接下来就是写接口。这个项目的接口风格是 RESTful API也就是用 HTTP 动词 资源路径来表达操作意图。比如POST /api/auth/register是注册POST /api/auth/login是登录GET /api/articles是获取文章列表这样的设计语义清晰也方便对接前端同学。4.1 入口文件 app.js 的完整结构require(dotenv).config(); const express require(express); const cors require(cors); const routes require(./routes); const errorHandler require(./middlewares/errorHandler); const { sequelize } require(./models); const app express(); const PORT process.env.PORT || 3000; // 全局中间件 app.use(cors()); app.use(express.json()); app.use(express.urlencoded({ extended: true })); // 路由 app.use(/api, routes); // 统一错误处理必须放在所有路由之后 app.use(errorHandler); // 启动前先检查数据库连接 (async () { try { await sequelize.authenticate(); console.log(MySQL 数据库连接成功); } catch (err) { console.error(数据库连接失败:, err.message); process.exit(1); } app.listen(PORT, () { console.log(API 服务已启动: http://localhost:${PORT}); }); })();我把sequelize.authenticate()放在服务启动之前是为了避免那种“代码没报错但接口全部 500”的情况——如果你没做这个检查服务照样启动但所有涉及数据库的接口都会抛异常排查很痛苦。4.2 注册与登录接口实战注册接口的逻辑比较简单接收用户名、邮箱、密码校验是否已存在然后 bcrypt 加密入库。注意密码加密这块绝对不能省明文存储密码在公司里属于安全事故级别的问题。const bcrypt require(bcryptjs); const jwt require(jsonwebtoken); const { User } require(../models); exports.register async (req, res, next) { try { const { username, email, password } req.body; if (!username || !email || !password) { return res.status(400).json({ message: 用户名、邮箱、密码不能为空 }); } // 检查用户名/邮箱是否已注册 const existingUser await User.findOne({ where: { [Op.or]: [{ username }, { email }] } }); if (existingUser) { return res.status(409).json({ message: 用户名或邮箱已被使用 }); } // 加密密码12 是 bcrypt 的常用工作因子 const passwordHash await bcrypt.hash(password, 12); const user await User.create({ username, email, passwordHash }); res.status(201).json({ id: user.id, username: user.username, email: user.email, createdAt: user.created_at }); } catch (err) { next(err); } };登录接口稍微不一样。密码比对不能用“解密”因为 bcrypt 是不可逆的哈希算法。正确做法是把用户传进来的密码和数据库里存的 hash 做比对验证exports.login async (req, res, next) { try { const { username, password } req.body; const user await User.findOne({ where: { [Op.or]: [{ username }, { email: username }] } }); if (!user) { return res.status(401).json({ message: 用户名或密码错误 }); } const isMatch await bcrypt.compare(password, user.passwordHash); if (!isMatch) { return res.status(401).json({ message: 用户名或密码错误 }); } const token jwt.sign( { userId: user.id, username: user.username }, process.env.JWT_SECRET, { expiresIn: 7d } ); res.json({ token, user: { id: user.id, username: user.username } }); } catch (err) { next(err); } };这里我用 JWTJSON Web Token来做身份认证。用户登录成功后拿到的 token之后每次请求放在Authorization: Bearer token请求头里。服务端不保存 session只看 token 签名是否有效、是否过期。这适合前后端分离和多个服务部署的场景因为无状态任何一个服务实例都能校验。4.3 文章 CRUD 接口与关联查询文章列表接口要支持分页、关键词搜索、按作者过滤这个接口基本涵盖了很多真实业务里“列表查询”的要素const { Article, User } require(../models); const { Op } require(sequelize); exports.list async (req, res, next) { try { const page parseInt(req.query.page) || 1; const pageSize parseInt(req.query.pageSize) || 10; const keyword req.query.keyword || ; const authorId req.query.authorId; const where {}; if (keyword) { where.title { [Op.like]: %${keyword}% }; } if (authorId) { where.user_id authorId; } const { rows, count } await Article.findAndCountAll({ where, include: [ { model: User, as: author, attributes: [id, username] } ], order: [[created_at, DESC]], limit: pageSize, offset: (page - 1) * pageSize }); res.json({ total: count, page, pageSize, data: rows }); } catch (err) { next(err); } };关于分页我用了findAndCountAll它一次返回符合条件的总数count和当前页数据rows。有些人喜欢先 count 再 findAll 分开写效果一样但findAndCountAll更简洁。offset的计算必须是(page - 1) * pageSize这个数学公式别想当然页码从 1 开始的话第一页 offset 就是 0。还有关联查询里我用了include把作者信息带出来但限制了attributes只返回id和username。这是为了避免把用户密码哈希也返回给前端这类安全性细节面试官和架构师都会关注。在设计 API 返回的字段时务必注意一个原则永远不要返回模型的所有字段给前端特别是密码、内部状态标记这类敏感字段必须用attributes白名单控制。4.4 统一的错误处理中间件Express 里最容易忽略的就是错误处理。默认情况下异步函数里的报错不会直接进到错误处理中间件而是挂起导致响应一直转圈。我给每个 controller 外面包了next(err)然后在middlewares/errorHandler.js里统一处理const errorHandler (err, req, res, next) { console.error(API 错误:, err.message); // Sequelize 唯一约束冲突 if (err.name SequelizeUniqueConstraintError) { return res.status(409).json({ message: 数据已存在 }); } // Sequelize 认证错误 if (err.name SequelizeConnectionError) { return res.status(500).json({ message: 数据库连接异常 }); } // 参数校验错误配合 express-validator if (err.name ValidationError) { return res.status(400).json({ message: err.errors }); } res.status(500).json({ message: 服务器内部错误 }); }; module.exports errorHandler;这种做法的好处是业务代码里只需要关心正常流程异常全部向上抛由统一出口处理日志也集中管理了。排查线上问题时你只需要看 errorHandler 里的日志就可以定位大部分问题。5. Docker 部署 MySQL 与项目容器化在没讲 Docker 之前每次换一台电脑或者接手同事的项目最痛苦的就是装环境。MySQL 版本不一致、密码配置麻烦、系统依赖缺三缺四。Docker 解决的就是“环境一致性”的问题。5.1 编写 docker-compose.yml 编排数据库我先用 Docker Compose 拉一个 MySQL 8.0 服务好处是团队同事拉下来就能跑version: 3.8 services: mysql: image: mysql:8.0 container_name: fullstack-mysql restart: always environment: MYSQL_ROOT_PASSWORD: root123456 MYSQL_DATABASE: fullstack_api TZ: Asia/Shanghai ports: - 3306:3306 volumes: - mysql_data:/var/lib/mysql command: - --character-set-serverutf8mb4 - --collation-serverutf8mb4_unicode_ci healthcheck: test: [CMD, mysqladmin, ping, -h, localhost, -proot123456] interval: 10s timeout: 5s retries: 5 volumes: mysql_data:关于 MySQL 8.0 的 utf8mb4 编码这里我多说一句MySQL 里的utf8是“假 utf8”最多只支持 BMP 字符集像 emoji 表情和一些生僻汉字存进去会变成乱码甚至报错。utf8mb4才是真正的完整 UTF-8 编码。所以务必在容器启动时加上这两个命令参数否则后面存文章内容遇到特殊字符就是事故现场。5.2 编写 Dockerfile 构建 Node 应用镜像FROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm install --production COPY . . EXPOSE 3000 CMD [node, app.js]有人会问为什么先拷贝 package.json 而不是整个目录这一步是 Docker 镜像缓存的关键技巧。Docker 在构建每一层时如果发现该层没有变化就会沿用缓存。package.json 我改的频率比源码低得多所以先拷贝依赖文件npm install这一层就能用缓存后面对源码的修改不会导致所有依赖重新装构建速度快好几倍。然后更新 docker-compose.yml 把应用加进去backend: build: . container_name: fullstack-backend restart: always depends_on: mysql: condition: service_healthy environment: - DB_HOSTmysql - DB_PORT3306 - DB_NAMEfullstack_api - DB_USERroot - DB_PASSWORDroot123456 ports: - 3000:3000depends_on这个配置有个细节默认的depends_on只控制启动顺序等 mysql 容器“启动完成”就算通过但没保证 MySQL 已经能接受连接。所以我加了condition: service_healthy等待 MySQL 的 healthcheck 通过之后才启动后端否则后端启动时去连数据库必然报连接拒绝ECONNREFUSED。5.3 Docker Desktop 环境常见问题快查我用 Docker Desktop 的时候踩过不少坑这里列几个最常见的现象原因解决办法Docker Desktop 启动失败提示 virtualization support not detectedWindows 没开启虚拟化或 WSL2 未安装BIOS 里开启 VT-x控制面板启用“适用于 Linux 的 Windows 子系统”容器内部无法访问宿主机 MySQL宿主机地址写成了 localhost容器里的 localhost 指的是容器本身要访问宿主机注意使用 host 网络模式或宿主机局域网 IPMySQL 容器启动后端口被占用宿主机已有 3306 听过修改宿主机端口映射比如3307:3306容器里跑 Node 服务连不上 MySQL报 ECONNREFUSED数据库还没就绪设置 healthcheck 并配置depends_on.condition: service_healthy之前有个朋友在 Windows 上折腾了一下午 Docker最后发现是“适用于 Linux 的 Windows 子系统”没装那 Docker Desktop 的核心引擎根本起不来。建议装之前先去系统设置—可选功能里确认 WSL 和虚拟机平台都已启用。6. 常见错误与本地调试心得每写完一批接口我会用 Apifox 批量自测。下面这表格里的都是我在这个项目里真实遇到过、花时间排查过的问题希望对你有帮助。错误信息可能原因排查步骤ER_ACCESS_DENIED_ERROR数据库密码不对或 root 仅限 localhost 访问检查 .env 和 docker-compose 里的密码是否一致SequelizeValidationError: isEmail failedSequelize 模型字段校验不通过对照接口传入参数是否满足模型定义里的 validate 规则Cannot read properties of undefined (reading id)请求 body 没拿到最可能是没加express.json()确认 app 里是否挂载 json 中间件connect ETIMEDOUT容器或宿主机之间网络隔离检查 Docker network 和防火墙规则401 unauthorizedJWT 缺失或过期检查 Authorization 请求头确认登录后拿到的 token 是否传对了位置SequelizeQueryError: Unknown column password模型字段名与数据库列名不一致关注field配置确认表是使用 sync 生成或手动建表的结构一致6.1 Debug 技巧善用 SQL 日志与打印响应体Sequelize 默认会打印输入的 SQL 语句很多人觉得烦所以把logging: false关掉了。但排查问题的时候把 console.log 打开超级有用——你可以直接看到 Sequelize 到底执行了什么 SQL是不是多带了一个你不想要的条件join 了哪些表。遇到列表接口返回数据和预期不一致时我习惯先把响应体原样打印出来再拿 SQL 去数据库客户端手动执行一遍。九成的 query 问题都能用这个方法定位。还有个取巧的办法在本地路由加一个临时端点只返回原始 SQL 日志不用反复重启也能观察到查询语句。6.2 生产不可用 sync那你该用啥前边说开发环境可以用sequelize.sync({ alter: true })但生产环境千万不要这么做。原因是sync不会删除数据库中已经存在但模型里不为知的多余列而且alter可能会触发破坏性的表重建尤其在大表上耗时极长。正确做法是用 Sequelize 官方 CLI 迁移工具npm install --save-dev sequelize-cli npx sequelize-cli init它会生成migrations和seeders目录然后用npx sequelize-cli migration:generate --name create-users-table npx sequelize-cli db:migrate迁移文件是 sqlite 或 JavaScript 格式记录每一次“把数据库从旧结构升级到新结构”的指令。这样每个部署环境的表结构都是可控、可回滚、可追溯的。当然本地小项目里 sync 比较爽但把习惯养好写迁移没什么坏处。6.3 调试接口时的常用套路用 Apifox 或 Postman 调试时我会做这几件事先跑通最简单的“无参”请求确认服务能起来、数据库能连上。加参数逐步扩展看模型的 validate 是否拦截。在 controller 开头加一行 console.log 打印req.body和req.params确认数据真的到达服务端。打开浏览器 DevTools 里的 Network 面板如果是 Web 端调接口看请求头和响应状态码区分是前端传参问题还是后端处理问题。绝大多数的 400 和 401问题出在请求头或请求体格式上不一定是后端逻辑写错了。7. “持续更新中”的规划我后续打算加什么标题既然写了“持续更新中”我确实不准备让它是个一次性的 demo。这个项目的下一批更新大概包含这几个方向7.1 用 Sequelize 迁移代替本地 sync前面提到 sync 有生产缺陷所以我会把项目的表结构导入迁移文件保证从开发到生产环境都能一键建表。这一块涉及到初始化迁移、生成业务迁移加列、改字段类型、种子数据写入基本可以单独开一篇文章。7.2 接口鉴权升级刷新令牌与角色权限目前只做了 JWT 签发但 token 一旦泄露就相当于账号被盗。更稳的做法是引入 refresh token长期有效和 access token短期有效双令牌机制再配合 RBAC 角色权限比如管理员能删任意文章普通用户只能操作自己的。这块技术点比较多值得写一篇细的。7.3 单元测试与自动化接口测试现在很多团队已经把接口测试自动化列为 CI 的一部分。这个项目会接入 Jest Supertest对注册、登录、文章 CRUD 写单元测试然后再配一个简单的 GitHub Actions 工作流每次提交代码自动跑测试。这些能在早期拦住回归问题比手动点接口靠谱得多。7.4 日志与监控接入生产环境的服务没有日志监控约等于裸奔。后面我会把日志输出收拢到文件按天滚动并且接入一个简单的告警比如接口 5xx 数量超过阈值就通知到企业微信或钉钉。这样线上出问题的时候你可以第一时间知道而不是等用户来骂。8. 我踩过坑之后总结的几点实操心得项目写到这里我觉得最有价值的不只是代码本身而是那些只有实际动手才会发现的坑和体会。第一配置先行。项目里所有的数据库密码、密钥、端口都是通过 dotenv 从 .env 读取.env 本身绝不提交到 Git。如果你在一个团队工作大概率你需要给新同事发一份.env.example加上说明文档让他在两分钟之内就能跑起来。为了这个目标我专门把 .env.example 里的注释写得跟教程一样清楚。第二强约束命名规范。字段名全部统一为snake_case下划线风格模型里的 JS 属性用 camelCase 并用field映射。这样数据库 DBA 看到的名字是created_atJavaScript 开发者看到的是createdAt。虽然初期多写十几个映射但后续跨部门协作、数据字典评审都会顺畅不少。第三别迷信框架的“自动魔法”。Sequelize 确实帮你生成了 SQL但底层发生了什么你心里要有数。我建议新手阶段把logging打开SQL 日志当成学习材料看看完正经 SQL 规范再回来优化写法比盲写findAll高效得多。最后说一句有点得罪人的话技术选型不在于最新而在于你能否基于它构建一条稳定、可复制的工程链路。Express Sequelize MySQL Docker 这套组合我用了两年从几万条数据的小站点到百万级日活的小型中台系统都扛得住。你把上面的内容玩转以后再去看 NestJS、Prisma、PostgreSQL 这些会发现核心思路其实都是一样的。如果你照着这个教程搭好了第一版 API 项目我建议你先做一件事给自己写的接口写一份简单的 README 文档。不用很复杂把每个接口的地址、请求参数、返回结构列出来就行。等你半个月后再回来看会发现这份文档比代码本身更能帮你理清需求逻辑。这也是我从这次项目里学到的最重要的一课。
返回列表