AI全栈入门指南:一文搞清楚NestJs 中的 Controller 和路由

0 阅读7分钟

大家好 👋,我是 Moment,目前正在使用 Next.js、NestJS、LangChain 开发 DocFlow。这是一个面向 AI 场景的协同文档平台,集成了基于 Tiptap 的富文本编辑、NestJS 后端服务、实时协作与智能化工作流等核心模块。

在这个项目的持续打磨过程中,我积累了不少实战经验,不只是 Tiptap 的深度定制、编辑器性能优化和协同方案设计,也包括前端工程化建设、React 源码理解以及复杂项目架构实践。

如果你对 AI 全栈开发、文档编辑器、前端工程化或者 React 源码相关内容感兴趣,欢迎添加我的微信 yunmz777 一起交流。觉得项目还不错的话,也欢迎给 DocFlow 点个 star ⭐

image.png

上一节我们已经跑通了第一个 NestJS 项目,也看到了 ControllerService 是如何配合的。这一节继续往前走,专门看 Controller 到底负责什么,以及路由在 NestJS 里是怎么声明出来的。

如果只用一句话概括,Controller 做的是"把一个 HTTP 请求接进来,再把结果返回出去"。至于真正的业务逻辑,通常不应该堆在控制器里,而是交给 Service

定义路由

NestJS 里,路由不是写在一张单独的表里,而是直接声明在控制器类和它的方法上。

类上的 @Controller() 用来定义这一组接口的公共路径。方法上的 @Get()@Post() 这类装饰器,用来定义具体某个接口对应的请求方式和子路径。

下面这段代码演示了一个很常见的写法:

import { Controller, Get } from "@nestjs/common";

@Controller("users")
export class UsersController {
  @Get()
  findAll(): string {
    return "all users";
  }

  @Get("profile")
  findProfile(): string {
    return "user profile";
  }
}

这段代码的含义分别是:

  • @Controller('users') 表示这一组接口都挂在 /users 下面
  • @Get() 对应 GET /users
  • @Get('profile') 对应 GET /users/profile

你可以把控制器理解成"某一类资源或某一块功能的入口集合"。比如用户相关接口放进 UsersController,订单相关接口放进 OrdersController。这样路径组织和代码组织会更一致。

GETPOSTPUTDELETE

NestJS 对常见 HTTP 方法都提供了对应装饰器。最常用的就是 @Get()@Post()@Put()@Delete()

下面这个例子把常见写法放在一起看,会更直观:

import {
  Body,
  Controller,
  Delete,
  Get,
  Param,
  Post,
  Put,
} from "@nestjs/common";

interface CreateUserDto {
  name: string;
  email: string;
}

interface UpdateUserDto {
  name?: string;
}

@Controller("users")
export class UsersController {
  @Get()
  findAll(): string {
    return "get all users";
  }

  @Post()
  create(@Body() body: CreateUserDto): CreateUserDto {
    return body;
  }

  @Put(":id")
  update(
    @Param("id") id: string,
    @Body() body: UpdateUserDto,
  ): { id: string; body: UpdateUserDto } {
    return { id, body };
  }

  @Delete(":id")
  remove(@Param("id") id: string): { deletedId: string } {
    return { deletedId: id };
  }
}

第一次看这段代码时,先不要急着记所有装饰器。先抓住一个核心规律:

不同 HTTP 方法,本质上就是在告诉框架,"同样是某个路径,这次应该用哪种请求方式来匹配它"。

通常可以先这样理解它们的语义:

  • GET 用来读取数据
  • POST 用来创建数据
  • PUT 用来整体更新数据
  • DELETE 用来删除数据

这不是绝对规则,但它是最常见的约定。按照这个约定设计接口,团队协作时会更容易理解。

Path 参数、Query 参数、Body 参数

写接口时大半时间在跟入参打交道。浏览器和客户端把数据放在 URL 路径里、问号后面或请求体里,NestJS 用三种装饰器一一对应,名字和业务含义基本对齐,读方法签名就能猜出数据从哪来。

下面这段代码放在同一个 PostsController 里:上面是带路径段和查询串的 GET,下面是读 JSON 体的 POST

import { Body, Controller, Get, Param, Post, Query } from "@nestjs/common";

interface CreatePostDto {
  title: string;
  content: string;
}

@Controller("posts")
export class PostsController {
  @Get(":id")
  findOne(
    @Param("id") id: string,
    @Query("preview") preview?: string,
  ): { id: string; preview?: string } {
    return { id, preview };
  }

  @Post()
  create(@Body() body: CreatePostDto): CreatePostDto {
    return body;
  }
}

对应关系可以这样记:

  • @Param() 对应路径里的动态段,/posts/123 里的 123 会进 id
  • @Query() 对应 ? 后面的键值,/posts/123?preview=true 里的 preview 会进来,没有则 previewundefined(这里写了可选参数)
  • @Body() 对应报文主体,常见于 POSTPUTPATCH 提交的 JSON 或表单序列化结果

更短的一句口诀是,路径用 @Param(),问号后用 @Query(),包体用 @Body()

这样一来,控制器里很少出现"这段到底读的是 req 的哪一块"的猜测。来源都写在参数列表上,也比到处翻 req.paramsreq.queryreq.body 更直观。

Header、状态码、重定向

除了读路径和请求体,控制器有时候还需要读取请求头、设置状态码,或者做重定向。NestJS 也提供了比较声明式的写法。

先看请求头的读取方式:

import { Controller, Get, Headers } from "@nestjs/common";

@Controller("info")
export class InfoController {
  @Get()
  getClient(@Headers("user-agent") userAgent?: string): { userAgent?: string } {
    return { userAgent };
  }
}

这里的 @Headers('user-agent') 就是在读取请求头中的 user-agent

如果你想显式设置状态码,也可以这样写:

import { Controller, HttpCode, Post } from "@nestjs/common";

@Controller("users")
export class UsersController {
  @Post("login")
  @HttpCode(200)
  login(): { message: string } {
    return { message: "login success" };
  }
}

这个例子里,虽然是 POST 请求,但我们明确把返回状态码设成了 200。这在登录接口这类场景里很常见。

如果你想做重定向,可以使用 @Redirect()

import { Controller, Get, Redirect } from "@nestjs/common";

@Controller()
export class AppController {
  @Get("docs")
  @Redirect("https://docs.nestjs.com", 302)
  goDocs(): void {}
}

这段代码的意思是,当用户访问 /docs 时,服务端直接把请求重定向到指定地址。

所以这一节可以先总结成一句话:

控制器不只负责匹配路径,它还负责把请求中的关键信息拿出来,并按需要影响最终响应行为。

返回值与原生 response 的区别

这是很多初学者刚接触 NestJS 时容易困惑的一点。

在大多数情况下,你只需要"直接返回值"就够了。比如返回对象、数组、字符串,NestJS 会帮你把这些结果自动序列化并发送给客户端。

例如下面这种写法,就是最推荐的默认方式:

import { Controller, Get } from "@nestjs/common";

@Controller("health")
export class HealthController {
  @Get()
  check(): { status: string } {
    return { status: "ok" };
  }
}

它的好处是,代码简洁,也更容易和 Interceptor、异常过滤器、状态码装饰器这些机制配合。

NestJS 也允许你拿到原生响应对象,比如 Express 下的 response。这种方式适合你需要手动控制响应细节的场景,比如流式输出、文件下载、特殊响应头等。

写法通常像这样:

import { Controller, Get, Res } from "@nestjs/common";
import type { Response } from "express";

@Controller("download")
export class DownloadController {
  @Get()
  download(@Res() res: Response): void {
    res.status(200).json({ message: "manual response" });
  }
}

一旦你使用了 @Res(),就意味着这一段响应由你自己接管。框架不会再按默认方式帮你自动返回结果。

所以两种方式的区别可以先这样理解:

  • 直接 return,更符合 NestJS 的默认风格,日常接口优先使用
  • 使用原生 response,控制力更强,但你需要自己负责响应的发送

对初学者来说,有一个很实用的判断标准:

如果只是普通的 JSON 接口,优先直接 return。 只有当你确实需要精细控制响应过程时,再考虑使用 @Res()

小结

ControllerNestJS 里承担的是请求入口角色。它负责定义路由、读取参数、组织响应,但不应该承载过多业务逻辑。

这一篇最重要的收获,可以先落成下面几件事:

  • 路由通过控制器类和方法上的装饰器来声明
  • GETPOSTPUTDELETE 对应不同的 HTTP 请求方式
  • @Param()@Query()@Body() 分别读取不同来源的参数
  • @Headers()@HttpCode()@Redirect() 可以影响请求处理和响应行为
  • 普通接口优先直接 return,原生 response 适合特殊控制场景

下一节,我们会继续从控制器往下走,看看 ServiceProviderModule 是怎样把业务能力真正组织起来的。