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 创建的初始项目一般还带有 AppControllerAppService。为了让示例保持清楚,可以暂时保留它们,并把新控制器加入原来的数组:

ts 复制代码
@Module({
  controllers: [
    AppController,
    UsersController,
  ],
  providers: [
    AppService,
  ],
})
export class AppModule {}

两种写法都可以。当前文章只关心 UsersController 是否已经被根模块注册。

模块在后续项目中会用于组织一组相关功能。第一篇先把它理解为应用的注册位置,importsprovidersexports 的关系会在专门的模块文章中解释。

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

参考资料

相关推荐
aiopencode1 小时前
SwiftUI Introspect生产环境完全指南:为什么它是安全可靠的选择
后端·ios
shengjk11 小时前
x86架构发展史:从8086到x86-64,一文看懂40多年CPU指令集如何改变世界
后端
JackSparrow4143 小时前
前端安全之JS混淆+请求加密+请求签名以提升爬虫难度
前端·javascript·后端·爬虫·python·安全
geovindu3 小时前
go:loghelper
开发语言·后端·golang
柳林林3 小时前
解决 nvm 切换 Node.js 版本时报“没有文件扩展 ‘.vbs’ 的脚本引擎”错误
node.js
小满zs4 小时前
Go语言第四章(类型转换)
后端·go
人间凡尔赛5 小时前
告别冷启动!WebAssembly + Spin 实战:Serverless 延迟从 1 秒降到 1 毫秒
后端·云原生·serverless·webassembly·spin
陈随易12 小时前
bm2,MoonBit实现的pm2替代品
前端·后端·程序员
To_OC12 小时前
从 0 到 1:Milvus + 大模型打造私人记忆知识库
人工智能·node.js·llm