news 2026/10/23 21:18:45

Node.js + Express 从零搭建 API 服务:AI 辅助实战与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Node.js + Express 从零搭建 API 服务:AI 辅助实战与避坑指南

1. 项目缘起与整体设计思路

1.1 为什么选这个题目练手

做了几年后端开发,带过不少新人,我发现一个很普遍的现象:很多人学Node.js和Express,看完文档、跟着教程敲完一个Hello World,然后就不知道下一步该干什么了。知识点是散的,串不起来。你问他中间件是什么,他能背出定义;你让他从零写一个能跑通的API服务,他卡在目录结构上。

这个实战项目的出发点就是解决这个问题。用AI辅助,从零搭一个完整的API服务,麻雀虽小五脏俱全——有路由、有中间件、有数据校验、有错误处理、有日志、有环境变量管理。做完这一遍,你对一个后端服务的基本骨架就有肌肉记忆了。

适合谁看?如果你已经会JavaScript基础语法,知道const、箭头函数、async/await是什么,但没完整做过一个后端项目,那这篇就是写给你的。如果你已经做过几个Express项目,也可以看看我在AI协作和工程化细节上的处理方式,说不定有能借鉴的地方。

1.2 技术选型背后的考量

为什么是Node.js + Express,而不是Koa、Fastify或者NestJS?这个问题我在动手前认真想过。

Express是目前生态最成熟的Node.js Web框架,没有之一。中间件数量多、社区答案全、遇到问题搜索出来的结果最多。对于练手项目来说,这一点极其重要——你不想在排查一个基础问题时,发现全网只有三个相关讨论,其中两个还没人回答。

Node.js这边,我建议直接用LTS版本。写这篇文章时Node.js 20是活跃LTS,Node.js 22也已经是LTS了。版本选择上有个原则:生产环境用LTS,练手也用LTS。不要追Current版本的新特性,除非你明确知道自己在做什么。LTS意味着更长的维护周期和更稳定的依赖兼容性。

AI在这个项目里扮演什么角色?我的定位是:AI是结对程序员,不是代驾。它帮你生成样板代码、补充边界情况、解释报错信息,但架构决策、目录组织、错误处理策略这些需要你自己想清楚。如果你全程让AI生成然后复制粘贴,做完这个项目你什么也学不到。

1.3 最终的项目结构长什么样

在动手写代码之前,先把目录结构定下来。这一步很多人会跳过,直接npm init然后开始写app.js,写到后面发现所有代码挤在一个文件里,改一处牵动全身。

我最终采用的结构是这样的:

mini-api/ ├── src/ │ ├── app.js # Express应用配置 │ ├── server.js # 服务启动入口 │ ├── routes/ │ │ └── users.js # 用户相关路由 │ ├── middleware/ │ │ ├── logger.js # 请求日志中间件 │ │ ├── validate.js # 参数校验中间件 │ │ └── errorHandler.js # 统一错误处理 │ └── utils/ │ └── response.js # 统一响应格式封装 ├── .env # 环境变量(不提交到git) ├── .env.example # 环境变量模板 ├── .gitignore ├── package.json └── README.md

这个结构不算复杂,但覆盖了一个API服务最核心的几个关注点:应用配置与启动分离、路由按业务模块拆分、中间件独立管理、工具函数统一存放。后面加新功能,往对应目录里塞文件就行,不会乱。

注意:app.js和server.js分开是有意为之。app.js只负责创建和配置Express应用,server.js负责监听端口。这样做的好处是测试的时候可以直接引入app.js,不用真的启动一个HTTP服务。

2. 环境搭建与AI协作的实操细节

2.1 Node.js环境准备与版本管理

安装Node.js这件事本身没什么难度,但有几个细节值得说。

去Node.js官网下载LTS版本,Windows和macOS都有安装包,一路下一步就行。Linux用户如果用Ubuntu,不要用apt install nodejs,那个版本通常很旧。用NodeSource的源来装:

curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - sudo apt-get install -y nodejs

装完之后验证:

node -v # 应该输出 v20.x.x npm -v # 应该输出 10.x.x

如果你电脑上已经有其他Node.js版本,或者以后需要在不同版本之间切换,建议装一个版本管理工具。Windows上用nvm-windows,macOS和Linux上用nvm。这东西的好处是,你可以在Node.js 18、20、22之间随意切换,不同项目用不同版本互不干扰。

# 安装nvm后 nvm install 20 nvm use 20 nvm alias default 20 # 设为默认版本

实操心得:我见过太多人因为Node.js版本不对导致npm install报错,排查半天以为是网络问题。养成习惯,拿到一个新项目先看package.json里的engines字段,确认Node.js版本要求。

2.2 初始化项目与依赖安装

新建目录,初始化:

mkdir mini-api && cd mini-api npm init -y

-y表示全部用默认值,之后可以手动改package.json。我习惯把main改成src/server.js,scripts里加上start和dev:

{ "name": "mini-api", "version": "1.0.0", "main": "src/server.js", "scripts": { "start": "node src/server.js", "dev": "node --watch src/server.js" } }

Node.js 18.11以上自带--watch模式,改代码自动重启,不需要额外装nodemon。这个细节很多人不知道,还在用nodemon。当然nodemon功能更丰富,但练手项目用--watch足够了。

接下来装依赖:

npm install express dotenv

就这两个。express是框架,dotenv用来加载.env文件里的环境变量。其他东西暂时不需要,用到再加。

注意:不要一上来就装一堆依赖。每装一个包,问自己“现在真的需要吗”。依赖越多,安全漏洞风险越大,版本冲突概率越高。练手项目更是如此,保持精简。

2.3 用AI辅助开发的正确姿势

这个环节我想多说几句,因为很多人用AI写代码的方式是低效甚至有害的。

错误用法:打开AI对话窗口,输入“帮我写一个Express API服务”,然后把生成的代码全部复制到项目里。结果代码能跑,但你不知道每一行在干什么,出了问题完全无法排查。

正确用法:把AI当成一个随时可以问的资深同事。具体来说:

第一,让AI解释代码而不是生成代码。比如你写了一个中间件,不确定next()的调用时机对不对,可以把代码贴给AI,问“这个中间件的执行顺序是什么,如果我不调用next()会发生什么”。这种用法能帮你建立正确的心理模型。

第二,让AI补充边界情况。你写了一个创建用户的接口,可以让AI帮你想想“这个接口有哪些边界情况没考虑到”。它可能会提醒你:请求体为空怎么办、字段类型不对怎么办、重复创建怎么办。这些你自己想可能要花不少时间。

第三,让AI帮你读报错。Node.js的报错信息有时候比较隐晦,特别是涉及异步操作的时候。把完整报错贴给AI,让它解释可能的原因和排查方向,比你自己瞎试效率高得多。

第四,关键代码自己写。路由处理逻辑、错误处理策略、数据校验规则,这些核心部分必须自己动手。AI可以帮你检查,但不能替你思考。

实操心得:我习惯在AI给出建议后,追问一句“为什么这样做,有没有其他方案”。很多时候AI的第一反应不是最优解,追问能逼出更多信息,也帮你理解不同方案之间的取舍。

3. 核心模块的编码实现与原理拆解

3.1 Express应用配置:app.js的每一行都有意义

先看src/app.js的完整代码:

const express = require('express'); const logger = require('./middleware/logger'); const usersRouter = require('./routes/users'); const errorHandler = require('./middleware/errorHandler'); const app = express(); // 解析JSON请求体 app.use(express.json()); // 解析URL编码的请求体 app.use(express.urlencoded({ extended: true })); // 自定义日志中间件 app.use(logger); // 挂载路由 app.use('/api/users', usersRouter); // 404处理 app.use((req, res) => { res.status(404).json({ code: 404, message: '接口不存在', data: null }); }); // 统一错误处理(必须放在最后) app.use(errorHandler); module.exports = app;

逐段拆解。

express.json()和express.urlencoded()是内置中间件,分别处理Content-Type: application/json和application/x-www-form-urlencoded的请求体。没有这两行,req.body就是undefined。这是新手最容易踩的坑之一——前端明明发了数据,后端拿不到,排查半天发现是没加解析中间件。

app.use(logger)挂载自定义日志中间件。中间件的执行顺序就是app.use的注册顺序,所以日志中间件放在路由之前,这样每个请求都会先经过日志记录。

app.use('/api/users', usersRouter)把用户路由挂载到/api/users路径下。路由文件里定义的/、/:id等路径,实际访问时都要加上/api/users前缀。这种拆分方式让路由管理清晰很多。

404处理放在所有路由之后。如果前面的路由都没有匹配到请求,就会走到这里。注意这个中间件没有next参数,因为它就是最后一站了。

errorHandler必须放在最后。Express的错误处理中间件有四个参数(err, req, res, next),Express通过参数个数来识别它是不是错误处理中间件。如果放在路由之前,路由里抛出的错误就传不到它这里。

注意:app.use((req, res) => {...})这个404处理有个细节——它只处理没有匹配到路由的请求。如果路由匹配到了但处理函数里抛了错,会直接跳到错误处理中间件,不会经过这个404处理。

3.2 日志中间件:记录什么、怎么记

src/middleware/logger.js:

function logger(req, res, next) { const start = Date.now(); // 响应结束后记录日志 res.on('finish', () => { const duration = Date.now() - start; const log = { method: req.method, url: req.originalUrl, status: res.statusCode, duration: `${duration}ms`, time: new Date().toISOString() }; console.log(JSON.stringify(log)); }); next(); } module.exports = logger;

这个中间件做了三件事:记录请求方法、记录请求路径、记录响应状态码和耗时。

关键点在于res.on('finish', ...)。为什么不在next()之前直接打印日志?因为那时候响应还没发出去,你拿不到最终的状态码。finish事件在响应完全发送后触发,这时候res.statusCode才是最终值。

耗时计算用Date.now()取时间戳差值。这个精度对练手项目足够了。生产环境可能用process.hrtime.bigint()获取纳秒级精度,但没必要。

日志格式用JSON字符串。为什么不用模板字符串拼一个好看的格式?因为JSON格式可以直接被日志收集系统解析。你现在可能觉得无所谓,但养成这个习惯有好处——以后接入ELK或者类似系统时,不用改代码。

实操心得:日志里记录req.originalUrl而不是req.url。originalUrl保留完整的原始路径,包括挂载点前缀。req.url在路由匹配后可能会被改写,丢失前缀信息。

3.3 参数校验中间件:把校验逻辑抽出来

参数校验是API服务里重复度最高的逻辑之一。每个创建、更新接口都要校验字段是否存在、类型是否正确。如果每个路由处理函数里都写一遍if (!req.body.name) {...},代码会非常臃肿。

我的做法是写一个校验中间件工厂函数:

// src/middleware/validate.js function validate(schema) { return (req, res, next) => { const errors = []; for (const [field, rules] of Object.entries(schema)) { const value = req.body[field]; if (rules.required && (value === undefined || value === null || value === '')) { errors.push(`${field} 是必填字段`); continue; } if (value !== undefined && rules.type && typeof value !== rules.type) { errors.push(`${field} 必须是 ${rules.type} 类型`); } if (value !== undefined && rules.minLength && value.length < rules.minLength) { errors.push(`${field} 长度不能少于 ${rules.minLength} 个字符`); } } if (errors.length > 0) { return res.status(400).json({ code: 400, message: '参数校验失败', errors }); } next(); }; } module.exports = validate;

使用方式:

router.post('/', validate({ name: { required: true, type: 'string', minLength: 2 }, email: { required: true, type: 'string' } }), (req, res) => { // 到这里说明参数已经校验通过 });

这个设计的好处是校验规则和业务逻辑分离。路由处理函数只关心业务,校验的事情交给中间件。加新字段只需要改schema,不用动处理函数。

当然这个校验器很简单,没有覆盖所有情况。比如邮箱格式校验、数字范围校验、嵌套对象校验都没做。但对于练手项目来说,这个程度刚好——你能看懂每一行,也能根据自己的需求扩展。

注意:校验中间件里用了continue跳过当前字段的后续校验。这意味着如果一个字段既必填又类型不对,只会报“必填”错误。这个行为是合理的——字段都没填,讨论类型没有意义。

3.4 统一响应格式:让前端好过一点

API返回的数据格式应该统一。不要这个接口返回{ data: ... },那个接口返回{ result: ... },另一个接口直接返回数组。前端会疯。

src/utils/response.js:

function success(res, data = null, message = '操作成功') { return res.status(200).json({ code: 0, message, data }); } function fail(res, statusCode = 400, message = '操作失败', errors = null) { return res.status(statusCode).json({ code: statusCode, message, errors, data: null }); } module.exports = { success, fail };

约定:code为0表示成功,非0表示失败。HTTP状态码和业务状态码分开——HTTP状态码表示请求本身的处理结果,业务code表示业务逻辑的执行结果。比如创建用户时邮箱已存在,HTTP返回200(请求处理成功),但业务code返回1001(邮箱重复)。

这个约定不是唯一的,你也可以用HTTP状态码直接表示业务错误。但分开的好处是前端可以根据code做精细化的错误提示,而不用去解析HTTP状态码。

实操心得:data字段即使没有数据也保留,设为null。这样前端可以统一用res.data.data来取值,不用判断data字段是否存在。少写很多if。

3.5 路由实现:CRUD的完整写法

src/routes/users.js:

const express = require('express'); const router = express.Router(); const validate = require('../middleware/validate'); const { success, fail } = require('../utils/response'); // 模拟数据 let users = [ { id: 1, name: '张三', email: 'zhangsan@example.com' }, { id: 2, name: '李四', email: 'lisi@example.com' } ]; let nextId = 3; // 获取用户列表 router.get('/', (req, res) => { success(res, users); }); // 获取单个用户 router.get('/:id', (req, res) => { const id = parseInt(req.params.id, 10); const user = users.find(u => u.id === id); if (!user) { return fail(res, 404, '用户不存在'); } success(res, user); }); // 创建用户 router.post('/', validate({ name: { required: true, type: 'string', minLength: 2 }, email: { required: true, type: 'string' } }), (req, res) => { const { name, email } = req.body; // 检查邮箱是否重复 if (users.some(u => u.email === email)) { return fail(res, 409, '邮箱已被使用'); } const newUser = { id: nextId++, name, email }; users.push(newUser); res.status(201).json({ code: 0, message: '创建成功', data: newUser }); }); // 更新用户 router.put('/:id', validate({ name: { required: true, type: 'string', minLength: 2 }, email: { required: true, type: 'string' } }), (req, res) => { const id = parseInt(req.params.id, 10); const index = users.findIndex(u => u.id === id); if (index === -1) { return fail(res, 404, '用户不存在'); } const { name, email } = req.body; if (users.some(u => u.email === email && u.id !== id)) { return fail(res, 409, '邮箱已被使用'); } users[index] = { id, name, email }; success(res, users[index], '更新成功'); }); // 删除用户 router.delete('/:id', (req, res) => { const id = parseInt(req.params.id, 10); const index = users.findIndex(u => u.id === id); if (index === -1) { return fail(res, 404, '用户不存在'); } users.splice(index, 1); success(res, null, '删除成功'); }); module.exports = router;

几个细节值得展开。

parseInt(req.params.id, 10)——URL参数永远是字符串,需要转成数字再比较。第二个参数10表示十进制,不写的话在某些情况下会有意外行为(比如parseInt('08')在老版本JavaScript里会被当成八进制)。养成写10的习惯。

users.some(u => u.email === email)检查邮箱重复。注意更新用户时,要排除自己:u.id !== id。否则用户不修改邮箱直接提交,会误报“邮箱已被使用”。

创建用户返回201状态码,表示资源创建成功。这是RESTful API的约定,不是强制的,但遵循约定让接口更规范。

注意:这里用内存数组模拟数据,服务重启数据就没了。这是故意的——练手项目先聚焦API逻辑,数据持久化是下一步的事。不要一开始就引入数据库,那会引入太多额外复杂度。

3.6 错误处理中间件:兜底的最后一道防线

src/middleware/errorHandler.js:

function errorHandler(err, req, res, next) { console.error('未捕获的错误:', err); // 如果响应已经发送,交给Express默认处理 if (res.headersSent) { return next(err); } const statusCode = err.statusCode || 500; const message = err.message || '服务器内部错误'; res.status(statusCode).json({ code: statusCode, message, data: null }); } module.exports = errorHandler;

这个中间件捕获所有路由处理函数中抛出的错误。Express 5之前,异步函数里抛出的错误不会被自动捕获,需要手动try/catch或者用next(err)传递。Express 5开始支持自动捕获异步错误,但如果你用的是Express 4,这一点要特别注意。

res.headersSent检查——如果响应已经开始发送了,就不能再改状态码和响应体了。这时候只能交给Express的默认错误处理,它会直接关闭连接。

err.statusCode——自定义错误可以带上状态码。比如在路由里const err = new Error('用户不存在'); err.statusCode = 404; throw err;,错误处理中间件就能返回404而不是500。

实操心得:错误处理中间件里一定要打日志。我见过有人写了错误处理但没打日志,结果线上出问题完全不知道发生了什么。console.error是最低要求,生产环境应该用专业的日志库。

4. 常见问题排查与避坑指南

4.1 启动报错排查速查表

报错信息可能原因解决方法
Cannot find module 'express'依赖没装执行npm install
EADDRINUSE: address already in use端口被占用换端口或杀掉占用进程
req.body is undefined没加express.json()在路由前加解析中间件
Cannot read property 'xxx' of undefined对象层级访问错误检查数据来源,加可选链?.
ERR_REQUIRE_ESM模块系统不匹配检查package.json的type字段
SyntaxError: Unexpected tokenJSON格式错误检查请求体JSON是否合法

端口被占用这个问题特别常见。排查方法:

# macOS/Linux lsof -i :3000 # Windows netstat -ano | findstr :3000

找到PID后杀掉进程,或者直接在代码里换个端口。

4.2 中间件顺序引发的诡异问题

中间件顺序是Express里最容易出错的地方。我列几个典型场景。

场景一:日志中间件放在路由后面。结果路由处理完了才打日志,而且如果路由里直接返回了响应,日志中间件根本不会执行。因为Express的中间件是线性的,前面的中间件不调用next(),后面的就不会执行。

场景二:错误处理中间件放在路由前面。路由里抛出的错误传不到错误处理中间件,因为错误处理中间件在路由之前就已经执行过了。Express的错误处理是向前查找的——从出错位置往后找第一个四参数中间件。

场景三:404处理放在路由前面。所有请求都被404拦截了,路由根本匹配不到。404处理必须放在所有路由之后。

正确的顺序是:解析中间件 → 日志中间件 → 路由 → 404处理 → 错误处理。

4.3 异步错误捕获的坑

这是Express 4的一个经典问题。看这段代码:

router.get('/test', async (req, res) => { const data = await someAsyncFunction(); // 如果这里抛错 res.json(data); });

如果someAsyncFunction抛错,Express 4不会自动捕获,错误会变成未处理的Promise rejection,进程可能直接崩溃。错误处理中间件也收不到这个错误。

解决方案有三种:

第一种,手动try/catch:

router.get('/test', async (req, res, next) => { try { const data = await someAsyncFunction(); res.json(data); } catch (err) { next(err); } });

第二种,写一个包装函数:

const asyncHandler = (fn) => (req, res, next) => { Promise.resolve(fn(req, res, next)).catch(next); }; router.get('/test', asyncHandler(async (req, res) => { const data = await someAsyncFunction(); res.json(data); }));

第三种,升级到Express 5。Express 5会自动捕获异步错误,不需要额外处理。但Express 5还在beta阶段,生产环境慎用。

我推荐第二种方案。asyncHandler写一次,所有异步路由都能用,代码也干净。

4.4 环境变量管理的注意事项

.env文件不要提交到git。这是铁律。API密钥、数据库密码这些敏感信息一旦提交,就等于公开了。

.gitignore里加上:

node_modules/ .env *.log

同时提供一个.env.example作为模板:

PORT=3000 NODE_ENV=development

别人克隆你的项目后,复制.env.example为.env,填入自己的值。

加载环境变量的代码放在server.js最顶部:

require('dotenv').config(); const app = require('./app'); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`服务已启动,监听端口 ${PORT}`); });

require('dotenv').config()必须在其他模块之前执行,否则其他模块里读process.env会拿到undefined。

实操心得:process.env.PORT || 3000这个写法很常见,但有个小问题——如果PORT被设成了空字符串,||会返回3000。更严谨的写法是process.env.PORT ?? 3000,空字符串也会被保留。不过实际项目中很少有人把PORT设成空字符串,所以两种写法都行。

4.5 AI生成代码的审查要点

用AI辅助写代码,审查环节不能省。我总结几个重点检查项。

检查错误处理。AI生成的代码经常忽略错误处理,或者只做最基础的try/catch。你要确认:异步操作的错误有没有被捕获、错误有没有传递给错误处理中间件、错误信息会不会泄露敏感数据。

检查边界条件。AI倾向于处理“正常情况”,对边界条件的覆盖不够。比如数组为空、参数为null、数字为0、字符串为空串,这些情况要自己过一遍。

检查安全性。AI生成的代码可能包含SQL注入风险(如果涉及数据库)、XSS风险(如果涉及HTML输出)、敏感信息硬编码等问题。练手项目虽然不涉及真实数据,但养成审查习惯很重要。

检查依赖版本。AI可能生成基于旧版本API的代码。比如它可能用body-parser而不是express.json(),用request而不是fetch。这些要手动更新到当前推荐的做法。

5. 项目扩展方向与进阶建议

5.1 从内存存储到真实数据库

当前项目用内存数组存数据,重启就丢。下一步自然是接入数据库。选择上,练手项目我推荐SQLite——零配置、单文件、不需要额外安装服务。用better-sqlite3或者knex都可以。

接入数据库后,路由处理函数里的users.find、users.push要替换成数据库查询。这时候你会发现,把数据访问逻辑抽到单独的models或repositories层是必要的。路由处理函数不应该关心数据存在哪里、怎么查。

5.2 加上接口文档和测试

接口写完了,怎么让别人知道怎么调?手写文档容易过时,推荐用Swagger/OpenAPI。在代码里写注释,自动生成文档页面。swagger-jsdoc和swagger-ui-express这两个包配合使用,效果不错。

测试方面,supertest+jest或者vitest是常见组合。supertest可以直接测试Express应用,不需要真的启动服务。写几个核心接口的测试用例,以后改代码时跑一遍,能快速发现回归问题。

5.3 部署上线的注意事项

练手项目部署到服务器上,有几个点要注意。

进程管理用pm2。直接node src/server.js启动,终端一关服务就停了。pm2能让服务在后台运行,还支持自动重启、日志管理、集群模式。

npm install -g pm2 pm2 start src/server.js --name mini-api pm2 save pm2 startup

反向代理用Nginx。Node.js直接监听80端口需要root权限,而且Nginx能处理静态文件、SSL终止、负载均衡这些事。Nginx配置里把请求转发到Node.js的端口就行。

环境变量在服务器上通过pm2的--env参数或者系统的环境变量设置,不要依赖.env文件。.env文件适合开发环境,生产环境应该用更安全的配置管理方式。

5.4 继续用AI辅助的进阶玩法

项目搭起来之后,AI还能在哪些环节帮上忙?

代码审查。把路由文件贴给AI,让它从安全性、性能、可读性三个角度提改进建议。它可能会指出你没注意到的N+1查询问题、缺少索引、错误信息泄露等。

生成测试用例。告诉AI你的接口定义和预期行为,让它生成测试用例。你审查和补充边界情况。这比从零写测试快很多。

排查性能问题。如果某个接口响应慢,把相关代码和日志贴给AI,让它分析可能的瓶颈。它可能会提醒你检查数据库查询、检查是否有同步阻塞操作、检查是否有内存泄漏。

学习新概念。遇到不懂的中间件、设计模式、架构方案,直接问AI。让它用生活化的例子解释,比看文档快。

实操心得:AI给出的建议不要照单全收。它有时候会过度设计,给一个练手项目推荐微服务架构或者复杂的缓存策略。记住你的项目规模和目标,只采纳匹配当前阶段的建议。

5.5 我踩过的几个坑

最后分享几个我在做这个项目时实际踩过的坑,希望能帮你省点时间。

第一个坑:express.json()的limit默认是100kb。如果你要接收大一点的JSON请求体,需要手动设置express.json({ limit: '1mb' })。我一开始不知道,传了一个稍大的数组就报PayloadTooLargeError,排查了好一会儿。

第二个坑:req.params和req.query的类型。两者都是字符串。req.params.id是"1"不是1。比较的时候要么转数字,要么用==(不推荐)。我习惯用parseInt转一下,明确意图。

第三个坑:路由路径的尾部斜杠。/api/users和/api/users/在Express默认配置下是不同的路径。strict routing关闭时(默认),两者等价。但如果你开启了strict routing,就要注意了。我建议统一不加尾部斜杠,避免混淆。

第四个坑:res.json()和res.send()的区别。res.json()会自动设置Content-Type: application/json,res.send()根据内容类型自动判断。返回JSON数据时用res.json()更明确。我见过有人用res.send返回对象,结果Content-Type不对,前端解析失败。

第五个坑:错误处理中间件的next参数不能省。即使你不用它,也必须写四个参数(err, req, res, next)。Express就是靠参数个数来识别错误处理中间件的。少写一个参数,它就被当成普通中间件了,错误不会传给它。

这些坑都不大,但每一个都可能让你多花半小时排查。提前知道,就能绕过去。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 3:04:33

Superpowers技能库:为AI编程助手注入Agent Skills工作流

最近在 GitHub 和 AI 编程工具社区里面逛,你应该会频繁撞见一个名字很中二的项目:Superpowers。它不是超级英雄模拟器,而是一套给 AI 编程助手用的技能(skills)增强包。装上之后,像 Claude Code、Codex CLI 这类工具就不再是只会在对话框里蹦代码的应答机,而是能在合适的时机自…

作者头像 李华
网站建设 2026/10/19 13:57:19

软件工程毕设提效攻略:8款AI工具从论文到代码全流程实战

毕设季一到&#xff0c;软件工程专业的学生基本就被两座大山夹着走&#xff1a;一边是代码要能跑、功能要完整&#xff0c;另一边是论文要能过查重、逻辑要自洽。很多人第一反应是“能不能让AI帮我写”&#xff0c;但真正经历过一遍的人会告诉你&#xff0c;AI对软件工程毕业设…

作者头像 李华
网站建设 2026/10/23 15:30:38

JavaWeb校园外卖系统:Servlet+JSP+MySQL全链路实战

简介&#xff1a;这是一套面向Java初学者与高校课程设计学生的校园外卖管理系统实战项目&#xff0c;基于JavaWeb技术栈开发&#xff0c;完整覆盖用户端、商家端与后台管理三大模块&#xff0c;可直接用于Java课程设计、毕业设计或期末大作业。资源包共329个文件&#xff0c;包…

作者头像 李华
网站建设 2026/10/19 19:56:16

DeepSeek Harness:本地AI工作流操作系统解析

1. DeepSeek Harness桌面端不是“另一个ChatGPT客户端”&#xff0c;而是本地AI工作流的中枢操作系统你点开官网下载那个带DeepSeek Logo的安装包时&#xff0c;别下意识把它当成又一个“套壳聊天界面”。我第一次双击DeepSeek-Harness-Setup-1.0.0-macOS.dmg时也这么想——直到…

作者头像 李华
网站建设 2026/10/23 19:12:33

Cesium前端解析GFS风场实现Windy级粒子动画

简介&#xff1a;本资源是一套基于JavaScript与Cesium平台实现气象可视化效果的完整毕业设计项目&#xff0c;面向地理信息、WebGIS、前端开发方向的本科生及课程设计学习者&#xff0c;解决GFS全球气象数据在三维地球上的动态粒子渲染难题。压缩包共535个文件&#xff0c;含14…

作者头像 李华
网站建设 2026/10/24 0:02:09

阿里EDAS Demo拆包实战:从本地跑通到企业级分布式应用托管

简介&#xff1a;这份阿里EDAS示例工程面向希望学习企业级分布式应用开发的Java开发者与微服务初学者&#xff0c;围绕阿里EDAS平台展示了微服务架构下的应用拆分、部署与治理思路。压缩包共15个文件&#xff0c;以8个xml配置文件和6个java源码为主&#xff0c;另含1个index入口…

作者头像 李华