You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.20 12:35:45