1. 项目缘起与整体设计思路
1.1 为什么选这个题目练手
做了几年后端开发,我越来越觉得,很多新手(包括当年我自己)学 Node.js 和 Express 的方式有问题——看了一堆文档,知道app.get()怎么写,知道中间件大概是个什么概念,但真要从零搭一个能跑、能看、能扩展的 API 服务,脑子里还是一团浆糊。所以我一直想找一个足够小、但又能把核心链路串起来的实战项目,作为团队新人练手或者自己温故知新的素材。
“用 AI 从零搭一个 API 服务”这个题目,恰好满足几个条件:第一,API 服务是后端最基础的形态,几乎所有业务系统都绕不开;第二,Node.js + Express 是目前上手成本最低、生态最成熟的组合之一,新手不用在环境配置上耗太久;第三,把 AI 引入进来,不是赶时髦,而是让 AI 承担“代码生成 + 方案建议 + 排错辅助”的角色,把原本需要查文档、搜社区、反复试错的过程压缩掉,让注意力集中在服务本身的设计和实现上。
这个项目适合谁?如果你刚学完 JavaScript 基础语法,想找一个能落地的练手项目;或者你之前用其他语言写过服务端,想快速了解 Node.js 生态下 API 服务长什么样;再或者你已经在做业务开发,但想看看 AI 辅助编码到底能在多大程度上提升效率——这个项目都值得跟着走一遍。
1.2 整体架构与选型考量
在动手之前,先把整体结构想清楚。一个最小可用的 API 服务,至少需要这几层:HTTP 服务层负责接收请求和返回响应,路由层负责把不同 URL 映射到不同处理逻辑,业务逻辑层负责具体的数据处理,数据层负责持久化(哪怕先用内存或 JSON 文件顶着)。Express 在这套结构里扮演的是 HTTP 服务层 + 路由层的角色,它足够轻,不会强迫你接受一套复杂的框架约定,同时又提供了中间件机制,让你可以按需插入日志、鉴权、错误处理等逻辑。
为什么不用 Fastify?Fastify 性能确实更好,Schema 校验也更规范,但对于一个练手项目来说,Express 的文档丰富度和社区示例数量是压倒性的优势。你遇到任何问题,搜索“Express + 某个需求”,几乎都能找到现成的答案。Fastify 的生态虽然也在成长,但新手在排错时容易卡在一些冷门问题上。所以这个项目选 Express,不是因为它最好,而是因为它最适合“从零搭一个能跑的服务”这个目标。
Node.js 版本方面,我建议直接用当前 LTS 版本。写这篇文章时,Node.js 20.x 是主流 LTS,安装方式后面会详细说。JavaScript 方面,这个项目用原生 JS 就够了,不需要 TypeScript——不是 TypeScript 不好,而是练手阶段先把运行时逻辑跑通,比纠结类型定义更重要。等你把服务搭起来、跑通了,再考虑用 TypeScript 重构,那时候你对整体结构的理解会深得多。
AI 在这个项目里的角色,我把它定位为“结对编程的伙伴”:你告诉它你要什么,它给你代码草稿;你运行报错了,把错误贴给它,它帮你分析原因;你想加一个功能,不知道从哪下手,它给你几种方案让你选。但最终的决定权和调试责任,还是在你手里。这一点很重要——AI 生成的代码不是拿来就能用的,你需要理解每一行在干什么,否则出了问题你根本不知道怎么修。
1.3 项目最终形态预览
在正式开始之前,先让你看看这个项目做完之后大概长什么样,这样你心里有个目标。最终我们会得到一个运行在本地 3000 端口的 API 服务,提供以下几组接口:健康检查接口GET /health,用来确认服务是否正常;用户资源的增删改查接口GET/POST/PUT/DELETE /api/users,用来演示标准的 RESTful 风格;以及一个简单的搜索接口GET /api/users?keyword=xxx,用来演示查询参数的解析和过滤逻辑。数据先存在内存里,服务重启后数据会丢失,但这不影响我们理解 API 服务的核心流程。后续如果你想扩展,可以把内存存储换成 SQLite 或 PostgreSQL,那是下一步的事。
整个项目的代码量大概在 200 行左右,分文件组织,结构清晰。你跟着走一遍,大概需要 1 到 2 小时,取决于你对 JavaScript 和命令行的熟悉程度。如果你完全没接触过 Node.js,可能需要 3 小时左右,因为环境安装和调试会花一些时间。但相信我,这个时间投入是值得的——走完一遍之后,你对 API 服务的理解会从“知道概念”变成“亲手做过”。
2. 环境准备与 AI 辅助编码的定位
2.1 Node.js 安装:别在这步卡太久
Node.js 的安装本身不复杂,但新手容易在版本选择和环境变量上出问题。我的建议是:直接去 Node.js 官网下载 LTS 版本的安装包,Windows 选.msi,macOS 选.pkg,一路下一步就行。安装完成后,打开终端(Windows 用 PowerShell 或 CMD,macOS 用 Terminal),输入node -v和npm -v,如果都能输出版本号,说明安装成功。
如果你在 Ubuntu 上,用包管理器安装可能会遇到版本过旧的问题。Ubuntu 自带的 apt 源里的 Node.js 版本往往落后好几个大版本,所以更推荐用 NodeSource 的源来安装。具体命令是:先curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -,然后sudo apt-get install -y nodejs。这两条命令执行完之后,再用node -v确认版本。注意,这里涉及的是正常的软件源配置,属于标准的开发环境搭建流程。
有一个坑我踩过:如果你之前用其他方式装过 Node.js,可能会有多个版本共存,导致node -v显示的版本和你预期的不一样。这时候可以用which node(macOS/Linux)或where node(Windows)看看当前用的是哪个路径下的 Node.js。如果路径不对,要么调整环境变量,要么把旧的卸载掉。这个问题不解决,后面装依赖时会出现各种莫名其妙的错误。
2.2 初始化项目与依赖安装
环境好了之后,找一个空目录,执行npm init -y,这会生成一个默认的package.json。然后安装 Express:npm install express。如果你想让开发过程更顺畅,可以再装一个nodemon作为开发依赖:npm install --save-dev nodemon。nodemon 的作用是监听文件变化,自动重启服务,这样你改完代码不用手动Ctrl+C再重新node index.js,效率会高很多。
在package.json里,把scripts部分改成这样:
"scripts": { "start": "node index.js", "dev": "nodemon index.js" }这样开发时用npm run dev,生产环境用npm start。虽然这个练手项目谈不上生产环境,但养成区分开发和生产脚本的习惯是好的。
2.3 AI 在编码过程中的正确打开方式
很多人用 AI 辅助编码,方式不对——直接把需求丢给 AI,让它生成一大段代码,然后复制粘贴,跑不起来就再问一遍。这种方式效率其实很低,因为你没有理解代码,出了问题只能反复问 AI,而 AI 每次给的答案可能都不一样,你会越来越糊涂。
我的做法是:把 AI 当成一个“随时可以问的资深同事”。具体来说,在写每一部分代码之前,先自己想清楚这部分要做什么,然后问 AI“用 Express 实现 XXX 功能,代码大概怎么写”,拿到代码后逐行看一遍,不理解的地方追问。运行报错时,把完整的错误信息贴给 AI,同时说明你正在做什么,让它帮你定位问题。这样一轮下来,你不仅得到了能跑的代码,还理解了背后的逻辑。
举个例子,当你需要实现“根据 ID 获取单个用户”的接口时,你可以先自己想:需要从请求参数里拿到 ID,然后在用户数组里查找,找到就返回,找不到就返回 404。然后问 AI“Express 里怎么获取 URL 参数”,它会告诉你用req.params.id。你再问“怎么返回 404 状态码”,它会告诉你用res.status(404).json(...)。这样一步步拼起来,比直接要一整段代码收获大得多。
3. 核心功能实现与关键细节
3.1 服务入口与基础中间件配置
先创建index.js,这是服务的入口文件。基础结构大概是这样的:引入 Express,创建 app 实例,配置中间件,定义路由,启动监听。中间件方面,至少需要express.json()来解析 JSON 请求体,否则 POST 和 PUT 请求里的req.body会是 undefined。这个坑很常见——新手写了 POST 接口,测试时发现req.body是空的,查半天以为是前端没传对,其实是忘了加express.json()。
const express = require('express'); const app = express(); const PORT = process.env.PORT || 3000; app.use(express.json()); // 路由定义放在这里 app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); });process.env.PORT || 3000这个写法是为了兼容部署环境——很多云平台会通过环境变量指定端口,本地开发时没有这个变量,就默认用 3000。这是一个很小的细节,但养成这个习惯没坏处。
3.2 数据层:先用内存数组顶着
练手项目不需要一上来就上数据库。用内存数组存数据,简单直接,能把注意力集中在 API 逻辑上。定义一个users数组,每个元素是一个对象,包含id、name、email三个字段。id 用自增的方式生成,可以定义一个nextId变量,每次新增用户时nextId++。
let users = [ { id: 1, name: '张三', email: 'zhangsan@example.com' }, { id: 2, name: '李四', email: 'lisi@example.com' } ]; let nextId = 3;这里预置两条数据,方便测试查询接口。实际开发中你可能需要更复杂的数据结构,但练手阶段这样足够了。注意,内存存储的缺点是服务重启后数据丢失,所以不要用它存重要数据。如果你想让数据持久化,可以把数组读写到 JSON 文件里,用fs模块操作,但那是另一个话题了。
3.3 路由设计:RESTful 风格的增删改查
RESTful 是一种 API 设计风格,核心思想是用 HTTP 方法表示操作类型:GET 查、POST 增、PUT 改、DELETE 删。URL 表示资源,比如/api/users表示用户集合,/api/users/1表示 ID 为 1 的用户。这种风格的好处是直观、统一,前端开发者看到 URL 和方法就能猜到接口的作用。
先写健康检查接口:
app.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); });这个接口看起来没什么用,但在实际项目中很重要——部署之后,你需要一个方式来确认服务是否正常启动。很多云平台也要求你提供一个健康检查端点,用来判断服务是否存活。
然后是获取用户列表:
app.get('/api/users', (req, res) => { const { keyword } = req.query; let result = users; if (keyword) { result = users.filter(u => u.name.includes(keyword) || u.email.includes(keyword) ); } res.json({ data: result, total: result.length }); });这里顺便实现了搜索功能——如果 URL 里带了keyword查询参数,就按名称或邮箱过滤。req.query是 Express 自动解析的查询参数对象,不需要额外配置。返回结构用{ data, total }包裹,而不是直接返回数组,这样后续加分页信息时不用改返回结构,前端也不用改解析逻辑。
获取单个用户:
app.get('/api/users/:id', (req, res) => { const id = parseInt(req.params.id, 10); const user = users.find(u => u.id === id); if (!user) { return res.status(404).json({ error: 'User not found' }); } res.json({ data: user }); });注意parseInt(req.params.id, 10)这一步——URL 参数默认是字符串,如果不转成数字,u.id === id永远为 false,因为1 === '1'是 false。这个坑我见过很多人踩,表现就是“明明数据存在,但就是查不到”。加个parseInt就解决了。
新增用户:
app.post('/api/users', (req, res) => { const { name, email } = req.body; if (!name || !email) { return res.status(400).json({ error: 'Name and email are required' }); } const newUser = { id: nextId++, name, email }; users.push(newUser); res.status(201).json({ data: newUser }); });这里做了基本的参数校验——name 和 email 必填,缺了就返回 400。返回状态码用 201 表示“已创建”,这是 RESTful 的惯例。虽然用 200 也不会报错,但遵循惯例能让你的 API 更规范。
更新用户:
app.put('/api/users/:id', (req, res) => { const id = parseInt(req.params.id, 10); const user = users.find(u => u.id === id); if (!user) { return res.status(404).json({ error: 'User not found' }); } const { name, email } = req.body; if (name) user.name = name; if (email) user.email = email; res.json({ data: user }); });这里用的是“部分更新”逻辑——只更新传了的字段。严格来说 PUT 应该全量替换,PATCH 才是部分更新,但实际项目中很多人用 PUT 做部分更新,前端也习惯了。如果你想更规范,可以把 PUT 改成 PATCH,或者两个都提供。
删除用户:
app.delete('/api/users/:id', (req, res) => { const id = parseInt(req.params.id, 10); const index = users.findIndex(u => u.id === id); if (index === -1) { return res.status(404).json({ error: 'User not found' }); } users.splice(index, 1); res.status(204).send(); });删除成功返回 204,表示“成功但没有内容返回”。这也是 RESTful 的惯例。如果你返回 200 加一个{ message: 'deleted' }也不会错,但 204 更简洁。
3.4 错误处理与 404 兜底
上面的路由都是正常流程,但实际运行中会有各种意外——请求了不存在的路径、服务器内部出错等。Express 提供了错误处理中间件机制,可以统一捕获和处理错误。在所有的路由定义之后,加一个 404 处理和一个错误处理:
app.use((req, res) => { res.status(404).json({ error: 'Not found' }); }); app.use((err, req, res, next) => { console.error(err.stack); res.status(500).json({ error: 'Internal server error' }); });404 处理放在所有路由之后,意思是“如果前面的路由都没匹配上,就走到这里”。错误处理中间件有四个参数(err, req, res, next),Express 通过参数个数来识别它是错误处理中间件。这个机制很巧妙,但新手容易写错——少写一个参数,Express 就把它当普通中间件了,错误不会被捕获。
4. 常见问题与排查技巧实录
4.1 启动报错:端口被占用怎么办
这是最常见的问题之一。你运行npm run dev,终端报Error: listen EADDRINUSE: address already in use :::3000,意思是 3000 端口已经被别的程序占用了。可能是你之前启动的服务没关干净,也可能是其他软件占用了这个端口。
解决办法有两个:一是找到占用端口的进程并杀掉。在 macOS/Linux 上,用lsof -i :3000找到进程 ID,然后kill -9 <PID>。在 Windows 上,用netstat -ano | findstr :3000找到 PID,然后taskkill /PID <PID> /F。二是换个端口,把PORT改成 3001 或别的。我一般先用第一种方法,实在找不到占用进程再换端口。
4.2 请求体为空:req.body是 undefined
前面提过,忘了加express.json()是最常见的原因。但还有一种情况:你加了express.json(),但前端发送请求时Content-Type没设成application/json。Express 的express.json()只解析Content-Type: application/json的请求体,如果前端用的是application/x-www-form-urlencoded或没设 Content-Type,req.body就会是空的。
排查方法:在路由处理函数里加一行console.log(req.headers['content-type']),看看实际收到的 Content-Type 是什么。如果是前端问题,让前端改;如果是你自己用 Postman 或 curl 测试,检查一下请求头设置。
4.3 ID 比较失败:字符串和数字的坑
前面提到过,req.params.id是字符串,直接和数字比较会失败。除了用parseInt,还有一种情况是数据库返回的 ID 是字符串(比如 MongoDB 的 ObjectId),而你在代码里用数字比较。这种问题的表现是“数据明明存在,但查询返回空”。排查时,在比较之前把两个值都打印出来,看看类型是否一致。JavaScript 的==会做类型转换,===不会,所以用===比较时类型不一致就会返回 false。我的习惯是:所有从外部来的 ID,先转成数字(如果是数字型 ID),再参与比较。
4.4 nodemon 不生效:文件监听失效
有时候你改了代码,nodemon 没有自动重启。可能的原因有几个:一是 nodemon 没装成功,检查package.json的devDependencies里有没有它;二是你改的文件不在 nodemon 的监听范围内,默认它监听当前目录下的.js、.json等文件,如果你把代码写在了别的目录,需要配置nodemon.json指定监听路径;三是文件系统监听数量限制,在 Linux 上可能会遇到ENOSPC错误,需要调整fs.inotify.max_user_watches参数。不过对于这个练手项目,一般不会遇到第三种情况。
4.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 启动报 EADDRINUSE | 端口被占用 | lsof -i :3000或netstat -ano | 杀掉进程或换端口 |
req.body为 undefined | 缺express.json()或 Content-Type 不对 | 打印req.headers['content-type'] | 加中间件或修正请求头 |
| 按 ID 查询返回空 | 字符串和数字比较失败 | 打印typeof req.params.id | 用parseInt转换 |
| nodemon 不重启 | 监听配置问题 | 检查 nodemon 是否运行 | 配置监听路径或重装 |
| 404 处理不生效 | 放在了路由之前 | 检查代码顺序 | 移到所有路由之后 |
| 错误处理不捕获 | 中间件参数个数不对 | 检查是否四个参数 | 补全(err, req, res, next) |
4.6 用 AI 排错的正确姿势
当你遇到报错时,不要只把错误信息贴给 AI,还要附上相关代码和你的操作步骤。比如“我运行npm run dev,终端报了这个错:Error: listen EADDRINUSE: address already in use :::3000,我之前启动过一个服务但已经 Ctrl+C 了,现在应该怎么处理?”这样 AI 能更准确地判断问题。如果你只贴一句错误信息,AI 可能会给出多种可能的原因,你还得自己一个个试,效率反而低。
另外,AI 给出的解决方案,你要理解之后再执行。比如它让你kill -9 <PID>,你要知道这是在强制杀掉进程,确认那个进程确实是你想杀的那个,再执行。不要盲目复制粘贴命令,尤其是涉及系统操作的命令。
5. 项目扩展与个人经验分享
5.1 从内存到数据库:下一步怎么走
这个项目的数据存在内存里,服务重启就没了。如果你想让它更实用,下一步可以把存储换成 SQLite。SQLite 是一个文件型数据库,不需要单独安装服务,Node.js 里有better-sqlite3这个库,用起来很简单。再往后可以换成 PostgreSQL 或 MySQL,那就更接近真实项目了。但我的建议是:先把内存版本跑通、理解透,再考虑换数据库。否则你会在数据库配置、连接池、SQL 语句上花很多时间,反而忽略了 API 服务本身的设计。
5.2 加一层日志中间件
实际项目中,日志是必不可少的。你可以在 Express 里加一个简单的日志中间件,记录每个请求的方法、路径、状态码和耗时:
app.use((req, res, next) => { const start = Date.now(); res.on('finish', () => { const duration = Date.now() - start; console.log(`${req.method} ${req.path} ${res.statusCode} ${duration}ms`); }); next(); });这个中间件放在所有路由之前,这样每个请求都会经过它。res.on('finish')是在响应发送完成后触发,这时候能拿到最终的状态码。这个日志格式虽然简单,但足够你排查大部分问题——哪个接口慢了、哪个接口报错了,一目了然。
5.3 参数校验:别等到出问题才补
上面的代码里,我只做了最基本的必填校验。实际项目中,参数校验要严格得多——email 格式对不对、name 长度有没有超、有没有非法字符等。你可以手写校验逻辑,也可以用joi或express-validator这类库。我的建议是:练手阶段先手写,理解校验的逻辑;等项目复杂了,再引入校验库,减少重复代码。手写校验的时候,注意把校验逻辑抽成单独的函数,不要全堆在路由处理函数里,否则代码会很难维护。
5.4 我踩过的几个坑
第一个坑是忘了加express.json(),导致 POST 请求的req.body一直是空的,查了半小时才发现。第二个坑是req.params.id没转数字,查询一直返回 404,打印出来才发现是字符串。第三个坑是 404 处理写在了路由前面,导致所有请求都被 404 拦截了。这三个坑都很典型,你在写的时候注意一下,能省不少时间。
还有一个经验:用 Postman 或 curl 测试接口时,先把每个接口的正常流程跑通,再测试异常情况(比如传空值、传不存在的 ID)。这样出问题时,你能快速判断是正常流程的问题还是异常处理的问题。我见过有人一上来就测各种边界情况,结果正常流程都没跑通,排查起来很混乱。
5.5 这个项目还能怎么玩
如果你已经把基础版本跑通了,可以试试这几个扩展方向:加一个PATCH /api/users/:id接口,实现部分更新;加一个分页功能,GET /api/users?page=1&size=10;加一个简单的 API Key 鉴权,请求头里带x-api-key才能访问;把数据存到 JSON 文件里,实现简单的持久化。每一个扩展都不难,但能让你对 API 服务的理解更深一层。
我个人在实际操作中的体会是:搭 API 服务这件事,看十遍不如亲手写一遍。你可能会在环境配置上卡住,可能会在某个报错上耗时间,但正是这些卡住和耗时间的时刻,让你真正理解了服务是怎么跑起来的。AI 能帮你省掉一些查文档的时间,但不能替你理解代码。所以,动手写,遇到问题解决问题,这才是最快的成长路径。