在NestJS中创建控制器时如何正确运用DRY原则?
在NestJS中创建控制器时如何正确运用DRY原则?
看了你写的BaseController基础控制器,已经精准踩中了*DRY(Don't Repeat Yourself)*原则的核心思路——复用通用逻辑!下面就来聊聊在NestJS里怎么把这个原则用得更彻底:
1. 把通用CRUD逻辑完全抽象到基础控制器
你的基础控制器已经搭了架子,我们可以把常见的列表查询、单条查询、创建、更新、删除逻辑都封装进去,让业务控制器直接继承就能用:
import { Get, Post, Put, Delete, HttpStatus, Request, Response, Body, Param, Query } from '@nestjs/common'; import { MessageCodeError } from './../index'; // 用泛型<T>来支持不同的实体类型 export class BaseController<T> { constructor(public dataService: any) {} // 带分页的列表查询 @Get('/') public async findAndCountAll(@Query() query, @Response() res) { const offset = Number(query.skip) || 0; const limit = Number(query.take) || 10; const where = query.where ? JSON.parse(query.where) : {}; try { const [data, total] = await this.dataService.findAndCount({ where, offset, limit, }); return res.status(HttpStatus.OK).json({ data, total }); } catch (error) { throw new MessageCodeError(error.message); } } // 根据ID查询单条数据 @Get('/:id') public async findOne(@Param('id') id: string, @Response() res) { try { const data = await this.dataService.findOne({ where: { id } }); if (!data) { return res.status(HttpStatus.NOT_FOUND).json({ message: '数据不存在' }); } return res.status(HttpStatus.OK).json(data); } catch (error) { throw new MessageCodeError(error.message); } } // 创建数据 @Post('/') public async create(@Body() body: T, @Response() res) { try { const data = await this.dataService.create(body); return res.status(HttpStatus.CREATED).json(data); } catch (error) { throw new MessageCodeError(error.message); } } // 更新数据 @Put('/:id') public async update(@Param('id') id: string, @Body() body: Partial<T>, @Response() res) { try { const [affectedCount] = await this.dataService.update(id, body); if (affectedCount === 0) { return res.status(HttpStatus.NOT_FOUND).json({ message: '数据不存在或未修改' }); } const updatedData = await this.dataService.findOne({ where: { id } }); return res.status(HttpStatus.OK).json(updatedData); } catch (error) { throw new MessageCodeError(error.message); } } // 删除数据 @Delete('/:id') public async delete(@Param('id') id: string, @Response() res) { try { const affectedCount = await this.dataService.delete(id); if (affectedCount === 0) { return res.status(HttpStatus.NOT_FOUND).json({ message: '数据不存在' }); } return res.status(HttpStatus.NO_CONTENT).send(); } catch (error) { throw new MessageCodeError(error.message); } } }
业务控制器只需要继承它,再注入对应的服务就行,还能添加自己的专属接口:
import { Controller, Post, Body } from '@nestjs/common'; import { BaseController } from './base.controller'; import { UserService } from './user.service'; import { User } from './user.entity'; @Controller('users') export class UserController extends BaseController<User> { constructor(private readonly userService: UserService) { // 把业务服务传给基础控制器 super(userService); } // 用户模块特有的登录接口 @Post('/login') async login(@Body() loginDto: { username: string; password: string }) { return this.userService.validateUser(loginDto.username, loginDto.password); } }
2. 用自定义装饰器减少重复的参数处理
如果多个控制器都需要获取当前登录用户、解析特定请求头这类操作,别在每个接口里重复写,自定义装饰器来搞定:
比如获取当前用户的装饰器:
import { createParamDecorator, ExecutionContext } from '@nestjs/common'; export const GetCurrentUser = createParamDecorator( (data: unknown, ctx: ExecutionContext) => { const request = ctx.switchToHttp().getRequest(); // 假设用户信息已经通过AuthGuard挂载到request.user上 return request.user; }, );
之后在任意控制器里直接用,不用再手动从request里取:
@Get('/profile') async getProfile(@GetCurrentUser() user: User) { return this.userService.findOne({ where: { id: user.id } }); }
3. 用管道统一处理参数校验和转换
像分页参数的类型转换、范围校验,别在每个接口里重复写Number(query.skip),写个通用管道一次性解决:
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common'; @Injectable() export class PaginationPipe implements PipeTransform { transform(value: any) { const skip = value.skip ? parseInt(value.skip, 10) : 0; const take = value.take ? parseInt(value.take, 10) : 10; if (isNaN(skip) || skip < 0) { throw new BadRequestException('skip参数必须是非负整数'); } if (isNaN(take) || take < 1 || take > 100) { throw new BadRequestException('take参数必须是1-100之间的整数'); } return { skip, take }; } }
然后在基础控制器的列表接口里使用:
@Get('/') public async findAndCountAll( @Query(new PaginationPipe()) pagination: { skip: number; take: number }, @Query('where') where: string, @Response() res ) { const whereCondition = where ? JSON.parse(where) : {}; try { const [data, total] = await this.dataService.findAndCount({ where: whereCondition, ...pagination, }); return res.status(HttpStatus.OK).json({ data, total }); } catch (error) { throw new MessageCodeError(error.message); } }
4. 抽象统一的响应格式
避免每个接口都重复写res.status(...).json(...),在基础控制器里封装通用的响应方法:
// 放在BaseController类里 protected successResponse(res, data, statusCode = HttpStatus.OK) { return res.status(statusCode).json({ success: true, data, timestamp: new Date().toISOString(), }); } protected errorResponse(res, message, statusCode = HttpStatus.BAD_REQUEST) { return res.status(statusCode).json({ success: false, message, timestamp: new Date().toISOString(), }); }
之后接口里直接调用,代码更简洁:
@Get('/:id') public async findOne(@Param('id') id: string, @Response() res) { try { const data = await this.dataService.findOne({ where: { id } }); if (!data) { return this.errorResponse(res, '数据不存在', HttpStatus.NOT_FOUND); } return this.successResponse(res, data); } catch (error) { throw new MessageCodeError(error.message); } }
内容的提问来源于stack exchange,提问作者Vladimir Denisov
相关产品推荐
相关产品推荐

