这次我们来看 Express.js 里特别有落地感的一个基础问题如何在服务端用 Node 内置的fs.readFile读取用户信息文件并通过 Express 路由返回给前端。很多初学者学完 Express 的app.get、app.listen之后会卡在“接口数据到底从哪里来”这一环。正常项目里会接 MySQL、MongoDB但在学习阶段、内部工具甚至一些小原型里直接用fs.readFile读取本地 JSON 文件是更快、更容易理解的方式。整个过程不需要数据库服务不需要 ORM只要有一个 Node.js 环境和一个文件就能把接口跑通。这篇文章会围绕一个核心演示用users.json存放用户信息然后通过 Express.js 创建两个接口分别返回用户列表和单个用户详情。代码会用同步和异步两种方式对比重点讲fs.readFile在 Express 路由中的正确姿势包括回调地狱怎么规避、JSON 解析错误怎么处理、文件不存在怎么返回 404、为什么不能无脑使用readFileSync。阅读本文不需要太多前置知识但要先了解 JavaScript 的基础语法和 Express 路由的基本写法。如果你正准备做 Node.js 后端项目但又暂时不想引入数据库先掌握用readFile读取本地的 JSON 文件可以说是性价比最高的一步。1. 核心能力速览能力项说明项目内容Express.js 教程使用 Node 内置 fs.readFile 读取用户信息依赖组件Express.js Node.js 内置 fs 模块主要功能从本地 JSON 文件读取用户数据通过路由返回 JSON数据来源users.json或拆分为多个用户文件的目录数据读写方式fs.readFile回调方式 / Promise 方式 /readFileSync同步方式启动方式node index.js或配合nodemon热重启接口能力自定义路由返回 JSON 内容可扩展 RESTful API批量任务支持可以使用Promise.all批量读取多个文件但需要控制并发适合场景学习演示、快速原型、内部工具、静态配置读取不适合场景高频并发读写、大量用户数据、需要事务或复杂查询readFile并不是 Express.js 的方法而是 Node.jsfs文件系统模块提供的方法。单独用 Express 只能处理 HTTP 请求真正读取磁盘文件的是fs.readFile。把两者组合起来就能在接口请求到达时读取文件内容再把内容结构化返回给客户端。2. 适用场景与使用边界用readFile读取用户信息适合解决这几种问题第一学习接口编程。本地接口的第一版通常不需要数据库先准备一个 JSON 文件模拟数据能降低学习门槛也能让前后端并行开发。前端拿着你提供的 JSON 数据联调后端后续再替换成真实数据库。第二做小规模配置读取。例如服务端启动时需要读取一个config.json或者在某个管理后台读取固定用户资料。用户量不大文件更新频率低完全可以不引入数据库。第三快速搭建内部工具。比如做一个只有几个人访问的用户查询页面用 Express JSON 文件就能完成。但它的边界也很明显。每次调用readFile都会触发一次磁盘 I/O如果每个用户请求都去读取同一个很大的 JSON 文件吞吐量会非常差。更关键的是多个用户同时写入时直接操作文件没有事务、没有锁容易产生数据覆盖和脏读。用户量一旦上来或者数据需要复杂查询和关联就该切换到 SQLite、MySQL、MongoDB 这类方案。关于安全边界必须额外说明如果读取的用户文件包含个人信息例如手机号、邮箱、身份证号要在本地开发中做好隐私保护不要把这些数据提交到公开仓库如果项目上线文件放在公网服务器上时必须限制接口访问范围避免将全部用户数据无限制暴露。文中涉及的用户 JSON 数据建议只使用测试数据不要直接使用真实用户资料。3. 环境准备与项目初始化3.1 安装 Node.jsfs是 Node.js 的内置模块所以第一步是确认本机有 Node.js 环境。打开终端输入node -v npm -v如果提示命令不存在需要先安装 Node.js建议选择官方 LTS 版本。安装完成后重新打开终端执行node -v能输出版本号就说明环境就绪。从本文的写法来看不需要特定 Node 大版本只要支持fs.readFile和 CommonJS 模块即可常规维护中的 Node.js 环境基本都能运行。更稳妥的判断是在本机执行上述两个命令再配合后面的示例代码独立验证。3.2 创建项目目录在一个合适的位置创建项目目录mkdir express-readfile-demo cd express-readfile-demo然后初始化package.jsonnpm init -y3.3 安装 Express在项目目录中安装 Express 依赖npm install express安装完成后package.json中会出现express依赖项。如果想开发时改代码自动重启可以额外安装nodemonnpm install -D nodemon如果安装依赖时下载速度慢或者超时可以先把 npm 镜像切换为国内镜像再重新执行安装具体镜像地址请以当前你使用的 npm 环境为准不要在代码仓库中提交未加密的认证信息。3.4 准备用户数据文件在项目根目录新建data文件夹并在里面创建users.json。这里使用测试数据{ users: [ { id: 1, name: Alice, email: aliceexample.com }, { id: 2, name: Bob, email: bobexample.com } ] }users.json是后面所有路由读取的数据源。你可以按照自己的需求扩展字段例如增加phone、avatar、role等但字段结构一旦变化Express 路由返回的 JSON 结构也会变化。4. readFile 的三种写法与核心差异在正式写 Express 路由前先把fs.readFile本身讲清楚。Node.js 的fs模块提供了多种读取文件方式实际开发中最常见的三种是回调写法、Promise 写法、同步写法。4.1 回调写法fs.readFile最经典的形式是传入文件路径、编码和回调函数const fs require(fs); fs.readFile(data/users.json, utf8, (err, data) { if (err) { console.error(读取失败:, err); return; } console.log(data); });回调函数接收两个参数第一个是错误对象第二个是文件内容。如果文件不存在或没有权限err不为空读取成功时data是字符串。这里必须要判断err否则后续代码可能会把null或错误信息当成正常内容处理。4.2 Promise 写法为了避免回调嵌套过深可以使用fs.promises.readFileconst fs require(fs).promises; async function readUsers() { try { const data await fs.readFile(data/users.json, utf8); console.log(data); } catch (err) { console.error(读取失败:, err); } } readUsers();这种写法的优点是可以用async/await的方式处理多个异步读取流程代码读起来更像同步逻辑错误处理也更集中。在 Express 路由中使用这种方式最直观。4.3 同步写法fs.readFileSync会阻塞当前线程直到文件读取完成const fs require(fs); const data fs.readFileSync(data/users.json, utf8); console.log(data);同步写法在 Node.js 脚本启动阶段没有问题但如果放在 Express 的请求处理函数中就会带来明显问题Node.js 是单线程事件循环模型读取文件期间无法处理其他请求。高并发场景下一个请求读取大文件就可能拖慢整个服务的响应速度。因此本文后续的 Express 路由示例会优先使用异步方式readFileSync只适合在项目启动时读取配置或者用于一些极简单的脚本场景。实际耗时差别会受文件大小、磁盘类型和并发请求数影响不能一概而论建议通过自己的接口压测观察真实数据。5. 在 Express 路由中读取用户信息进入本文核心部分创建index.js这是一个完整的 Express 项目入口文件。5.1 基础文件路径处理直接写字符串路径如data/users.json在一个固定的启动目录下能工作但一旦从其他目录启动服务相对路径就会失效。更可靠的做法是使用path.join(__dirname, data, users.json)其中__dirname指向当前文件所在目录这样无论从哪里启动都能找到用户文件。const path require(path); const USERS_FILE path.join(__dirname, data, users.json);5.2 使用 Promise 方式读取并返回用户列表完整示例代码如下const express require(express); const fs require(fs).promises; const path require(path); const app express(); const PORT 3000; const USERS_FILE path.join(__dirname, data, users.json); app.get(/api/users, async (req, res) { try { const data await fs.readFile(USERS_FILE, utf8); const parsed JSON.parse(data); res.json(parsed); } catch (err) { console.error(读取用户信息失败:, err); res.status(500).json({ error: 服务器内部错误 }); } }); app.listen(PORT, () { console.log(服务已启动: http://localhost:${PORT}); });启动服务node index.js控制台输出内容后浏览器访问http://localhost:3000/api/users。这里重点观察三点第一fs.promises.readFile返回的是 Promise所以它可以被await。第二readFile读取出来的是字符串必须通过JSON.parse转成对象才能交给res.json直接输出。如果不做JSON.parse返回给客户端的内容就是纯字符串Content-Type 可能不是application/json接口使用方会收到一串文本而不是结构化的 JSON。第三JSON.parse本身可能抛异常。比如users.json文件被误编辑成非法 JSONawait fs.readFile不会报错但JSON.parse会抛错。把fs.readFile和JSON.parse都放在同一个try/catch中能避免因为 JSON 格式错误导致 Express 进程直接崩溃。5.3 使用 readFile 回调方式编写的等价路由如果你更习惯回调风格可以这样写const express require(express); const fs require(fs); const path require(path); const app express(); const PORT 3000; const USERS_FILE path.join(__dirname, data, users.json); app.get(/api/users, (req, res) { fs.readFile(USERS_FILE, utf8, (err, data) { if (err) { console.error(读取用户信息失败:, err); res.status(500).json({ error: 服务器内部错误 }); return; } try { const parsed JSON.parse(data); res.json(parsed); } catch (parseErr) { console.error(JSON 解析失败:, parseErr); res.status(500).json({ error: 用户数据格式错误 }); } }); }); app.listen(PORT, () { console.log(服务已启动: http://localhost:${PORT}); });回调写法有一个容易忽略的点一旦fs.readFile发生错误已经执行的回调中很可能还没有调用res。这时如果不return后续代码会继续尝试执行res.json但 HTTP 响应可能已经处于结束状态最终会出现客户端一直等待或者收到无法解析的响应。因此错误处理分支里需要在调用res.status(...).json(...)后立即return。5.4 根据用户 id 读取单个用户信息有了用户列表接口再增加一个查询单个用户的接口。这一步更贴近实际 API 设计同时也是对参数校验、查找逻辑的练习。const express require(express); const fs require(fs).promises; const path require(path); const app express(); const PORT 3000; const USERS_FILE path.join(__dirname, data, users.json); app.get(/api/users/:id, async (req, res) { const userId Number(req.params.id); if (!Number.isInteger(userId) || userId 0) { res.status(400).json({ error: 无效的用户 id }); return; } try { const data await fs.readFile(USERS_FILE, utf8); const parsed JSON.parse(data); const users parsed.users || []; const user users.find((item) item.id userId); if (!user) { res.status(404).json({ error: 用户不存在 }); return; } res.json(user); } catch (err) { console.error(读取用户信息失败:, err); res.status(500).json({ error: 服务器内部错误 }); } }); app.listen(PORT, () { console.log(服务已启动: http://localhost:${PORT}); });这里的Number(req.params.id)会把req.params.id转成数字。如果不做转换直接拿字符串1去和 JSON 中的数字1比较会返回false导致明明存在用户却找不到用户的情况。Number.isInteger(userId) userId 0这个判断能过滤abc、-1、0、1.5这类异常输入。如果接口设计时希望 ID 中包含字符串则需要换一种校验策略本文假设用户 ID 是正整数。5.5 启动服务与验证路由在项目目录执行node index.js看到如下日志说明启动成功服务已启动: http://localhost:3000然后打开另一个终端执行以下请求测试curl http://localhost:3000/api/users预期返回 JSON 数组对象封装{ users: [ { id: 1, name: Alice, email: aliceexample.com }, { id: 2, name: Bob, email: bobexample.com } ] }继续测试单个用户curl http://localhost:3000/api/users/1预期返回{ id: 1, name: Alice, email: aliceexample.com }测试不存在的用户curl http://localhost:3000/api/users/999预期返回状态码 404 和对应的错误 JSON。6. 接口 API 调用与批量任务设计实际的 Express 服务启动后接口天然就是 HTTP API可供前端、爬虫、测试脚本或其他服务调用。下面从接口测试和批量读取两个角度继续展开。6.1 使用浏览器或 Postman 验证接口浏览器直接访问http://localhost:3000/api/users是最快的验证方式适合确认服务是否启动。Postman 或 Apifox 适合验证请求方法、响应头和状态码。在 Postman 中新建 GET 请求GET http://localhost:3000/api/users/2如果返回{ id: 2, name: Bob, email: bobexample.com }说明 Express 路由和readFile读取流程正常。6.2 使用 Python requests 调用接口如果自动化脚本是 Python 项目可以这样请求服务import requests response requests.get(http://127.0.0.1:3000/api/users, timeout5) print(response.status_code) print(response.json())脚本直接打印服务端返回的用户列表。需要注意的是启动 Express 服务的终端窗口要保留否则服务关闭后接口无法访问。6.3 使用 Node.js 调用接口在另一个 Node.js 脚本中请求自己的接口async function requestUsers() { const response await fetch(http://127.0.0.1:3000/api/users); const data await response.json(); console.log(data); } requestUsers();这展示了 Express 服务可以很轻松地成为其他程序的 HTTP 数据源。6.4 批量读取多个用户文件当用户信息拆分成单文件存储时例如data/ ├── users.json └── users/ ├── 1.json ├── 2.json └── 3.json每个文件内容可能是一个独立用户对象。这时候需要读取多个文件并合并结果。使用Promise.all能并行读取但要注意文件数量和文件大小避免一次性并发过多 I/O 请求const fs require(fs).promises; const path require(path); const USERS_DIR path.join(__dirname, data, users); async function readUsersByIds(ids) { const tasks ids.map((id) { const filePath path.join(USERS_DIR, ${id}.json); return fs.readFile(filePath, utf8) .then((content) { const user JSON.parse(content); return { id, user }; }) .catch((err) { if (err.code ENOENT) { return { id, user: null }; } throw err; }); }); const results await Promise.all(tasks); return results.filter((item) item.user ! null); } readUsersByIds([1, 2, 3]) .then((users) { console.log(users); }) .catch((err) { console.error(批量读取失败:, err); });这里的Promise.all适合文件数量可控的场景。如果一次要读取数千个文件建议分批执行并控制每批并发数量在 10 到 20 之间避免瞬间打开大量文件句柄导致进程报错。每批读取完成后记录成功和失败的文件列表方便重试。6.5 批量任务的失败重试建议批量读取用户信息时文件缺失、编码异常、JSON 解析失败都可能发生。建议在代码中做三层处理第一层单文件读取失败时记录失败原因但不要让整个批量任务中断第二层批次级别做整体失败处理例如 20 个文件中某个文件格式损坏这一批是否继续处理需要根据业务决定第三层重试策略。文件缺失通常不需要重试可能是数据本身不存在而如果是因为“文件暂时被占用”或“系统 I/O 错误”则等待几百毫秒后重试一次更合理。具体重试次数和时间间隔建议根据本机稳定性和磁盘压力测试后确定。7. 资源占用与性能观察方法7.1 readFile 对服务器资源的消耗逻辑readFile一次性把整个文件读入内存因此对内存的消耗与文件大小直接相关。读取 1KB 的用户配置文件和读取 1GB 的超大文件占用的内存完全不同。在读取用户信息这个场景中数据量一般不大正常开发中不会成为瓶颈但你可以用系统命令观察 Node.js 进程的内存变化node index.js然后在另一个终端查看 Node.js 进程资源Linux / macOS 使用ps aux | grep node查看进程。Windows 使用任务管理器查看进程内存。在代码中打印内置process.memoryUsage()也能看到内存占用。需要注意这些数字会因操作系统和 Node.js 版本不同而变化不需要把某一次观察结果当成固定结论。7.2 高并发下的注意事项Express 路由中频繁调用readFile高并发时有三个突出问题第一磁盘 I/O 压力。即使readFile是异步的底层仍然要读取磁盘文件。如果每个请求都重复读取同一个文件文件缓存失效时磁盘 I/O 会成为瓶颈。第二CPU 压力。JSON.parse是 CPU 操作大 JSON 文件在请求路径中反复解析会消耗大量 CPU。第三资源竞争。多个请求同时读取同一个文件通常不会出问题因为文件系统的读锁粒度较低但如果你的业务是写操作手动改写文件内容时并发写会产生冲突因此不要把私有协议或频繁变更的数据保存在 JSON 文件中。缓解手段包括服务启动时读取一次文件到变量后续直接使用内存中的数据适合“用户数据很少变化”的场景。使用集中式缓存例如 Redis 缓存用户信息减少磁盘读取。文件更新时通过重启服务或其他缓存失效机制重新读取。用户量增大后把数据迁移到 SQLite保留文件读取作为兜底或还原机制。7.3 如何观察接口响应耗时在 Express 中写一个简单的中间件就能观测每个接口的耗时app.use((req, res, next) { const start Date.now(); res.on(finish, () { const duration Date.now() - start; console.log(${req.method} ${req.originalUrl} ${duration}ms); }); next(); });不同的磁盘类型、请求参数和并发数量都会导致耗时不同关键是建立起“先观察、再优化”的习惯。8. readFile 读取用户信息常见问题与排查问题现象可能原因排查方式解决方案Node.js 提示Error: Cannot find module expressExpress 未安装或安装目录不对检查node_modules是否存在在项目根目录执行npm install express启动后报错ENOENT读取的文件路径不存在打印USERS_FILE检查路径使用path.join(__dirname, data, users.json)确认文件存在接口一直返回 500JSON.parse解析失败或读取权限错误查看终端控制台中的 stack trace用编辑器打开 JSON 文件检查格式可先执行 JSON 格式化校验请求/api/users/1返回 404但 ID 存在于 JSON参数类型没有转换打印req.params.id和类型使用Number(req.params.id)后再比较页面内容显示为纯文本而不是 JSON 对象忘记执行JSON.parse直接返回字符串查看响应头 Content-Type先JSON.parse再用res.json返回改代码后重启服务不起作用进程没有退出查看端口占用终止旧进程后重新启动开发时使用nodemon中文用户信息返回乱码读取时没有指定utf8查看文件编码readFile第二个参数传入utf8多个请求同时操作用户文件时数据可能丢失写文件时没有加锁或事务观察数据文件变化小项目用数据库替代文件写入策略文件明明存在但读取超时磁盘 I/O 繁忙或文件过大检查系统负载使用服务启动时只读取一次或数据库方案8.1 端口被占用怎么解决启动服务时如果看到Error: listen EADDRINUSE: address already in use :::3000说明 3000 端口已经被其他进程占用。可以修改代码中的PORT例如改为 3001const PORT 3001;或者找到占用端口的进程并结束后再启动。Windows 下执行netstat -ano | findstr :3000Linux / macOS 下执行lsof -i :3000排查时注意不要随意 kill 系统关键进程只处理明确属于自己项目的 Node.js 进程。9. 最佳实践与使用建议9.1 第一次测试先用最小数据不要一开始就生成一个几十 GB 的用户数据文件来压测。先用 2 到 3 个测试用户跑通流程确认readFile、JSON.parse、res.json三个环节都没有问题再考虑是否增加更多数据或引入缓存。9.2 保持固定目录结构建议把代码、数据、输出分开管理express-readfile-demo/ ├── data/ │ ├── users.json │ └── users/ ├── src/ │ └── index.js ├── logs/ └── package.json这样写死路径时思路清晰后续做批量任务、日志记录和打包部署也方便。如果把用户 JSON 文件和业务代码混在一个目录中项目会越来越难维护。9.3 路由和读文件逻辑拆分不要在 Express 路由函数里写太多的业务逻辑。可以抽一个独立的userService.js或userRepository.js把读取文件、解析 JSON、查找用户的逻辑封装起来。// userService.js const fs require(fs).promises; const path require(path); const USERS_FILE path.join(__dirname, data, users.json); async function getUsers() { const data await fs.readFile(USERS_FILE, utf8); return JSON.parse(data); } module.exports { getUsers };路由文件只负责接收请求并返回响应这样做的好处是后面把getUsers替换为数据库查询实现时Express 路由不用大改。9.4 文件内容被外部修改时要有兜底机制如果用户信息 JSON 文件会被其他进程修改例如运维脚本定期更新、后台界面手动编辑就要在服务端增加文件 mtime 检查或重启期间重新读取。最简单稳妥的办法是如果数据更新频率极低每次请求都读取文件可以保证拿到最新数据如果文件较大再配合缓存方案。9.5 日志和异常记录本地开发时打印错误到控制台没问题但生产环境建议把错误记录到文件或日志采集系统。错误对象中应该包含路由地址、错误码、堆栈和时间戳避免排查问题时无从下手。9.6 数据与隐私合规如果你的“用户信息”不是测试数据而是真实用户资料必须至少完成以下操作不在浏览器页面直接暴露敏感字段除非业务真正需要给接口加上身份鉴权常见方式包括 Token、Session 或其他认证机制不要把所有用户信息一次性返回给无权限的人本地数据文件不要提交进 Git 仓库可以使用.gitignore忽略如果数据中包含第三方素材或受版权保护的内容使用前必须确认授权范围。10. 总结与下一步这篇文章里跑通的核心链路是创建 Express 项目准备users.json在路由中用fs.promises.readFile读取本地 JSON返回用户列表和单个用户。针对readFile的回调写法、Promise 写法和同步写法做了对比并说明了为什么请求处理中不建议使用readFileSync。你最容易踩的坑有三个第一忘记判断错误对象导致请求无响应第二读取结果没有JSON.parse就当成对象使用第三拿字符串 ID 和数字 ID 比较导致永远找不到用户。如果你准备继续深入建议先做两件事第一把用户列表接口从 JSON 文件改成 SQLite 存储使用better-sqlite3或 Node.js 自带能力对比一下文件读取和数据库查询在代码结构上的差别。第二实践批量任务场景把几十个 JSON 用户文件批量导入到一个总 JSON 对象中记录每个文件的导入状态并给失败的文件生成日志。做好这一层后续处理 CSV、Excel 或外部同步任务时思路会清楚很多。Express.js 中readFile只是一个起点但从这个点出发你已经开始接触文件系统、路径处理、异步编程、错误处理、接口设计与性能权衡。这些正是独立开发前后端应用最需要的基本功。建议把示例代码保存在自己的本地项目中作为最小可运行模板需要时直接改路径和数据字段就能复用。