从零写个 NestJS 应用

88 阅读6分钟

写在前面

刚接触 NestJS 的时候,你可能会看到满屏的 @ 符号——@Controller@Injectable@Module……它们到底是什么?为什么加个 @ 就能让框架自动注入服务、注册路由?
其实,这些 @ 背后是 TypeScript 的装饰器机制,也是 NestJS 实现依赖注入和模块管理的核心。

在这篇文章里,我会从创建一个新项目开始,带你一步步看懂 NestJS 的基本结构:控制器怎么处理请求,服务层如何写业务逻辑,模块之间怎么组织,以及如何连接 PostgreSQL 数据库。
你会发现,NestJS 虽然看起来“规矩多”,但它带来的清晰分层和可维护性,特别适合构建中大型后端应用。

安装

首先全局安装 NestJS CLI:

 npm i -g @nestjs/cli

image.png 然后创建一个新项目:

nest new nest-test-demo

我这里选择了 pnpm 作为包管理器,当然你也可以用 npm 或 yarn。

image.png 创建完成后,我们来看看 src 目录的结构:

接下来,我们就从这些文件入手,看看 NestJS 是如何通过 装饰器(Decorator)  来组织代码和实现核心功能的。

image.png

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 等,构建出完整的请求处理流程。

启动项目后,访问对应接口就能看到效果:

image.png

访问数据库

我们使用的是 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 {}

image.png

现在,只要访问 /db-test 接口,就能看到从 PostgreSQL 查询出的用户数据: image.png

这样,我们就完成了 NestJS 与 PostgreSQL 的基础集成。虽然这里直接使用了原生 pg 驱动,但在实际项目中,你也可以选择更高级的 ORM,比如 TypeORM 或 Prisma,它们能提供更强的类型安全和开发体验。

至此,一个包含路由、服务、数据库连接的最小 NestJS 应用就跑起来了。
你会发现:装饰器定义结构,模块组织依赖,分层处理逻辑——这正是 NestJS 清晰架构的魅力所在。

总结

通过这个小项目,我们走通了 NestJS 的基本开发流程:用装饰器定义控制器和服务,通过模块组织依赖,再接入真实数据库完成数据查询。

你会发现,那些一开始看起来“繁琐”的 @Module@Injectable@Controller,其实都在默默帮你建立清晰的代码结构

NestJS 不强制你用 MVC,也不绑定特定 ORM,它提供的是一个骨架。你可以在此基础上选择分层架构、CQRS,甚至 DDD,按需演进。

如果你刚开始接触 NestJS,不妨从这样一个“能跑起来的小例子”出发,慢慢体会它的设计哲学。毕竟,好的框架不是替你写代码,而是帮你在复杂中保持秩序。