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。建议从一个小项目开始实践,你会发现维护成本大幅下降。