NestJS 入门指南:用 TypeScript + 依赖注入构建类型安全的 RESTful API

5 阅读5分钟

NestJS 作为基于 TypeScript 的企业级 Node.js 框架,正逐渐成为许多开发者从 Express 迁移的首选。它继承了 Express 的简洁性,同时引入了模块化架构、依赖注入和装饰器模式,这些特性让代码更具结构化、类型安全和可扩展性,尤其适合构建高效、可维护的后端应用。本文将基于一个完整的 Todo API 示例,逐步讲解 NestJS 的核心概念和实践用法,帮助你快速上手。

1. NestJS 简介与安装

NestJS 是一个旨在构建高效、可扩展且易于维护的企业级后端应用的框架。它基于 TypeScript,采用模块化架构和依赖注入(IoC),这与 Angular 的设计哲学类似,能让前端开发者快速适应。相比 Express 的极简主义,NestJS 更注重代码组织和可测试性,避免了大型项目中常见的“意大利面”代码问题。

安装 CLI 并新建项目非常简单:

Bash

npm i -g @nestjs/cli
nest new nest-test-demo

这会生成一个基本的项目结构,包括 main.ts(入口文件)、app.module.ts(根模块)和一些默认的控制器、服务。启动项目后,你可以用 npm run start:dev 进入开发模式,支持热重载。

2. NestJS 的核心理解

NestJS 的设计深受工厂模式影响:它通过 IoC 容器自动管理对象的创建和依赖注入,避免手动实例化。整个应用像一个“工厂”,模块是生产单元,控制器和服务是产品。

  • main.ts:应用的入口文件,通常用于创建 Nest 应用实例、配置中间件和监听端口。例如:

TypeScript

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();
  • Module:NestJS 的基本构建块。每个模块封装了相关的控制器、服务和提供者。通过 @Module 装饰器定义,例如根模块 app.module.ts:

TypeScript

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TodosModule } from './todos/todos.module';
import { DatabaseModule } from './database/database.module';

@Module({
  imports: [
    TodosModule,
    DatabaseModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

这里,imports 引入子模块;controllers 定义路由处理器;providers 注册服务。MVC 模式在这里体现:模型(数据实体)、视图(响应格式,通常是 JSON)、控制器(路由逻辑)。

依赖注入是 NestJS 的灵魂:通过构造函数注入依赖,而不是手动 new。例如,在控制器中注入服务。

3. RESTful API 设计:HTTP 方法的语义化

NestJS 鼓励使用 RESTful 风格,一切皆资源,通过 Method + URL 定义操作。这让 API 更具语义化:

  • GET:读取资源,例如获取列表。
  • POST:创建资源,例如添加新项。
  • PUT:整体更新资源,例如上传/更新头像。
  • PATCH:局部更新,例如修改昵称或密码。
  • DELETE:删除资源,例如删除用户。

在示例中,我们用这些方法构建 Todo API,确保每个端点清晰、可预测。

4. 控制器:路由与参数处理

控制器负责处理 HTTP 请求,使用装饰器如 @Get、@Post 定义路由。示例中的 AppController:

TypeScript

import { Controller, Get, Post, Body, Inject } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(
    @Inject('PG_CONNECTION') private readonly db: any,
    private readonly appService: AppService,
  ) {}

  @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,
        message: "用户名或密码不能为空"
      }
    }
    if (password.length < 6) {
      return {
        code: 400,
        message: "密码长度不能小于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,
      }
    }
  }
}

这里,@Body() 提取请求体;@Inject 注入自定义提供者(如数据库连接)。注意:代码中直接导入 DatabaseModule 不必要,因为模块已在 AppModule 中导入。参数校验是手动写的,生产环境中推荐用 DTO 和 ValidationPipe 自动化。

另一个控制器 TodosController 处理 Todo 相关路由:

TypeScript

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

import {
  TodoService,
} from './todos.service';

@Controller('todos')
export class TodosController {
  constructor(private readonly todosService: TodoService){}

  @Get()
  getTodos() {
    return this.todosService.findAll();
  }

  @Post()
  addTodo(@Body('title') title: string) {
    return this.todosService.addTodo(title);
  }

  @Delete(':id')
  deleteTodo(@Param('id', ParseIntPipe) id: number) {
    console.log(typeof id, '////');
    return this.todosService.deleteTodo(id);
  }
}

ParseIntPipe 自动将 :id 转为 number,如果不是整数会抛 400 错误。这比手动 parseInt 更安全。注意:@Body('title') 只取单个字段,适合简单场景;复杂时用 DTO。

5. 服务层:业务逻辑封装

服务是业务逻辑的核心,通过 @Injectable() 装饰器注册,可注入到控制器中。AppService 示例:

TypeScript

import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  getHello(): string {
    return '你好!!!'
  }
  getWelcome(): string {
    return 'welcome'
  }
  handleLogin(username: string, password: string) {
    if (username === 'admin' && password === '123456') {
      return {
        code: 200,
        message: "登陆成功"
      }
    } else {
      return {
        code: 400,
        message: "登录失败"
      }
    }
  }
}

TodoService 管理 Todo 数据(内存存储,生产中替换为数据库):

TypeScript

import {
  Injectable,
} from '@nestjs/common';

export interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

@Injectable()
export class TodoService {
  private todos: Todo[] = [
    {
      id: 1,
      title: '周五狂欢',
      completed: false,
    },
    {
      id: 2,
      title: '睡觉',
      completed: true,
    }
  ];

  findAll() {
    return this.todos;
  }

  addTodo(title: string) {
    const todo: Todo = {
      id: + Date.now(),
      title,
      completed: false,
    };
    this.todos.push(todo);
    return todo;
  }

  deleteTodo(id: number) {
    this.todos = this.todos.filter((todo) => todo.id !== id);
    return {
      message: 'Todo deleted',
      code: 200
    };
  }
}

这里用接口定义 Todo 结构,确保类型安全。注意:+ Date.now() 是简易 ID 生成,生产中用 uuid 避免冲突。

6. 模块组织:TodosModule

每个业务功能应独立成模块:

TypeScript

import {
  Module
} from '@nestjs/common';

import {
  TodosController,
} from './todos.controller';

import {
  TodoService,
} from './todos.service';

@Module({
  controllers: [TodosController],
  providers: [TodoService],
})
export class TodosModule {}

这让代码解耦,便于测试和复用。

7. 数据库集成:PostgreSQL 示例

NestJS 支持各种数据库,这里用 pg 连接池:

TypeScript

import { Module, Global } from '@nestjs/common';
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 {}

注意:dotenv.config() 只需在入口文件调用一次,这里重复可移除。连接池是全局的,便于注入。db-test 路由演示了查询。

8. 进阶实践与优化建议

  • 参数校验:用 class-validator 库定义 DTO,例如 CreateTodoDto,并启用全局 ValidationPipe。
  • 错误处理:添加异常过滤器统一响应格式。
  • ORM:替换内存数组用 TypeORM 或 Prisma。
  • 安全:登录示例硬编码密码不安全,生产中用 bcrypt + JWT。
  • 测试:NestJS 内置 Jest,支持单元/集成测试。

在实际项目中,这些实践能显著降低 bug 率,提升协作效率。

9. 总结

NestJS 通过 TypeScript 和 IoC,让后端开发更像“组装积木”。从这个 Todo 示例可见,它在类型安全、模块化和可扩展性上远超 Express。建议从一个小项目开始实践,你会发现维护成本大幅下降。