ARTICLE DETAIL

资讯详情

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

Express+MySQL脚手架完全指南:半小时搭建增删改查接口

Express+MySQL脚手架完全指南:半小时搭建增删改查接口 简介这是一份基于Node.js、Express与MySQL的快速开发脚手架面向需要快速搭建后端服务或学习全栈项目结构的初中级开发者。脚手架预置了标准的项目分层与基础配置涵盖数据库连接、API路由、服务封装、通用工具模块以及文档说明开发者拿到后只需根据业务需求调整扩展即可快速启动稳定可扩展的Web应用省去从零搭建框架与配置环境的时间。资源为zip压缩包共31个文件以JavaScript脚本为主辅以Markdown文档、JSON配置文件、HTML页面及YAML等js文件对应入口、路由、服务与工具函数md/json等用于项目说明与依赖配置整体仅37KB轻量易用。目前已有31人学习下载适合作为个人项目起步模板也便于团队统一开发规范、减少环境差异。1. 这个脚手架解决什么半小时从空 zip 到跑通增删改查接口拿到一个“基于 nodeexpressmysql 快速开发脚手架”的 zip 包第一反应通常是解压后直接npm install但这类包真正值钱的不是那几个文件而是它帮你把“node 环境装到哪个版本、express 路由怎么挂、mysql 连接串从哪读、接口返回什么格式”这些重复决策一次性定了下来。它面向两类人刚接手 Node 后端、不想从空目录开始搭底子的初级开发以及同时维护多个内部系统、需要统一技术栈和代码风格的中级工程师。简单说它不解决业务只解决“底子”——让你在半小时内跑通一个能连上 mysql、能增删改查、日志和报错都看得懂的骨架。下文会照着这个 zip 最常见的组织方式把它拆开讲透再给你一份能直接复现的落地方案。2. 拆开脚手架看结构入口、db 封装与路由挂载的三条主线拿到压缩包不要急着双击点开某个 readme先看三个文件package.json、入口文件通常是app.js或server.js、db相关模块。把这三条主线看懂整个脚手架的脾气就摸清了一半。2.1 入口文件与依赖清单先读 package.json 再动手一个合格的脚手架会把依赖写得克制。常见的依赖不外乎express、mysql2、cors、dotenv再加一个开发热重载工具nodemon。如果看到依赖列表里堆了十几个中间件启动脚本写得花里胡哨这个包反而不值得直接拿来用——因为你不知道哪些配置是作者项目残留哪些是通用模板。{ name: express-mysql-scaffold, version: 1.0.0, main: app.js, scripts: { start: node app.js, dev: nodemon app.js }, dependencies: { express: ^4.19.2, mysql2: ^3.10.0, cors: ^2.8.5, dotenv: ^16.4.5 }, devDependencies: { nodemon: ^3.1.0 } }这段清单的逻辑很简单express提供 Web 框架mysql2比老牌的mysql包多了 Promise 原生支持和预处理语句dotenv用来读环境变量cors解决本地联调时的跨域问题。nodemon只装进开发依赖生产环境用node app.js直接起这是 Node 项目最常见的分工方式。看scripts字段时要留个心眼如果start脚本里带了NODE_ENVproduction这类赋值在 Windows 的 cmd 里是跑不起来的需要cross-env才能跨平台。后面排查章节会专门说这个坑。入口文件是理解脚手架的第二个钥匙。我一般会先看它干了四件事读配置、连数据库、挂路由、起服务。四件事的顺序如果乱了很容易出现“路由先注册但数据库还没连上”的幽灵问题。// app.js 入口文件的典型组织方式 const express require(express); const cors require(cors); const dotenv require(dotenv); dotenv.config(); // 1. 先读 .env后续所有配置都依赖它 const db require(./db); // 2. 初始化数据库连接池 const userRouter require(./routes/user); const orderRouter require(./routes/order); const app express(); app.use(cors()); // 3a. 跨域中间件 app.use(express.json()); // 3b. 解析 JSON 请求体 app.use(/api/user, userRouter); app.use(/api/order, orderRouter); // 4. 统一 404 与错误处理要放在所有路由之后 app.use((req, res) { res.status(404).json({ code: 404, msg: not found }); }); const PORT process.env.PORT || 3000; app.listen(PORT, () { console.log(server running at http://localhost:${PORT}); });入口文件的顺序本身就是行为dotenv.config()必须最先执行否则process.env.PORT读到的是 undefined端口回落到 3000。错误处理中间件放最后是 Express 的硬性规定如果放在路由之前所有正常请求都会被拦下来。这里把 404 处理写成了一个返回 JSON 的中间件保证了接口风格统一而不是丢一个 HTML 错误页。2.2 db 模块的封装思路连接池 Promise 才是能上线的形态很多新手拿到脚手架后会困惑为什么连接 mysql 不直接mysql.createConnection而要绕一层连接池原因很简单createConnection是单连接每次请求都新建连接高并发时 mysql 服务端会报Too many connections而且每次握手都有开销。连接池的思路是提前建一批连接放在池子里请求来了借一条用完还回去这正是脚手架里最常见的 db 模块写法。// db/index.js 连接池封装示例 const mysql require(mysql2/promise); const pool mysql.createPool({ host: process.env.DB_HOST || 127.0.0.1, port: Number(process.env.DB_PORT || 3306), user: process.env.DB_USER || root, password: process.env.DB_PASSWORD || , database: process.env.DB_NAME || scaffold, waitForConnections: true, connectionLimit: 10, queueLimit: 0, charset: utf8mb4 }); async function query(sql, params) { const [rows] await pool.execute(sql, params); return rows; } async function getConnection() { return await pool.getConnection(); } module.exports { pool, query, getConnection };注意这里用的是mysql2/promise而不是mysql2默认导出的回调版。这么写的好处是pool.execute直接返回[rows, fields]配合async/await后业务代码里不再有回调地狱。query函数把最常用的查询场景收口成一个方法业务路由里只关心 SQL 和参数。参数说明里有几个值是必须确认的connectionLimit决定连接池上限对大部分内部系统 10 就够用但如果接口里同时有慢查询这个值要调大后面有专门章节讲。queueLimit为 0 表示连接池满了之后请求无限排队生产环境建议设个有限值否则请求会一直挂起前端表现就是接口迟迟不返回。charset用utf8mb4而不是utf8因为utf8在 mysql 里最多存 3 字节遇到 emoji 和生僻字会直接报错。这个配置几乎每个新人都踩过。2.3 路由挂载与中间件顺序Express 里“先注册先生效”不是玄学路由文件通常是脚手架里业务密度最高的地方。一个典型的业务路由文件会把 CRUD 接口按资源拆分每个接口只做一件事校验参数、调用 db 层、返回统一格式。这里有个常见反模式是把 SQL 直接写在路由文件里几十行 SQL 混着响应逻辑看着能跑实际上没法维护。// routes/user.js 路由模块示例 const express require(express); const router express.Router(); const { query } require(../db); // GET /api/user/:id 查询单个用户 router.get(/:id, async (req, res, next) { try { const id Number(req.params.id); if (Number.isNaN(id)) { return res.status(400).json({ code: 400, msg: id 必须为数字 }); } const rows await query( SELECT id, username, email FROM user WHERE id ?, [id] ); if (rows.length 0) { return res.status(404).json({ code: 404, msg: 用户不存在 }); } res.json({ code: 0, data: rows[0] }); } catch (err) { next(err); // 把错误抛给全局错误处理中间件 } }); module.exports router;这段代码里?占位符配合params数组传参是防 SQL 注入的正确姿势千万不要用字符串拼接把id拼进 SQL。Number(req.params.id)做了一次显式类型转换因为路径参数默认是字符串1和1在 JavaScript 里相等但在 SQL 里可能触发隐式转换影响索引命中。所有接口的返回格式统一为{ code, data, msg }前端解析时只看code是否为 0这套约定比直接返回裸数据要省事得多。挂载顺序上脚手架里常见的心智模型是全局中间件在最前业务路由按功能划分依次挂载404 和错误处理永远在最后。如果你发现某个接口总是执行不到多半是前面挂了个app.use(/api, xxx)把请求吞掉了。Express 的中间件是洋葱模型next()不调用请求就停在那层这是排查路由问题时首先要检查的。3. 从模板到跑通环境准备、建库建表与最小接口复现把 zip 解压到本地只是第一步真正让它跑起来还需要过三道关node 环境对不对、mysql 实例通不通、配置项是否指向你的库。这一章按操作顺序把完整流程走一遍每个命令都给出失败时看什么。3.1 环境准备用 nvm 固定 node 版本再装依赖脚手架一般会在package.json里声明engines字段但很多包没写导致你用的是 node 20作者是在 node 14 下调的某些老版本依赖会编译失败。我的做法是用nvm安装并切换 node 版本先看项目要求的版本没有要求就用当前 LTS。用 nvm 而不是直接去官网下载安装包是因为同一个机器上可能要切换多个 node 版本直接装会把node、npm写死在系统 PATH 里后面升级成本很高。# 查看当前 node 与 npm 版本 node -v npm -v # 用 nvm 安装并切换到指定版本示例为 LTS 版本 20.x nvm install 20 nvm use 20 # 进入项目目录安装依赖 cd express-mysql-scaffold npm installnpm install出现红色报错时不要急着重装先看报错的前三行。常见的node-gyp编译错误、python not found这类问题通常是本地缺少编译工具链跟项目本身关系不大如果是ERESOLVE依赖树冲突多半是某个包版本过老考虑升级依赖而不是硬刚。安装完成后执行npm audit看一眼漏洞报告高危漏洞集中在express4.x 旧版本时要慎重决定是否继续用这个脚手架而不是明知有洞还往下走。3.2 建库建表与配置替换把占位配置改成你的 mysql 实例参数脚手架里的.env文件通常是占位状态比如DB_PASSWORDyour_password。这一步要把它改成你本地 mysql 的真实参数。先确认 mysql 服务真的在跑而不是启动了但端口被占用。Windows 上常见错误是装了 mysql 但没把bin目录加进 PATH导致mysql命令找不到这时候要用绝对路径或先配置环境变量。# 登录本地 mysql验证账号密码可用 mysql -u root -p # 创建脚手架示例要用的数据库 CREATE DATABASE IF NOT EXISTS scaffold DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci; USE scaffold;数据库字符集建库时就要定好否则后面每张表都要单独ALTER。utf8mb4_unicode_ci是通用选择排序规则对中文比较友好如果你的业务里有大量按拼音排序的需求可以考虑utf8mb4_general_ci但差异只在极端场景下才明显。表结构创建一个用户表就够了字段不要多够跑通 CRUD 即可。CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL, email VARCHAR(100) DEFAULT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;表结构里两个细节值得说id用AUTO_INCREMENT分布式场景下这个方案不够用但脚手架阶段这样最省事created_at用DATETIME而不是TIMESTAMP因为TIMESTAMP有 2038 年问题而且会受时区影响新手阶段用DATETIME少踩一个坑。建完表后回到.env文件把DB_HOST设为127.0.0.1而不是localhost原因是 mysql2 在某些 node 版本下解析localhost会走 socket 而不是 TCP导致连接报错。3.3 启动服务并验证第一个接口curl 与日志双确认配置改完、依赖装好、表建完剩下就是启动和验证。这里的关键是“先确认动了哪个端口”防止服务起来了但你访问的是另一个应用。# 开发模式启动nodemon 会监听文件变化自动重启 npm run dev # 另开一个终端验证服务进程和端口 curl http://localhost:3000/api/user/1如果curl返回{code:0,data:{...}}说明整条链路是通的。返回404先查路由路径是否拼错返回500去终端看堆栈最常见的是数据库连接失败或表名不存在终端没有任何输出说明请求都没进到 node 进程去查端口是否被防火墙挡了。这里要养成一个习惯起服务后先看控制台有没有server running日志再看有没有数据库连接报错。很多脚手架会在启动时主动SELECT 1做一次连通性测试如果没有这个机制第一次请求才会暴露数据库问题排错时容易被“明明启动了却报 500”搞晕。4. 要改就改对地方连接池、事务与统一响应的参数边界脚手架能用和好用之间隔着一层参数调优。这一章挑出六个必调参数覆盖连接池、事务、日志与响应格式每个都给出改到多少、影响什么、改坏了怎么回退。4.1 连接池参数并发场景下先改 connectionLimit第 2 章代码里的连接池参数在本地开发时用默认值没问题一旦接口压力上来最先崩溃的点往往不是 SQL 写得差而是连接池耗尽。表现是前端请求一直转圈mysql 端SHOW PROCESSLIST看到大量Sleep连接node 应用日志里出现Timeout类的异常。参数默认值建议调优方向副作用connectionLimit10压测时逐步上调到 30~50超过 mysql 端 max_connections 会直接拒连queueLimit0生产建议设为 500队列过长会积压内存connectTimeout默认 10s内网可降到 5s值太小在 mysql 重启时会误报acquireTimeout默认 10s池满时低于此值会让请求快速失败前端能收到错误而不是无限等待connectionLimit不是越大越好。每个连接在 mysql 端都是一个线程内存占用不可忽略如果同时跑着多个 node 实例各自连接池的上限加总要小于 mysql 的max_connections否则就是自己把数据库打挂。线上排查时先SHOW VARIABLES LIKE max_connections看总上限再按实例数均分。queueLimit设为 0 在本地没问题但在生产环境等于允许请求无限排队。一个慢查询把 10 个连接全占住后面几千个请求全部挂在队列里表现为内存飙升但接口全部超时。我一般把queueLimit设为connectionLimit的 50 倍排队超过这个数就让请求快速失败返回 503前端能立刻感知到服务过载而不是傻等。4.2 事务与异常处理手动提交回滚别赌 autocommit 的默认行为mysql 默认开启 autocommit单条 SQL 自动提交但“扣库存 写订单”这类多步操作一旦第二步失败第一步已经提交了数据就对不上。脚手架里最常见的错误是直接在业务代码里连写三个await query()不做事务包裹开发时数据量小看不出问题压测时并发一高脏数据立刻冒出来。// services/orderService.js 事务处理正确姿势 const { getConnection } require(../db); async function createOrder(userId, items) { const conn await getConnection(); // 从池子里借一条连接 try { await conn.beginTransaction(); // 显式开启事务 // 插入订单主表 await conn.execute( INSERT INTO order (user_id, total_amount) VALUES (?, ?), [userId, 100] ); const orderId conn.insertId; // 插入订单明细 for (const item of items) { await conn.execute( INSERT INTO order_item (order_id, product_id, quantity) VALUES (?, ?, ?), [orderId, item.productId, item.quantity] ); } await conn.commit(); // 全部成功才提交 return orderId; } catch (err) { await conn.rollback(); // 任何一步失败回滚所有修改 throw err; } finally { conn.release(); // 归还连接不是关闭连接 } }这段代码有几个关键点事务必须用同一条连接执行所以不能用模块里封装的query而要getConnection单独借连接。conn.release()放在finally里保证一定执行否则事务过程中抛异常连接没归还连接池会被慢慢占满。rollback之后要throw err让上层错误处理中间件统一记录日志和返回 500不能在 catch 里吞掉错误。这里顺带提一个高频问题误把commit放在循环里执行。循环里每次execute后提交一次等于把事务切割成了多段中间某个点失败前面几段已经落库。事务的边界是整个业务操作不是单条 SQL。4.3 跨域、日志与响应格式三个中间件的配置边界脚手架自带的中间件通常是最简配置但在联调阶段会暴露问题。跨域中间件cors如果直接app.use(cors())等于开放了所有来源本地联调没问题上线前必须收紧。// 跨域中间件配置示例 app.use(cors({ origin: process.env.ALLOW_ORIGIN ? process.env.ALLOW_ORIGIN.split(,) : *, methods: [GET, POST, PUT, DELETE], allowedHeaders: [Content-Type, Authorization], maxAge: 86400 }));origin从环境变量读取意味着部署时可以通过ALLOW_ORIGIN列出合法来源而不是改代码。maxAge设置预检请求的缓存时间一天内同一来源的复杂请求不再重复发 OPTIONS能明显减少请求量。注意allowedHeaders没加Authorization的话前端带 token 的请求会被浏览器拦下报 CORS 错误这个字段最容易漏。日志中间件不要用console.log到处打点而是固定一个请求日志格式时间、方法、路径、状态码、耗时。脚手架里常见做法是写一个几行的中间件不额外引第三方日志库够用且无依赖。// 请求日志中间件示例 app.use((req, res, next) { const start Date.now(); res.on(finish, () { const cost Date.now() - start; console.log(${new Date().toISOString()} ${req.method} ${req.originalUrl} ${res.statusCode} ${cost}ms); }); next(); });这个中间件挂在路由之前res.on(finish)在响应结束时触发能拿到真实状态码和耗时。统一响应格式的约定在第 2 章提过这里补充一点分页接口的返回格式建议固定为{ code, data: { list, total, page, pageSize }, msg }前后端都按这个契约走避免每个接口各写各的。脚手架不会帮你约束这个但你在改业务代码时应该主动遵守。5. 常见问题排查新手上线前最容易踩的 6 个坑这一章把从“本地跑通”到“让同事也能跑通”之间最常遇到的 6 个问题摊开按现象、原因、解决的顺序写。每条都是我见过不止一次的真实事故不是从文档里抄来的理论。5.1 环境与启动类Windows 下的 PATH、端口占用和 npm 脚本兼容坑 1启动项目时报NODE_ENV 不是内部或外部命令现象npm start直接报错命令执行失败。原因package.json 的start脚本里写了NODE_ENVproduction node app.js这是 Linux/macOS 的写法Windows cmd 不认这种内联环境变量赋值。解决要么把脚本改成cross-env NODE_ENVproduction node app.js先安装cross-env作为开发依赖要么干脆不在脚本里写环境变量改用.env文件通过dotenv读取。这个问题在团队成员混用 Windows 和 macOS 时一定会炸出来最稳妥的方案是后者因为 dotenv 本身就跨平台。坑 2mysql命令找不到或者 mysql 服务启动失败现象执行mysql -u root -p提示命令不存在或者net start mysql提示服务名无效。原因Windows 安装 mysql 时选了不加入 PATH或者安装的是 zip 解压版没有手动注册服务。另一类是从官网下载了 msi 安装包但没选“安装为 Windows 服务”导致每次启动都要手动mysqld --console。解决把 mysql 的bin目录加进系统 PATH服务方式安装的执行mysqld --install然后用net start mysql启动。端口冲突时先查netstat -ano | findstr 3306确认占用进程是另一个 mysql 实例还是其他应用如果是其他应用占用了 3306改 mysql 的port配置比强制杀进程更安全。坑 3npm install时 node-gyp 报错依赖装不上现象安装过程中出现gyp ERR! find Python或MSB4019最终npm install失败。原因部分依赖包含原生 C 模块需要编译工具链Windows 上是 Visual Studio Build ToolsLinux 上是python3和make。解决这不是脚手架的问题是机器缺编译环境。Windows 上安装windows-build-tools或者直接换用 node 高版本因为新版 node 自带预编译二进制省去本地编译。这个坑的教训是如果脚手架依赖很冷门的包优先怀疑依赖本身不值得用而不是为它配编译环境。5.2 数据库与业务类认证插件、排序规则和 Docker 网络坑 4ER_NOT_SUPPORTED_AUTH_MODE或Client does not support authentication protocol requested by server现象node 应用连 mysql 报认证协议不支持但用 Navicat 连同一个库却正常。原因mysql 8.x 默认认证插件是caching_sha2_password旧版 mysql2 驱动只认mysql_native_password。解决先升级mysql2到较新版本它已经支持caching_sha2_password如果项目不方便升级降低 mysql 用户的认证插件ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码;。但这里要提醒改认证插件只是绕路新项目应该跟随驱动升级别在新库上迁就旧驱动。坑 5中文排序结果不对ORDER BY出来的顺序跟字典序不一致现象查询用户列表时按username排序中文名的顺序乱七八糟不是按拼音排的。原因表和字段的COLLATE排序规则设置不当或者字段类型是utf8mb4_general_ci但在 SQL 里用了ORDER BY后又加了不同的COLLATE子句。解决优先在建表时统一用utf8mb4_unicode_ci它按 Unicode 编码排序对中文更合理。实在要按拼音排推荐在应用层排序而不是依赖 mysql 的CONVERT因为 mysql 的拼音排序要写ORDER BY CONVERT(name USING gbk)这是个 hack性能差且依赖特定字符集实现换到其他数据库就废了。坑 6mysql 跑在 Docker 容器里node 应用本地连不上现象用localhost:3306连 docker 里的 mysql 报ECONNREFUSED但docker exec -it 容器 mysql -u root -p能正常登录。原因node 应用的localhost解析走了 IPv6 的::1而 docker 端口映射默认绑在0.0.0.0:3306即 IPv4 才有或者容器启动时没加-p 3306:3306映射端口。解决先确认容器启动命令里有没有端口映射docker ps看PORTS列没有就重建容器并加-p 3306:3306。连接串里把localhost改成127.0.0.1强制走 IPv4。如果还连不上查容器日志docker logs 容器看 mysql 是否真的启动完成有时候容器起来了但 mysql 初始化没跑完需要等几秒。6. 把三层验证变成固定动作冒烟、压测与配置外置脚手架只是起点真正让它变得可信的是你给它加的三层验证。我在每个基于这个模板的新项目里都会先把这三件事做成固定脚本之后再改任何代码都不会心里发虚。第一层是冒烟脚本用curl把核心接口跑一遍断言状态码和返回字段。不要依赖浏览器手工点写成一个 bash 或node脚本改动接口后执行一次能立刻发现路由挂载、参数校验和数据库连接的整体是否正常。第二层是简单的压测。压测工具选择很多但目标不是秀工具而是确认连接池参数和 mysql 的max_connections匹配。我会在 staging 环境跑一次 50 并发、持续 1 分钟的请求观察两个指标接口 95 分位耗时和错误率。如果错误率高八成是connectionLimit太小或某条 SQL 没有走索引这时候不要盲目调大连接池先用EXPLAIN看 SQL 执行计划。第三层是配置外置。环境变量不仅覆盖数据库配置还要覆盖端口、跨域来源、日志级别。这一步的意义在于脚手架内联的默认值只服务于本地开发部署到测试机、生产机时不应该改任何代码文件。把.env.example提交到仓库真实.env留在本地和服务器新同事入职复制一份示例文件就能跑。三件套做完脚手架就不再是别人给的 zip而是你自己维护的基础设施。我的教训是不要因为模板跑通了就急着堆业务代码先用这三层验证把底盘焊死。尤其是压测这一层很多项目上线后第一次出问题回溯原因往往是脚手架阶段的连接池参数没调过默认值太小扛不住真实流量。希望这个章节能帮你少走这一段弯路。本文还有配套的精品资源点击获取
返回列表