Nest.js 是什么:怎样用它写出第一个后端接口

本文是「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 接口、后台服务和其他服务端程序,官方入门内容也是从创建应用、控制器、服务和根模块开始。

在当前用户列表接口里,需要完成三件事:

  1. 启动一个可以接收 HTTP 请求的应用
  2. 把 GET /users 和一段 TypeScript 方法关联起来
  3. 把方法返回的数组发送给请求方

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 生成的文件分别承担什么职责。

参考资料

相关推荐
打工仔折腾 AI7 小时前
工业场景下时序库与实时计算一体化架构选型实践对比
java·开发语言·后端·python·性能优化·架构·ai agent 实战
蜗牛互联网8 小时前
长任务多Agent共享文件系统的Manifest交接模式
java·人工智能·后端
苏supper9 小时前
Spring Boot多环境配置与打包运行
spring boot·后端·spring
geovindu10 小时前
rust: Composite Pattern
开发语言·后端·设计模式·rust·组合模式
SearchMan10 小时前
还有人不知道&和&&的区别?
后端
想用offer打牌10 小时前
Personal Agent爆火 - 它到底是个什么
人工智能·后端·ai编程
IT_陈寒10 小时前
Vue的v-if和v-for混用居然是个天坑
前端·人工智能·后端
打工仔折腾 AI12 小时前
从Attention到BERT:双向预训练语言模型到底解决了什么问题
人工智能·后端·python·深度学习·语言模型·bert
lizhongxuan14 小时前
Agent Sandbox: Firecracker 任务生命周期
后端
YYYing.14 小时前
【设计模式系列 (七) 】桥接模式
c++·后端·设计模式·桥接模式