成为全栈·Node 后端篇·后端工程从零搭建:TypeScript、目录与热更新

成为全栈·Node 后端篇·后端工程从零搭建:TypeScript、目录与热更新

前置知识:建议先读 为什么前端工程师要走向全栈(M0-01)、七个子项目技术栈的定法(M0-03)。这篇是 M1 的第一站,后面三十篇都建立在这个地基上。

上一篇产品篇里,我把「一个真实的多端文章系统」摊开给你看:七个子项目、一份共享 API 契约、一条共同的版本历史。从这一篇起,我们要钻进其中一个子项目------Node 后端,把它从 git init 之后的一张白纸,搭成能跑、能测、能部署的工程。

我会用三十一篇讲完它。而所有故事,都从今天这个最朴素的问题开始:一个后端工程,初始化时到底该长什么样?


一、为什么后端也得用 TypeScript

很多前端同学第一次写后端,会本能地想:「反正也是写 JS,直接用 Node 跑 .js 文件不就行了?」我早期也这么干过,后来在生产环境踩够了坑,才认定一条底线:后端工程,能用 TypeScript 就别用裸 JavaScript。

原因不是「TS 更高级」,而是后端和前端的失败成本完全不同。

前端代码跑在用户浏览器里,出个类型错误,最坏是某块 UI 白屏,刷新一下也许就好了,影响面有限。但后端代码跑在服务器上,是所有人共享的那一个进程:一个 undefined 被当成字符串拼接进 SQL、一个本该是数字的 id 变成了 NaN 写进数据库、一个本该返回数组的接口返回了 null 被上游当成对象遍历------这些都不是「刷新就好」的小事,它们会污染真实数据,而且一旦落库,修复成本是指数级的。

TypeScript 在这里的作用,是把一类错误从「运行时才爆、用户先发现」挪到「编译时就拦、你本地就发现」。

举一个最实在的例子。我们有一个角色阶梯表 ROLE_RANK,用角色名查它的排序值:

ts 复制代码
const rank = ROLE_RANK[role]; // 如果是裸 JS,这里拿到的可能是 undefined
if (rank >= ROLE_RANK.admin) { /* ... */ }

在裸 JS 里,rankundefined 时这段比较会静默失败(或在某些上下文里阴差阳错地通过),bug 就这么溜进了生产。而当我们打开 noUncheckedIndexedAccess 这个编译选项后,TS 会明确告诉你:rank 的类型是 number | undefined,你必须处理「查不到」的情况(比如补一个 ?? 0)。编译器逼你把这个分支写出来,而不是靠运气。

这就是后端用 TS 的核心价值:类型不是炫技,是给「共享进程里的共享数据」加一道护栏。


二、目录约定:七个目录各管一摊

工程初始化之后,第一件要紧事是定目录。我见过太多项目,所有 .ts 文件平铺在一个 src 里,三个月后谁也分不清哪是路由、哪是业务逻辑、哪是纯类型。我们的 Node 后端,根目录下最终定为七个目录(早期曾有一个 lib/ 放领域工具,后来在结构调优时拆进了 services/shared/,现已彻底移除):

复制代码
src/
├── config/      配置读取(环境变量 → 统一 Env 对象)
├── db/          数据库连接、迁移与 Drizzle schema
├── middleware/  中间件(认证、CORS、校验、全局错误捕获)
├── routes/      HTTP 层:解析参数、调 service、包响应信封
├── services/    业务层:承载领域规则 + 数据库操作
├── shared/      基础设施(jwt、密码哈希、存储适配、分页、响应信封、错误与错误码)
└── types/       纯类型定义(不依赖任何运行时)

这七个目录的边界,其实对应一个很重要的认知:后端分层不是「目录装饰」,是「职责边界」

  • routes/ 要薄。它只做三件事:把请求参数解析出来、调用对应的 service、把结果包进统一响应信封返回。它不应该直接写 SQL,也不应该塞满 if/else 业务判断。
  • services/ 是领域逻辑的归宿。文章的增删改、评论的审核流转、阅读量的去重计数......这些「业务规则」写在这里,而不是散落在路由里。
  • shared/ 放基础设施:JWT 怎么签、密码怎么哈希、文件怎么存、分页怎么算。这些东西跟具体业务无关,谁都能用。
  • types/ 只放类型,不写任何运行时代码,是真正的「纯类型」。

这里我先不展开分层为什么这么切(那是 {{LINK:M1-03}} 的主舞台),只想让你记住一件事:目录约定是给未来的自己看的契约。 当你三个月后回来加一个新接口,你该把代码放哪、该去哪找现成的工具,目录结构会替你回答。

真实踩坑(P-05):这套目录能扛住后来的大重构,靠的是一条纪律------全仓只用 @/ 别名(如 @/services/article)和 ./ 同目录相对引用,绝对禁止 ../../ 跨目录相对引用 。原因很现实:当我们要把 lib/ 拆进 services/shared/ 时(这件事后面会专门讲),如果文件里到处是 ../../,移动一个文件就会牵一发动全身,而且 tsc 不会立刻报错,要等到具体引用处才爆。别名 + 同目录相对引用,相当于给「大规模重命名」装了一根保险丝。我们迁移前甚至养成了一个习惯:先 grep '\.\./\.\./' 确认全仓零命中,再动手。


三、热更新与本地调试

工程能跑起来,比什么都重要。本地开发时,我们希望改一行代码、保存,服务自动重启、立刻看到效果------这就是「热更新」。

我们的 package.json 里,开发脚本长这样:

json 复制代码
{
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "start": "tsx src/index.ts",
    "build": "tsc -p tsconfig.json"
  }
}

注意我用的是 tsx watch,不是 ts-node、也不是 nodemon + ts-nodetsx 是基于 esbuild 的 TS 运行器,它直接吃 .ts 文件、不需要先编译成 .js 再跑,启动极快;watch 模式监听文件变化自动重启进程。

为什么不用 nodemonnodemon 本身只负责「文件变了就重启命令」,它不认识 TS,得配合 ts-node 才能跑 TS,链条长、冷启动慢、配置碎。而 tsx 一个工具把「识别 TS + 热重启」都包了,开发体验清爽很多。

调试这边,因为 tsx 底层走 esbuild 转译、保留了 source map,你在 VS Code 里打断点,命中的就是你的 .ts 源码行,而不是转译后的乱码。这对定位问题至关重要------你调试的是你写的代码,不是编译器吐出来的东西。

还有一个小但关键的点:build 这一步用的是 tsc 而不是 tsxtsc 负责「类型检查 + 产出可部署的 JS」,它开着 noEmit(只检查不落盘也行,看配置),是上线前的质量闸门之一。开发用 tsx 图快,上线用 tsc 图稳,两者分工明确。


四、门禁雏形:tsc、biome、vitest 三件套

讲完初始化,我想给你种下一个贯穿整套后端工程的意识:门禁(gate)

所谓门禁,就是「代码能不能算『过关』」的自动化检查。我们本地的工程,从第一天起就有三道:

  1. tsc --noEmit(类型检查) :编译期拦错,前面说的 undefined 问题就是它抓的。
  2. biome check(代码规范 + 静态检查) :统一格式、禁 any、查未使用变量等。
  3. vitest run(测试):跑单元测试和接口测试,确保行为没被破坏。

这三件套不是我拍脑袋定的,而是行业里反复验证过的「最低防线」。真正有意思的是后面两件事,它们会单独成篇深入讲:

  • 测试策略本身是一套学问({{LINK:M1-21}} 会展开:测试金字塔、用 app.request 发真实请求、测试库怎么隔离)。
  • 除了这三道本地门禁,我们还有一道更特殊的------契约门禁:一份 OpenAPI 文档是「前后端唯一的接口真相源」,任何代码改动都不能偏离它({{LINK:M1-20}} 会讲为什么文档会脱节、又怎么用双门校验守住)。

真实踩坑(P-01):@/ 这个别名,在 TypeScript 7 里差点让我们翻车。baseUrl 这个配置项在 TS 7 被彻底移除了,如果你还像老教程那样写 baseUrl + paths,编译器直接甩你一个 TS5102 错误,项目起不来。正确做法是删掉 baseUrl,只留 paths: { "@/*": ["./src/*"] } ------paths 会相对 tsconfig 自身解析,不需要 baseUrl 当锚。测试侧(vitest)也要对齐,用 resolve.alias['@'] 指到 ./src。这事儿看着小,但凡是照着旧博客配 TS 的,十个有八个会卡在这一行。
真实踩坑(P-02):类型纪律不是开个 strict: true 就完事。我们额外开了两个「硬核」选项。verbatimModuleSyntax: true 强制你用 import type 导入类型------它逼着你在「值」和「类型」之间划清界限,依赖关系更干净。noUncheckedIndexedAccess: true 让上面那个 ROLE_RANK[role] 的类型变成 number | undefined,逼你处理查不到的情况。配合 biome 的 noExplicitAny(禁 any)和「全量避免 any」,这套组合拳下来,很多「运行时才暴露的类型错」在保存的那一刻就被编译器摁住了。


小结

这一篇我们搭起了 Node 后端的工程骨架:

  • 用 TypeScript 而不是裸 JS------后端是共享进程、共享数据,类型护栏的失败成本远低于前端。
  • 七个目录各管一摊 ------routes 薄、services 装领域、shared 放基础设施、types 纯类型,目录是给未来的自己看的契约。
  • tsx watch 做热更新、tsc 做上线前的类型闸门------开发图快、上线图稳。
  • tsc / biome / vitest 三道门禁------从第一天就给代码质量兜底,后面会逐步加深(测试策略、契约门禁)。

三个真实踩坑你先记个印象:TS 7 删了 baseUrl(P-01)、类型纪律要开硬核选项(P-02)、@/ 别名是重构保险丝(P-05)。它们都不是教科书上的标准答案,是我们在这个工程里真踩过、真改过的。

下一篇({{LINK:M1-02}})我们聊框架选型:Express、Koa、Fastify、NestJS 差在哪,以及为什么这个工程最终选了 Hono。


订阅这个专栏

如果你也想跟着一个真实的多端文章系统,从「前端工程师」走到「能设计全栈系统的人」,欢迎订阅我的《成为全栈开发工程师》专栏。整个 Node 后端篇会用三十一篇,带你从工程初始化一路走到部署上线,欢迎在评论区讨论、指正。

相关推荐
梅雅达编程笔记1 小时前
实战:用Harness做一个自动化日报Agent
typescript·实战教程·deepseek·harness·自动化agent
Y3815326622 小时前
body-upload-js
前端·javascript·浏览器·深拷贝·数据克隆
Json____2 小时前
java-宿舍安全卫生检查系统项目源码
java·前端·javascript·课程设计·it学习·wwwoop.com
AbelTomato2 小时前
博客评论系统搭建回顾
javascript·typescript·cloudflare·supabase·hyperdrive
梦醒沉醉2 小时前
4、函数function
javascript
摇滚侠3 小时前
《SpringBoot 3:入门与应用实战》第 9 章 使用 WebMvc 开发应用 阅读笔记 22
javascript·spring boot·笔记
xiaominlaopodaren3 小时前
three.js地图视口瓦片(一):屏幕角点射线
javascript·gis·three.js
用户0934077735143 小时前
HarmonyOS WPS Open SDK 实践:registerApp 鉴权与就绪门禁
android·typescript·harmonyos
一水行3 小时前
博客评论系统搭建回顾
javascript·后端