NestJS 入门:从零到 CRUD 接口
本文基于一个真实的 NestJS + MongoDB 项目,带你理解 NestJS 的核心概念和目录结构,适合有 Node/TypeScript 基础、想上手 Nest 的读者。
一、话不多说,NestJS 是什么得搞清楚?
NestJS 是一个用于构建服务端应用的 Node 框架,底层默认基于 Express(也可切换为 Fastify)。支持用 TypeScript 编写,采用模块化和依赖注入,结构类似 Angular,适合写中大型后端项目。
让我们简要概括一下特点:
- 分层清晰:Controller → Service → Model,职责分离
- 装饰器驱动:路由、参数、校验等用
@Get()、@Body()等声明 - 模块化:按功能拆成 Module,便于扩展与复用
- 开箱即用:与 TypeORM、Mongoose、Config、Validation 等集成简单
二、项目结构一览
src/
├── main.ts # 应用入口,创建实例、全局配置、监听端口
├── app.module.ts # 根模块,汇总各功能模块与全局依赖
├── app.controller.ts # 根控制器(如健康检查)
├── app.service.ts
├── example/ # 示例功能模块(按业务划分)
│ ├── example.module.ts # 示例模块:注册模型、控制器、服务
│ ├── example.controller.ts # 示例相关 HTTP 接口
│ ├── example.service.ts # 示例业务逻辑与数据库操作
│ ├── dto/
│ │ └── example.dto.ts # 入参/出参的数据结构
│ └── schemas/
│ └── example.schemas.ts # Mongoose 文档模型
├── tansform/
│ └── tansform.interceptor.ts # 全局响应包装(统一返回格式)
└── http-exeption/
└── http-exception.filter.ts # 全局异常捕获与错误返回格式
先建立整体印象:入口 → 根模块 → 各功能模块(Module + Controller + Service + Schema/DTO),再配合拦截器、过滤器做统一格式和错误处理。
三、首先看一下入口文件:main.ts
大都为固定写法,应用从这里启动:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { TansformInterceptor } from './tansform/tansform.interceptor';
import { HttpExceptionFilter } from './http-exeption/http-exception.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('api'); // 所有路由加 /api 前缀
app.useGlobalInterceptors(new TansformInterceptor()); // 统一成功响应格式
app.useGlobalFilters(new HttpExceptionFilter()); // 统一错误响应格式
await app.listen(process.env.PORT ?? 3005);
}
bootstrap();
简单解释一下:
NestFactory.create(AppModule):根据根模块创建应用。setGlobalPrefix('api'):例如/example会变成/api/example。- 全局拦截器:在控制器返回后,把结果包成
{ errno: 0, data }。 - 全局异常过滤器:捕获
HttpException,返回{ error: -1, message, ... }。 - 端口从环境变量
PORT读,默认 3005。
四、再看根模块:app.module.ts
根模块负责「拼装」整个应用:配置、数据库、各业务模块。
@Module({
imports: [
ConfigModule.forRoot(), // 必须最先加载,才能读到 process.env
MongooseModule.forRoot(
`mongodb://${process.env.MONGO_HOST}:${process.env.MONGO_PORT}/${process.env.MONGO_DATABASE}`,
),
ExampleModule,
UserModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
注意几个点:
- ConfigModule.forRoot() 要放在最前面,否则后面的
process.env.*可能是undefined(例如 MongoDB 连接会变成undefined:undefined)。 - MongooseModule.forRoot() 连接 MongoDB,一条连接多模块复用。
- ExampleModule、UserModule 等是功能模块,各自内部再注册 Controller、Service 和
forFeature的模型。
五、功能模块:以 Example 为例
一个完整功能通常包含:Module + Controller + Service + Schema(+ DTO)。
5.1 模块:example.module.ts
模块把「该功能需要的模型、控制器、服务」捆绑在一起:
@Module({
imports: [
MongooseModule.forFeature([
{ name: Example.name, schema: ExampleSchema },
]),
],
controllers: [ExampleController],
providers: [ExampleService],
})
export class ExampleModule {}
- MongooseModule.forFeature:在当前模块内注册 Mongoose 模型,这样在 Service 里才能
@InjectModel(Example.name)。 - controllers / providers:声明本模块的控制器和可注入服务。
5.2 控制器:example.controller.ts
控制器只做「接收请求、调 Service、返回结果」,不写具体业务和数据库逻辑(类似于java中的controller去调用Facade,其实就是一个分层,使逻辑更清晰):
@Controller('example')
export class ExampleController {
constructor(private readonly exampleService: ExampleService) {}
@Post()
create() {
return this.exampleService.create();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.exampleService.findOne(id);
}
@Patch(':id')
UpdateOne(@Param('id') id: string, @Body() exampleDto: ExampleDto) {
return this.exampleService.update(id, exampleDto);
}
@Delete(':id')
deleteOne(@Param('id') id: string) {
return this.exampleService.delete(id);
}
}
注意一下装饰器语法,常用的有:
@Controller('example'):路径前缀,得到/api/example/...。@Get() / @Post() / @Patch() / @Delete():HTTP 方法。@Query():查询参数;@Param():路径参数;@Body():请求体。- Service 通过构造函数注入,由 Nest 自动创建并注入实例。
5.3 服务:example.service.ts
业务逻辑和数据库访问都放在 Service 里,通过注入的 Model 调用 Mongoose API:
@Injectable()
export class ExampleService {
constructor(
@InjectModel(Example.name)
private readonly exampleModel,
) {}
async create() {
const example = new this.exampleModel({ title: 'test' + Date.now(), name: 'nest.js' });
return await example.save();
}
async findOne(id: string) {
return await this.exampleModel.findById(id);
}
async update(id: string, updateData) {
return await this.exampleModel.updateOne({ _id: id }, updateData);
}
async delete(id: string) {
return await this.exampleModel.findByIdAndDelete(id);
}
}
- @InjectModel(Example.name):注入 Mongoose Model,
findById、updateOne、findByIdAndDelete等都是 Model 自带方法。
5.4 文档模型:schemas/example.schemas.ts
用 Nest 的 @Schema 和 Mongoose 定义集合结构:
@Schema({ timestamps: true })
export class Example {
@Prop({ required: true })
title: string;
@Prop()
name: string;
}
export const ExampleSchema = SchemaFactory.createForClass(Example);
timestamps: true会自动加createdAt、updatedAt。ExampleSchema在example.module.ts里通过forFeature注册,对应 MongoDB 里的一个集合。
5.5 DTO:dto/example.dto.ts
DTO 描述「请求体或某段数据的形状」,便于类型约束和后续加校验(如 class-validator):
export class ExampleDto {
readonly title: string;
readonly name: string;
}
在 Controller 里用 @Body() exampleDto: ExampleDto 即可获得类型提示和统一结构。
六、全局拦截器与异常过滤器
6.1 统一成功响应:TansformInterceptor
所有正常返回都会先经过拦截器,被包装成:
{ "errno": 0, "data": { ... } }
实现方式:在 next.handle() 的 Observable 上 map 一层即可,这样前端可以统一根据 errno === 0 和 data 处理成功结果。
6.2 统一错误响应:HttpExceptionFilter
当控制器或 Service 抛出 HttpException(或 Nest 内置的 BadRequestException、NotFoundException 等)时,会被该过滤器捕获,返回例如:
{
"error": -1,
"message": "错误信息",
"timestamp": "2025-01-30T...",
"path": "/api/example/xxx"
}
同时可以保留正确的 HTTP 状态码(如 400、404)。这样成功走拦截器、失败走过滤器,前后端约定清晰。
七、环境变量与 MongoDB
根目录 .env 示例:
MONGO_HOST=127.0.0.1
MONGO_PORT=27017
MONGO_DATABASE=nestdb
PORT=3005
务必保证 ConfigModule.forRoot() 在 AppModule 的 imports 里排在最前,否则 process.env.MONGO_HOST 等可能未定义,导致连接失败。
八、小结与常用命令
| 概念 | 作用 |
|---|---|
| Module | 把 Controller、Service、Mongoose forFeature 等组织在一起 |
| Controller | 定义路由,使用 @Query / @Param / @Body,调用 Service |
| Service | 业务逻辑 + 注入 Model,做增删改查 |
| Schema | Mongoose 文档结构,对应一个集合 |
| DTO | 请求/响应数据结构,便于类型与校验 |
| Interceptor | 在响应返回前统一包装(如 errno + data) |
| ExceptionFilter | 捕获异常,统一错误返回格式 |
常用命令(与常规前端应用基本一致):
npm run start:dev # 开发模式,热重载
npm run build # 编译
npm run start:prod # 生产运行(需先 build)
按「入口 → 根模块 → 功能模块(Module/Controller/Service/Schema/DTO)→ 拦截器与过滤器」这条线把项目走一遍,再动手增删改几条示例接口,就能快速入门 NestJS啦。
后续也可以在此基础上加入校验(ValidationPipe)、鉴权(Guard)等,逐步完善项目。
如有错误,敬请指正~