Пример UsersController
Возьмём пример из feeldown-backend
На первый взгляд — аккуратный, лаконичный, с декораторами и чёткими методами. Но давай копнём глубже.
Что хорошо
Чёткое разделение ответственности
Контроллер занимается только тем, для чего он создан — маршрутизацией.
Вся бизнес-логика вынесена в UsersService. Это идеальное попадание в SRP.
public constructor(private readonly service: Service) {}Правильное использование декораторов
@Public()— для публичных маршрутов.@OnlyMe()— для проверки владения ресурсом (авторизация).@UseGuards(AuthGuard)— аутентификация на уровне всего контроллера.
Это гибкая система, которую легко комбинировать.
Типизация
Использование ResolvedUsernameSlug и ServerUser — типы явно продуманы и помогают избежать ошибок.
Единообразие методов
Все методы (put, patch, delete) используют одинаковый подход:
получают slug, преобразуют его в where через SlugPipe.resolveMe(),
и передают в сервис. Это уменьшает когнитивную нагрузку.
Что плохо
SlugPipe в контроллере
Часто вызывается SlugPipe.resolveMe(slug, me), однако контроллер не должен знать о SlugPipe,
эту задачу лучше всего переложить на ещё один декоратор, чтобы избежать изменения
этого вызова и от дублирования кода.
Улучшенный код
users.controller.ts
import type { ResolvedUsernameSlug } from "@1/types";
import type { ServerUser } from "@1/types/server.types";
import {
Controller,
Injectable,
Get,
Param,
Body,
Delete,
UseGuards,
} from "@nestjs/common";
import { ApiOperation } from "@nestjs/swagger";
import { Public, Me, OnlyMe, Update } from "@/decorators";
import { Parameters } from "@1/enums";
import { SlugPipe } from "@1/pipes";
import { AuthGuard } from "@1/guards";
import { UserUpdateDto } from "./dto";
import { ROUTE, ROUTES, OPERATIONS } from "./users.routes";
import { UsersService as Service } from "./users.service";
@Injectable()
@Controller(ROUTE)
@UseGuards(AuthGuard)
export class UsersController {
public constructor(private readonly service: Service) {}
@ApiOperation(OPERATIONS.GET_ONE)
@Get(ROUTES.GET_ONE)
@Public()
public getOne(
@UserFindOptions(Parameters.slug)
options: UserFindOptions,
) {
return this.service.getOne(options);
}
@ApiOperation(OPERATIONS.PUT)
@ApiOperation(OPERATIONS.PATCH)
@Update([ROUTES.PUT, ROUTES.PATCH])
public update(
@UserFindOptions(Parameters.slug)
options: UserFindOptions,
@Body() data: UserUpdateDto,
) {
return this.service.update(options, data);
}
@ApiOperation(OPERATIONS.DELETE)
@Delete(ROUTES.DELETE)
@OnlyMe(Parameters.id, "id")
public delete(@Param(Parameters.id) id: string) {
return this.service.delete(id);
}
}
export default UsersController;Пример работы UserFindOptions:
user-find-options.decorator.ts
import type { Request } from "express";
import type { ExecutionContext } from "@nestjs/common";
import type { PrismaService } from "@/database";
import { createParamDecorator, HttpException, HttpStatus} from "@nestjs/common";
import { SlugPipe } from "@1/pipes";
import { ServerUserService } from "@1/services";
import { prisma } from "@/database";
export const UserFindOptions = createParamDecorator<Properties>((
parameter: string;
context: ExecutionContext
) => {
const request = context.switchToHttp().getRequest<Request>();
const value = request.params[parameter] as string;
if (!value) {
throw new HttpException("Parameter is not defined", HttpStatus.BAD_REQUEST);
}
const pipe = new SlugPipe("username"); // Нарушение DIP
const slug = pipe.transform(value);
const serverUserService = new ServerUserService(prisma as PrismaService); // Нарушение DIP
const me = serverUserService.getByRequest(request);
const findOptions = SlugPipe.resolveMe(slug, me.user.id);
return findOptions;
});Примечание: здесь использовано упрощение для наглядности; в реальном проекте применяйте DI.
Проблема, которую мы решали
В исходном UsersController было несколько моментов, которые нарушали SRP и создавали дублирование:
@Put(ROUTES.PUT)
@OnlyMe(Parameters.slug, "slug")
public put(
@Param(Parameters.slug, new SlugPipe("username")) slug: ResolvedUsernameSlug,
@Body() data: UserUpdateDto,
@Me() me: ServerUser,
) {
const where = SlugPipe.resolveMe(slug, me); // вот это повторяется
return this.service.put(where, data);
}Что было не так:
- Контроллер знал о
SlugPipeи его методеresolveMe. - Логика трансформации повторялась в трёх методах.
- При изменении логики поиска пользователя пришлось бы править все методы.
- Контроллер делал больше, чем должен.
Что мы сделали
Создали декоратор @UserFindOptions
Вместо того чтобы вручную вызывать SlugPipe.resolveMe() в каждом методе, мы создали параметр-декоратор:
@UserFindOptions(Parameters.slug)
options: UserFindOptionsДекоратор берёт параметры запроса, получает текущего пользователя и строит готовый объект findOptions.
Контроллер перестал знать о трансформации
Теперь каждый метод просто принимает готовый options и передаёт его в сервис:
@Put(ROUTES.PUT)
@OnlyMe(Parameters.slug, "slug")
public put(
@UserFindOptions() options: UserFindOptions,
@Body() data: UserUpdateDto,
) {
return this.service.update(options, data);
}Убрали дублирование
Раньше resolveMe вызывался в трёх местах. Теперь — только в декораторе.
Объединили PUT и PATCH
С помощью кастомного декоратора @Update мы объединили два метода в один:
@Update([ROUTES.PUT, ROUTES.PATCH])
public update(
@UserFindOptions() options: UserFindOptions,
@Body() data: UserUpdateDto,
) {
return this.service.update(options, data);
}