Использование в GraphQL

В GraphQL-экосистеме валидация входных данных выполняет роль первого уровня защиты бизнес-логики, предотвращая попадание некорректных значений в резолверы и сервисный слой. В связке с class-validator наиболее часто используется подход, при котором входные схемы описываются через классы (DTO), а правила проверки навешиваются декораторами непосредственно на поля.

Основная модель работы в GraphQL

GraphQL не навязывает строгую валидацию на уровне спецификации: типизация ограничивается примитивами (String, Int, Boolean и т.д.) и структурой схемы. Поэтому сложные ограничения (диапазоны, формат строк, условные проверки) реализуются на уровне приложения.

При использовании class-validator типичная архитектура выглядит следующим образом:

  • GraphQL InputType описывает структуру входных данных
  • class-validator задаёт правила валидации
  • резолвер получает уже проверенный объект

Использование с TypeGraphQL

Наиболее естественная интеграция достигается через библиотеку type-graphql, где классы одновременно являются и GraphQL-схемой, и объектами валидации.

import { InputType, Field, Int } from "type-graphql";
import { Length, IsEmail, Min, Max } from "class-validator";

@InputType()
export class CreateUserInput {
  @Field()
  @Length(2, 30)
  name: string;

  @Field()
  @IsEmail()
  email: string;

  @Field(() => Int)
  @Min(18)
  @Max(120)
  age: number;
}

В этом случае один и тот же класс выполняет двойную роль: формирует GraphQL-схему и задаёт ограничения через декораторы class-validator.

Включение валидации в резолверах

Сам по себе GraphQL не запускает class-validator. Для этого требуется явный вызов валидации либо интеграционный слой.

В type-graphql валидация может включаться автоматически через validate: true при построении схемы:

import { buildSchema } from "type-graphql";

const schema = await buildSchema({
  resolvers: [UserResolver],
  validate: true
});

При таком подходе входные объекты проходят через class-validator до попадания в резолвер.

Ручная валидация через validate()

В более низкоуровневых интеграциях (например, Apollo Server без дополнительных обёрток) валидация выполняется вручную.

import { validate } from "class-validator";
import { plainToInstance } from "class-transformer";

async function createUser(_, args) {
  const input = plainToInstance(CreateUserInput, args.input);

  const errors = await validate(input);

  if (errors.length > 0) {
    throw new Error("Validation failed");
  }

  return userService.create(input);
}

Ключевой момент заключается в преобразовании plain-объекта в экземпляр класса через class-transformer. Без этого декораторы class-validator не будут корректно обработаны.

Валидация вложенных объектов

GraphQL часто использует сложные входные структуры. class-validator поддерживает вложенную проверку через @ValidateNested и @Type.

import { InputType, Field } from "type-graphql";
import { ValidateNested, IsString } from "class-validator";
import { Type } from "class-transformer";

class ProfileInput {
  @IsString()
  bio: string;
}

@InputType()
class CreateUserInput {
  @Field()
  name: string;

  @Field(() => ProfileInput)
  @ValidateNested()
  @Type(() => ProfileInput)
  profile: ProfileInput;
}

Без @Type вложенная структура останется обычным объектом, и валидация не будет применена корректно.

Работа с массивами

GraphQL часто передаёт списки сущностей, что требует специальной настройки валидации элементов массива.

import { IsArray, ValidateNested } from "class-validator";
import { Type } from "class-transformer";

class RoleInput {
  @IsString()
  name: string;
}

@InputType()
class CreateUserInput {
  @Field(() => [RoleInput])
  @IsArray()
  @ValidateNested({ each: true })
  @Type(() => RoleInput)
  roles: RoleInput[];
}

Флаг each: true обеспечивает проверку каждого элемента массива отдельно.

Кастомные валидаторы в GraphQL DTO

В GraphQL-сценариях часто требуется логика, не укладывающаяся в стандартные декораторы. Для этого используются пользовательские валидаторы.

import { registerDecorator, ValidationOptions } from "class-validator";

function IsEven(validationOptions?: ValidationOptions) {
  return function (object: Object, propertyName: string) {
    registerDecorator({
      name: "isEven",
      target: object.constructor,
      propertyName,
      options: validationOptions,
      validator: {
        validate(value: number) {
          return typeof value === "number" && value % 2 === 0;
        }
      }
    });
  };
}

Использование в GraphQL InputType:

@InputType()
class TestInput {
  @Field()
  @IsEven({ message: "Число должно быть чётным" })
  value: number;
}

Валидация в NestJS GraphQL

В NestJS GraphQL интеграция чаще всего реализуется через глобальные пайпы.

import { ValidationPipe } from "@nestjs/common";

app.useGlobalPipes(
  new ValidationPipe({
    transform: true,
    whitelist: true,
    forbidNonWhitelisted: true
  })
);

GraphQL resolver:

@Resolver()
export class UserResolver {
  @Mutation(() => User)
  createUser(@Args("input") input: CreateUserInput) {
    return this.userService.create(input);
  }
}

В этом случае ValidationPipe автоматически применяет class-validator к входным DTO.

Ограничение лишних полей

В GraphQL часто возникает проблема «лишних» полей, которые не описаны в схеме, но приходят от клиента. class-validator решает это через комбинацию с class-transformer.

import { Expose, Exclude } from "class-transformer";

@InputType()
class CreateUserInput {
  @Field()
  @Expose()
  name: string;

  @Exclude()
  internalFlag: boolean;
}

При включённом whitelist лишние поля автоматически удаляются, что снижает риск загрязнения бизнес-логики.

Асинхронная валидация в GraphQL

Некоторые проверки требуют обращения к базе данных: проверка уникальности email, существования сущности и т.д.

import { ValidatorConstraint, ValidatorConstraintInterface } from "class-validator";

@ValidatorConstraint({ async: true })
class IsEmailAlreadyUsed implements ValidatorConstraintInterface {
  async validate(email: string) {
    const user = await userRepository.findOne({ email });
    return !user;
  }
}

Использование:

@Validate(IsEmailAlreadyUsed)
@IsEmail()
email: string;

Асинхронная валидация выполняется до входа в резолвер, что предотвращает лишние вызовы бизнес-логики.

Обработка ошибок в GraphQL контексте

Ошибки class-validator преобразуются в массив ValidationError, который может быть трансформирован в формат GraphQL-ошибок.

if (errors.length > 0) {
  const formatted = errors.map(e => ({
    field: e.property,
    constraints: e.constraints
  }));

  throw new Error(JSON.stringify(formatted));
}

В более зрелых архитектурах ошибки преобразуются в ApolloError с расширенной структурой extensions.

Особенности проектирования DTO для GraphQL

GraphQL-схемы требуют явного описания всех полей, поэтому DTO становятся центральным элементом архитектуры. При использовании class-validator важно учитывать:

  • классы должны быть изолированы от бизнес-логики
  • декораторы не должны зависеть от окружения выполнения
  • вложенные структуры требуют явного указания @Type
  • массивы требуют each: true для полной проверки

Такой подход позволяет сохранять согласованность между схемой GraphQL и правилами валидации без дублирования логики в резолверах