写在前面
刚接触 NestJS 的时候,你可能会看到满屏的 @ 符号——@Controller、@Injectable、@Module……它们到底是什么?为什么加个 @ 就能让框架自动注入服务、注册路由?
其实,这些 @ 背后是 TypeScript 的装饰器机制,也是 NestJS 实现依赖注入和模块管理的核心。
在这篇文章里,我会从创建一个新项目开始,带你一步步看懂 NestJS 的基本结构:控制器怎么处理请求,服务层如何写业务逻辑,模块之间怎么组织,以及如何连接 PostgreSQL 数据库。
你会发现,NestJS 虽然看起来“规矩多”,但它带来的清晰分层和可维护性,特别适合构建中大型后端应用。
安装
首先全局安装 NestJS CLI:
npm i -g @nestjs/cli
然后创建一个新项目:
nest new nest-test-demo
我这里选择了 pnpm 作为包管理器,当然你也可以用 npm 或 yarn。
创建完成后,我们来看看
src 目录的结构:
接下来,我们就从这些文件入手,看看 NestJS 是如何通过 装饰器(Decorator) 来组织代码和实现核心功能的。
app.module.ts
@Module 装饰器的作用是把 AppModule 类标记为一个 NestJS 模块。
这种“用注解/装饰器声明功能”的方式,在很多现代框架里都能看到。但具体怎么实现,其实高度依赖语言本身的特性。
比如在 Java 中,类是一等公民,Spring Boot 可以在启动时扫描类路径,把所有带注解的类收集到一个 Map 里(key 是类名,value 是实例)。
而在 JavaScript/TypeScript 里,对象才是一等公民,类本质上只是一个构造函数。
所以 NestJS 并不会自动扫描文件,而是通过你在 @Module 中显式注册的 providers 和 controllers,结合装饰器元数据,构建内部的依赖图。
它持有的不是“类”,而是对构造函数的引用,并封装在 InstanceWrapper 这样的结构中来管理生命周期和依赖关系。(这里就不继续展开讲了。)
接下来再来看看app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
我们可以在这个控制器里添加更多路由,比如登录接口和数据库测试接口:
import { Body, Controller, Get, Post ,Inject} from '@nestjs/common'
import { AppService } from './app.service'
import { log } from 'console';
// 负责路由
@Controller()
export class AppController {
@Inject('PG_CONNECTION')private readonly db: any
constructor(private readonly appService: AppService) {
}
@Get()
getHello(){
return this.appService.getHello();
}
@Get('welcome')
getWelcome():string {
return this.appService.getWelcome();
}
@Post('login')
login(@Body() body: {username: string, password :string }){
const { username, password } = body;
console.log(username,password);
if(!username || !password) {
return {
code: 400,
msg: '用户名密码不能为空'
}
}
if(password.length < 6) {
return {
code: 400,
msg: '密码不能少于6位'
}
}
return this.appService.handleLogin(username,password)
}
@Get('db-test')
async testConnection() {
try {
const res = await this.db.query('SELECT * from users')
return {
status: '连接成功',
data: res.rows
}
}
catch (error) {
return {
status: '连接失败',
error: error.message
}
}
}
}
可以看到,@Controller 定义了这个类是一个路由处理器,而 @Get、@Post 等装饰器则绑定了具体的 HTTP 方法和路径。参数上的 @Body() 会自动解析请求体,@Inject('PG_CONNECTION') 则是从容器中注入我们自定义的数据库连接。
这一切的背后,都是装饰器 + 元数据 + 依赖注入系统在协同工作。
app.service.ts
我们再来看看service:
从这里能明显看出 NestJS 采用的是分层架构:Controller 负责处理请求和响应,Service 则专注于业务逻辑。两者职责分离,代码更清晰、也更容易测试。
如果你在这样的结构上再加一个 View 层(比如用 Handlebars 或 Pug 渲染 HTML) ,那就构成了传统的 MVC 模式。但 NestJS 本身并不强制你这么做——它足够灵活。MVC 只是其中一种使用方式。 这是官方给我们的AppService代码:
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}
我们可以往里面添加自己的逻辑,比如:
import { Injectable } from '@nestjs/common'
@Injectable()
export class AppService {
getHello(): string {
return '小王';
}
getWelcome():string {
return 'nestjs 测试项目'
}
handleLogin(username: string, password: string) {
if(username === "admin" && password === "123456") {
return {
code: 200,
msg: '登录成功'
}
}else{
return {
code: 400,
msg: '用户名密码错误'
}
}
}
}
注意这里的 @Injectable() ——
它是一个装饰器,作用是告诉 NestJS:“这个类可以被依赖注入容器管理”。
正是通过这类装饰器,NestJS 才能自动完成服务的注册、实例化和注入,配合 @Controller、@Get、@Body 等,构建出完整的请求处理流程。
启动项目后,访问对应接口就能看到效果:
访问数据库
我们使用的是 PostgreSQL(psql) 。
在此之前,已经向 users 表中插入了如下测试数据:
INSERT INTO "users" ("id", "name", "password") VALUES
('1', '王皓', '$2b$10$CsO/ykedPpuxqUETBZTYm.F2U4TXDdo01rLmoRPwjKBv3pIL5pnWq'),
('2', '小雪', '$2b$10$CsO/ykedPpuxqUETBZTYm.F2U4TXDdo01rLmoRPwjKBv3pIL5pnWq'),
('3', '李白', '$2b$10$CsO/ykedPpuxqUETBZTYm.F2U4TXDdo01rLmoRPwjKBv3pIL5pnWq'),
('4', '杜甫', '$2b$10$CsO/ykedPpuxqUETBZTYm.F2U4TXDdo01rLmoRPwjKBv3pIL5pnWq'),
('5', '白居易', '$2b$10$CsO/ykedPpuxqUETBZTYm.F2U4TXDdo01rLmoRPwjKBv3pIL5pnWq'),
('6', '张三', '$2b$10$CsO/ykedPpuxqUETBZTYm.F2U4TXDdo01rLmoRPwjKBv3pIL5pnWq');
现在,我们要通过接口查询这些数据。
首先,安装 PostgreSQL 的 Node.js 驱动:
我们要安装一下链接池
pnpm i pg
接着就在自己的env文件下填写自己的配置就好了。
DB_HOST=localhost
DB_PORT=5432
DB_USER=your_user
DB_PASSWORD=your_password
DB_NAME=your_db
接着,创建一个全局的数据库模块来管理连接:
import { Module, Global } from '@nestjs/common';
// 数据库驱动
// @ts-ignore 缺少 @types/pg,先忽略类型检查
import { Pool } from 'pg';
import * as dotenv from 'dotenv';
dotenv.config();
// 数据库基础服务
@Global() // 全局服务
@Module({
providers:[
{
provide: 'PG_CONNECTION',
// 连接池
useValue: new Pool({
user: process.env.DB_USER,
host: process.env.DB_HOST,
database: process.env.DB_NAME,
password: process.env.DB_PASSWORD,
port: parseInt(process.env.DB_PORT || '5432', 10),
})
}
],
exports: ['PG_CONNECTION']
})
export class DatabaseModule {}
现在,只要访问 /db-test 接口,就能看到从 PostgreSQL 查询出的用户数据:
这样,我们就完成了 NestJS 与 PostgreSQL 的基础集成。虽然这里直接使用了原生 pg 驱动,但在实际项目中,你也可以选择更高级的 ORM,比如 TypeORM 或 Prisma,它们能提供更强的类型安全和开发体验。
至此,一个包含路由、服务、数据库连接的最小 NestJS 应用就跑起来了。
你会发现:装饰器定义结构,模块组织依赖,分层处理逻辑——这正是 NestJS 清晰架构的魅力所在。
总结
通过这个小项目,我们走通了 NestJS 的基本开发流程:用装饰器定义控制器和服务,通过模块组织依赖,再接入真实数据库完成数据查询。
你会发现,那些一开始看起来“繁琐”的 @Module、@Injectable、@Controller,其实都在默默帮你建立清晰的代码结构
NestJS 不强制你用 MVC,也不绑定特定 ORM,它提供的是一个骨架。你可以在此基础上选择分层架构、CQRS,甚至 DDD,按需演进。
如果你刚开始接触 NestJS,不妨从这样一个“能跑起来的小例子”出发,慢慢体会它的设计哲学。毕竟,好的框架不是替你写代码,而是帮你在复杂中保持秩序。