大家好 👋,我是 Moment,目前正在使用 Next.js、NestJS、LangChain 开发 DocFlow。这是一个面向 AI 场景的协同文档平台,集成了基于
Tiptap的富文本编辑、NestJS后端服务、实时协作与智能化工作流等核心模块。在这个项目的持续打磨过程中,我积累了不少实战经验,不只是
Tiptap的深度定制、编辑器性能优化和协同方案设计,也包括前端工程化建设、React 源码理解以及复杂项目架构实践。如果你对 AI 全栈开发、文档编辑器、前端工程化或者 React 源码相关内容感兴趣,欢迎添加我的微信
yunmz777一起交流。觉得项目还不错的话,也欢迎给 DocFlow 点个 star ⭐

前两篇把认知铺好了,这一篇进入上手阶段。
如果你是第一次接触 NestJS,最推荐的方式不是手动搭目录,而是直接使用官方 CLI 创建项目。原因很简单,CLI 不只是帮你生成几个文件,它还会顺手把一个标准可运行的项目骨架搭好。这样你第一次接触时,不会一上来就被配置细节打断。
下面的示例统一使用 pnpm。如果你平时用的是 npm 或 yarn,只要把命令替换成自己习惯的包管理器即可。
在终端中执行下面这条命令,就可以创建一个新的 NestJS 项目:
shell
pnpm dlx @nestjs/cli new hello-nest
执行之后,CLI 会提示你选择包管理器。直接选择自己当前项目里最常用的那个就行。如果你平时主要使用 pnpm,这里继续选 pnpm 会更顺手。
这一步完成之后,你会得到一个已经初始化好的项目目录。依赖会自动安装,基础文件也会一并生成,所以不需要你再从零创建入口文件、路由文件和配置文件。
如果你之前没有用过 dlx,可以把它理解成临时拉起来跑一下某个命令行工具,不必先全局安装 @nestjs/cli,用完就走,比较适合第一次体验。
认识默认目录结构
项目创建完成后,先不要急着写代码。更重要的是先看一眼默认目录结构,因为这会直接帮助你理解 NestJS 是怎么组织应用的。
一个刚创建出来的项目,核心目录大致会像下面这样:

第一次看这些文件时,可以先抓最重要的几个:
src/main.ts是应用入口,负责把整个NestJS应用启动起来src/app.module.ts是根模块,用来组织当前应用的基础结构src/app.controller.ts是控制器,负责接收请求和返回结果src/app.service.ts是服务层,负责承载具体业务逻辑
你会发现,哪怕只是一个最简单的 Hello World 项目,NestJS 也没有把所有逻辑都塞进一个文件里。它一开始就把入口、模块、控制器、服务拆开了。这正是前面提到的结构约束。
也就是说,NestJS 希望你从第一个项目开始,就用一种更接近真实业务系统的方式来组织代码,而不是先随便写,等项目变大后再重构。
如果你现在只记一句话,可以先记这个:
src/main.ts 负责启动,Module 负责组织,Controller 负责接请求,Service 负责写逻辑。
后面无论项目变得多复杂,这套基础分工都不会变。
启动开发环境
进入项目目录后,就可以把开发环境跑起来了:
shell
cd hello-nest
pnpm run start:dev
这条命令会以开发模式启动项目,并开启监听。也就是说,你修改 src 里的代码后,服务通常会自动重新编译并重启,不需要你每次手动停掉再启动。
项目启动成功后,终端一般会看到类似 Nest application successfully started 的提示。默认情况下,服务会监听 3000 端口。

这时候你可以先用浏览器打开下面这个地址:
text
http://localhost:3000
如果一切正常,你会看到默认返回的 Hello World!。这说明项目已经跑起来了。
这一步的意义不只是确认环境没问题,更重要的是让你先建立一个非常直接的印象:
一个刚生成出来的 NestJS 项目,本身就是可运行的。
也正因为默认项目能立刻跑起来,后面改代码时,你能很直观地对照改了哪个文件、行为发生了什么变化。
写第一个接口
默认项目已经有一个最基础的接口,只是它的业务非常简单。为了真正理解 NestJS 的组织方式,最好的做法不是新建一堆复杂模块,而是先把这个默认接口改一遍。
先看服务层。把 src/app.service.ts 改成下面这样:
typescript
import { Injectable } from "@nestjs/common";
@Injectable()
export class AppService {
getHello(): { message: string; from: string } {
return {
message: "Hello NestJS",
from: "AppService",
};
}
}
这段代码的重点不是返回什么内容,而是让你看到,真正的业务结果是由 AppService 提供的。控制器不直接写死所有内容,而是去调用服务层。
接着修改 src/app.controller.ts:
typescript
import { Controller, Get } from "@nestjs/common";
import { AppService } from "./app.service";
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get("hello")
getHello(): { message: string; from: string } {
return this.appService.getHello();
}
}
这里有两个点值得第一次接触时特别留意。
第一,@Controller() 和 @Get("hello") 这类装饰器,标的是这个类、这个方法在 HTTP 这一层各自干什么。
第二,控制器通过构造函数拿到 AppService,而不是自己手动 new AppService()。这就是依赖注入最直观的体现。你声明自己需要什么,框架负责把依赖准备好。
改完之后,重新访问下面这个地址:
text
http://localhost:3000/hello
如果一切正常,你会看到类似下面这样的返回结果:

看到这里,其实你已经完成了自己的第一个 NestJS 接口。虽然它非常简单,但最关键的骨架已经出现了。
从 Hello World 看 NestJS 的基本组织方式
一个最简单的 Hello World 也能看出 NestJS 的分层习惯,请求不会随便落进某个函数,而是按约定往前走。最短路径可以先想成下面五步:
- 客户端发起请求
- 路由命中控制器方法
- 控制器调用服务层
- 服务层返回业务结果
- 框架把结果写回客户端

在这五步之上还要叠上两块,根 Module(例如 AppModule)把 Controller 和 Service 登记到同一个模块里,src/main.ts 创建应用实例并监听端口,进程才真正跑起来。接请求、写业务、做装配、拉起监听,是同一条最小链路上的不同环节。
对应到代码里,先记住四个角色就够:
Controller接住请求Service处理业务Module把相关角色收进同一个模块src/main.ts把应用跑起来并监听端口
这和先把逻辑全塞进一个文件再说的写法差别很大,NestJS 从第一个接口就在推你把入口、业务和装配拆开。返回值具体写了哪句文案反而不那么重要,更值得留意的是三件事已经成习惯:入口和业务分开、依赖交给框架装配、结构按角色长而不是按临时想法堆。
把这条轮廓记熟,后面再学模块拆分、参数校验、异常处理、数据库接入,都容易挂回同一套形状里。
小结
第一个 NestJS 项目的重点,不是把服务跑起来本身,而是借这个最小示例看清它的基本骨架。
通过 CLI 创建项目,你拿到的是一套标准初始结构。通过修改默认接口,你能看到 Controller、Service、依赖注入和应用入口是如何协同工作的。
如果你现在已经能理解下面这几件事,这一篇的目标就达到了:
- 如何用
CLI创建一个NestJS项目 - 默认目录里几个核心文件分别负责什么
- 怎样启动开发环境并访问默认服务
- 怎样改出自己的第一个接口
- 为什么这个最小例子已经体现了
NestJS的基本组织方式
接下来会从这个最小项目出发,看 Controller 和路由是怎么对应起来的。