本文是「Nest.js」系列的第 1 篇。
用户没有提供具体项目和版本,本文按照 Nest 官方入门项目常见的 TypeScript 结构编写,不展开版本差异。第一篇只处理一个最小的 GET 接口,数据库、参数校验、登录和权限留到后续文章。
一个用户管理页面已经写好了列表组件,现在只缺一份数据。前端准备请求:
text
GET http://localhost:3000/users
希望服务端返回:
json
[
{
"id": 1,
"name": "Alice"
},
{
"id": 2,
"name": "Bob"
}
]
如果直接写一个很小的 Node.js HTTP 服务,也能完成这件事。但如果项目继续加入新增用户、参数校验、登录、权限和数据库以后,接口代码需要有稳定的位置,功能之间也要有清楚的组织方式,这些用原生的 Node.js 实现起来较为麻烦。Nest.js 提供了一套约定,让请求入口、业务处理和功能注册分别落在不同文件中。
我们通过了解一次 GET /users 请求怎样找到代码,以及最小接口为什么需要控制器、根模块和启动文件来开始我们学习 Nest.js 的道路。
一、Nest.js 在这个接口中负责什么
Nest.js 是运行在 Node.js 环境中的服务端应用框架。它可以用来编写 HTTP 接口、后台服务和其他服务端程序,官方入门内容也是从创建应用、控制器、服务和根模块开始。
在当前用户列表接口里,需要完成三件事:
- 启动一个可以接收 HTTP 请求的应用
- 把
GET /users和一段 TypeScript 方法关联起来 - 把方法返回的数组发送给请求方
Nest.js 帮助项目组织这几步,但用户数据仍然要由项目代码提供。它不会自动生成数据库内容,也不会自动判断谁有权限访问接口。
可以先把当前分工理解为:
| 位置 | 当前负责的事情 |
|---|---|
| Node.js | 提供服务端运行环境 |
| Nest.js | 启动应用并组织请求处理代码 |
| Controller | 接收指定地址的请求并返回结果 |
| 项目代码 | 决定用户数据和业务规则 |
本章先简单介绍控制器 Controller 的内容。
二、先创建并启动一个最小项目
为了让示例可以直接运行,先使用 Nest 官方提供的命令行工具创建项目:
bash
npm i -g @nestjs/cli
nest new nest-first-api
cd nest-first-api
npm run start:dev
nest new 会生成项目目录并安装基础依赖。npm run start:dev 用开发模式启动应用,并在文件变化后重新编译和加载代码。项目创建命令具体生成了哪些配置,会在下一篇单独拆开。
终端启动成功后,官方入门项目通常会监听 3000 端口。浏览器访问:
text
http://localhost:3000/
可以看到初始返回内容。
这一步说明服务已经启动,但还没有 /users 接口。浏览器访问下面的地址时:
text
http://localhost:3000/users
通常会得到路由未找到的响应。接下来需要告诉 Nest.js,哪段代码负责处理这个地址。
三、Controller 怎样声明第一个接口
控制器负责接收进入应用的请求,并把处理结果返回给客户端。在 Nest.js 中,这类文件通常使用 .controller.ts 结尾。
在 src 目录中创建 users.controller.ts。这段代码只返回两条固定数据,读者需要关注类上方和方法上方的两个装饰器:
ts
import {
Controller,
Get,
} from '@nestjs/common';
@Controller('users')
export class UsersController {
@Get()
findAll() {
return [
{
id: 1,
name: 'Alice',
},
{
id: 2,
name: 'Bob',
},
];
}
}
@Controller('users') 声明了这组接口共同使用的路径前缀 /users。
@Get() 声明 findAll() 负责处理 GET 请求。方法装饰器没有继续填写子路径,因此控制器前缀和请求方法组合后的地址就是:
text
GET /users
findAll 只是普通的方法名,可以换成其他名称。Nest.js 根据装饰器声明的请求方法和路径建立路由关系,不会根据 findAll 这个名字猜测接口地址。
当前方法返回一个数组。使用 Nest.js 的标准响应方式时,对象和数组会被转换成 JSON,GET 请求默认返回 200 状态码。因此前端请求成功后,会收到两条用户数据。
这段代码还没有查询数据库。每次请求得到的内容都来自方法里写死的数组,重启服务也不会影响结果。
四、只创建 Controller,为什么接口还不能访问
控制器文件写完后,Nest.js 还需要知道它属于当前应用。打开 src/app.module.ts,把 UsersController 注册到根模块中:
ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
@Module({
controllers: [
UsersController,
],
})
export class AppModule {}
@Module() 用来描述当前模块包含哪些控制器和其他能力。这里的 controllers 数组告诉 Nest.js:
text
启动 AppModule 时
→ 创建 UsersController
→ 读取它声明的路由
→ 把 GET /users 加入应用
如果只创建 users.controller.ts,却没有放进任何模块的 controllers 中,这个类只是项目里的普通 TypeScript 文件。文件位置相同不代表 Nest.js 会自动扫描并启用它。
官方 CLI 创建的初始项目一般还带有 AppController 和 AppService。为了让示例保持清楚,可以暂时保留它们,并把新控制器加入原来的数组:
ts
@Module({
controllers: [
AppController,
UsersController,
],
providers: [
AppService,
],
})
export class AppModule {}
两种写法都可以。当前文章只关心 UsersController 是否已经被根模块注册。
模块在后续项目中会用于组织一组相关功能。第一篇先把它理解为应用的注册位置,imports、providers 和 exports 的关系会在专门的模块文章中解释。
五、main.ts 怎样让应用开始接收请求
项目入口位于 src/main.ts。官方入门项目中的启动代码通常类似下面这样:
ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(
AppModule,
);
await app.listen(
process.env.PORT ?? 3000,
);
}
bootstrap();
这段代码处于应用启动阶段。NestFactory.create(AppModule) 根据根模块创建 Nest 应用,随后 listen() 开始监听端口。
没有配置 PORT 时,当前代码使用 3000。因此前端访问:
text
http://localhost:3000/users
请求才有机会到达刚才注册的控制器。
第一篇不展开启动过程里的依赖创建和底层 HTTP 平台。这里需要记住的只有一条关系:
text
main.ts 启动 AppModule
→ AppModule 注册 UsersController
→ UsersController 声明 GET /users
如果端口被占用,应用会在启动阶段报错,如果端口修改为其他值,前端请求地址也要一起调整。
六、一次 GET /users 请求经过了哪些位置
服务启动以后,在浏览器地址栏访问:
text
http://localhost:3000/users
也可以在终端中执行:
bash
curl http://localhost:3000/users
请求进入应用后的主要过程是:
text
浏览器或 curl 发出 GET /users
→ 运行中的 Nest 应用接收请求
→ 路由匹配到 UsersController
→ 调用 findAll()
→ 方法返回用户数组
→ Nest.js 将数组作为 JSON 响应发送
最终响应内容类似:
json
[
{
"id": 1,
"name": "Alice"
},
{
"id": 2,
"name": "Bob"
}
]
控制器方法返回 JavaScript 数组;在标准响应模式下,Nest.js 会负责把对象或数组序列化后写入 HTTP 响应。
当前写法没有直接操作底层响应对象,因此也不需要手动调用 send() 或 json()。后续需要自定义响应头、状态码或直接使用底层平台接口时,再讨论相应写法。
七、这个接口为什么暂时没有 Service
官方入门项目通常同时包含 Controller 和 Service。当前示例故意先把固定数组写在控制器中,因为第一篇需要先看清请求怎样找到方法。
随着需求增加,控制器可能开始出现这些代码:
- 查询数据库;
- 判断用户状态;
- 合并多个数据来源;
- 调用第三方接口;
- 处理新增和修改规则。
这些内容长期堆在控制器里,接口入口和业务规则会混在一起。Nest.js 项目通常把复杂处理交给 Service,由控制器接收请求并调用服务,关于 Service 的内用我们后面再详细讨论。
八、Nest.js、Node.js 和底层 HTTP 平台怎样区分
写出第一个接口后,可以再补充一层必要的边界。
Nest.js 应用运行在 Node.js 环境中。Node.js 提供运行 TypeScript 编译结果、访问网络和管理进程所需的基础能力。Nest.js 在这些能力之上组织控制器、服务和模块。
Nest.js 还需要 HTTP 平台负责底层请求与响应。官方文档说明,它可以使用 Express 或 Fastify,官方入门项目默认使用 Express 平台。第一篇没有直接调用 Express API,因为 Nest.js 已经提供了控制器和标准响应方式。
| 层次 | 当前接口中负责的事情 |
|---|---|
| Node.js | 运行服务端程序并提供网络环境 |
| HTTP 平台 | 接收底层 HTTP 请求并发送响应 |
| Nest.js | 根据模块和装饰器组织请求处理 |
| Controller | 处理 GET /users 并返回数组 |
| 业务代码 | 决定应该返回哪些用户 |
九、第一个接口还没有处理哪些问题
现在的 /users 可以返回 JSON,但它只是一个演示接口。
第一,数据写死在代码里。新增用户、修改用户和重启服务都不会形成可持久保存的数据,需要数据库或其他存储方案。
第二,没有参数。分页、搜索、用户详情等需求需要读取查询参数或路径参数,这部分会在接口参数文章中处理。
第三,没有输入校验。后续加入 POST 请求后,请求体即使写了 TypeScript 类型,也不代表运行时数据已经经过检查。DTO 和校验管道需要单独配置。
第四,没有登录和权限。任何能够访问端口的人都可以调用该接口。JWT、Guard 和资源权限属于后面的用户体系与请求链路内容。
第五,没有统一错误处理和日志。固定数组几乎不会失败,接入数据库和外部服务后,需要考虑异常如何返回、日志记录哪些信息,以及请求超时后怎样处理。
所以,第一个接口的完成标准只是:
text
服务能够启动
→ GET /users 能匹配到控制器
→ 控制器返回预期 JSON
它不能代表项目已经具备完整的服务端工程能力。
十、小结
Nest.js 是用于构建 Node.js 服务端应用的框架。第一条用户列表接口中,它负责启动应用、读取模块注册信息,并根据控制器上的声明把 GET /users 请求交给 findAll() 方法。
这次只用了三个位置:main.ts 启动应用,AppModule 注册控制器,UsersController 声明请求地址并返回数据。控制器返回数组后,Nest.js 的标准响应方式会把它作为 JSON 发给客户端。
下一篇会从项目创建命令开始,看看 Nest CLI 生成的文件分别承担什么职责。