NestJS та GraphQL у 2026: Схеми, Резолвери та Питання на Співбесіді

Повний посібник з інтеграції NestJS GraphQL: підходи schema-first та code-first, резолвери, DataLoader та питання на технічних співбесідах 2026 року.

NestJS та GraphQL у 2026: Схеми, Резолвери та Питання на Співбесіді

Інтеграція NestJS з GraphQL пропонує структурований підхід до створення типобезпечних API з використанням декораторів TypeScript та мови запитів GraphQL. Цей посібник охоплює підходи schema-first та code-first, патерни резолверів, а також питання, які задають менеджери з найму на співбесідах у 2026 році.

Code-First vs Schema-First

NestJS 11 за замовчуванням використовує code-first GraphQL, генеруючи схему з класів TypeScript. Підхід schema-first залишається доступним для команд з існуючими файлами .graphql або робочими процесами на основі SDL.

Налаштування NestJS GraphQL з Apollo Server

NestJS інтегрується з Apollo Server через пакет @nestjs/graphql. Налаштування відрізняється залежно від обраного підходу — code-first генерує SDL з декораторів, тоді як schema-first парсить файли .graphql безпосередньо.

app.module.tstypescript
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { join } from 'path';

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: join(process.cwd(), 'src/schema.gql'), // Code-first: генерує схему
      sortSchema: true, // Алфавітне сортування для читабельності
      playground: process.env.NODE_ENV !== 'production', // Вимкнути в продакшені
      introspection: process.env.NODE_ENV !== 'production',
    }),
  ],
})
export class AppModule {}

Опція autoSchemaFile вмикає режим code-first. Встановлення значення true генерує схему в пам'яті без запису на диск — корисно для serverless-розгортань, де доступ до файлової системи може бути обмежений.

Визначення Типів GraphQL з Декораторами Code-First

Підхід code-first визначає типи GraphQL за допомогою класів TypeScript, декорованих @ObjectType(). Кожне поле використовує @Field() для вказівки типу GraphQL та можливості null.

user.entity.tstypescript
import { ObjectType, Field, ID, Int } from '@nestjs/graphql';

@ObjectType({ description: 'Користувач додатку' }) // Опис з'являється в документації схеми
export class User {
  @Field(() => ID) // Мапиться на скаляр GraphQL ID
  id: string;

  @Field()
  email: string;

  @Field({ nullable: true }) // Опціональне поле в GraphQL
  displayName?: string;

  @Field(() => Int, { defaultValue: 0 })
  postCount: number;

  @Field(() => [Post], { nullable: 'itemsAndList' }) // Як список, так і елементи можуть бути null
  posts?: Post[];

  // Поля без @Field() виключаються зі схеми GraphQL
  passwordHash: string;
}

Опція nullable приймає три значення: true (поле опціональне), 'items' (елементи списку можуть бути null) та 'itemsAndList' (як список, так і елементи можуть бути null). Ця деталізація точно відповідає семантиці null у GraphQL.

Створення Резолверів для Запитів та Мутацій

Резолвери обробляють вхідні операції GraphQL. NestJS використовує @Resolver() для позначення класу як резолвера, з декораторами методів, що вказують типи операцій.

users.resolver.tstypescript
import { Resolver, Query, Mutation, Args, ID } from '@nestjs/graphql';
import { User } from './user.entity';
import { UsersService } from './users.service';
import { CreateUserInput } from './dto/create-user.input';

@Resolver(() => User) // Прив'язує резолвер до типу User для розв'язання полів
export class UsersResolver {
  constructor(private readonly usersService: UsersService) {}

  @Query(() => [User], { name: 'users' }) // Явна назва запиту
  findAll(): Promise<User[]> {
    return this.usersService.findAll();
  }

  @Query(() => User, { nullable: true })
  user(@Args('id', { type: () => ID }) id: string): Promise<User | null> {
    return this.usersService.findOne(id);
  }

  @Mutation(() => User)
  createUser(@Args('input') input: CreateUserInput): Promise<User> {
    return this.usersService.create(input);
  }
}

Декоратор @Resolver(() => User) встановлює контекст для декораторів @ResolveField(), уможливлюючи розв'язання на рівні полів для обчислюваних або пов'язаних даних.

Input-Типи та Валідація з class-validator

Input-типи GraphQL визначають payload мутацій. Поєднання @InputType() з декораторами class-validator дозволяє валідацію на рівні схеми та під час виконання.

dto/create-user.input.tstypescript
import { InputType, Field } from '@nestjs/graphql';
import { IsEmail, MinLength, IsOptional, Matches } from 'class-validator';

@InputType()
export class CreateUserInput {
  @Field()
  @IsEmail({}, { message: 'Невірний формат email' })
  email: string;

  @Field()
  @MinLength(8, { message: 'Пароль має містити щонайменше 8 символів' })
  @Matches(/[A-Z]/, { message: 'Пароль має містити велику літеру' })
  password: string;

  @Field({ nullable: true })
  @IsOptional()
  @MinLength(2)
  displayName?: string;
}

Увімкніть валідацію глобально, додавши ValidationPipe у файл main.ts. Помилки GraphQL включають повідомлення валідації в полі extensions, зберігаючи сумісність з клієнтами.

Готовий до співбесід з Node.js / NestJS?

Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.

Розв'язання Пов'язаних Даних з @ResolveField та DataLoader

Резолвери полів обробляють зв'язки між типами. Без оптимізації, отримання списку користувачів з їхніми постами викликає N+1 запитів — один для користувачів, потім по одному для постів кожного користувача.

users.resolver.tstypescript
import { Resolver, ResolveField, Parent } from '@nestjs/graphql';
import { User } from './user.entity';
import { Post } from '../posts/post.entity';
import { PostsLoader } from '../posts/posts.loader';

@Resolver(() => User)
export class UsersResolver {
  constructor(private readonly postsLoader: PostsLoader) {}

  @ResolveField(() => [Post])
  async posts(@Parent() user: User): Promise<Post[]> {
    // DataLoader групує запити: один запит для всіх ID користувачів
    return this.postsLoader.batchByUserId.load(user.id);
  }

  @ResolveField(() => Int)
  async postCount(@Parent() user: User): Promise<number> {
    const posts = await this.postsLoader.batchByUserId.load(user.id);
    return posts.length;
  }
}

DataLoader групує та кешує запити в межах однієї операції GraphQL. У NestJS DataLoader має бути обмежений до області запиту за допомогою @Injectable({ scope: Scope.REQUEST }).

posts/posts.loader.tstypescript
import { Injectable, Scope } from '@nestjs/common';
import * as DataLoader from 'dataloader';
import { PostsService } from './posts.service';
import { Post } from './post.entity';

@Injectable({ scope: Scope.REQUEST }) // Новий екземпляр для кожного запиту
export class PostsLoader {
  constructor(private readonly postsService: PostsService) {}

  public readonly batchByUserId = new DataLoader<string, Post[]>(
    async (userIds: readonly string[]) => {
      // Один запит: SELECT * FROM posts WHERE user_id IN (...)
      const posts = await this.postsService.findByUserIds([...userIds]);
      // Мапування результатів до вхідного порядку
      const postsMap = new Map<string, Post[]>();
      posts.forEach(post => {
        const existing = postsMap.get(post.userId) || [];
        postsMap.set(post.userId, [...existing, post]);
      });
      return userIds.map(id => postsMap.get(id) || []);
    }
  );
}

DataLoader скорочує N+1 запитів до одного пакетного запиту — критично важливо для API GraphQL, де клієнти контролюють глибину запитів. Більше інформації про архітектурні патерни NestJS можна знайти в модулі питання співбесіди про модулі та Dependency Injection в NestJS.

Підписки для Даних у Реальному Часі

Підписки GraphQL передають дані клієнтам через з'єднання WebSocket. NestJS використовує бібліотеку graphql-ws, яка реалізує протокол GraphQL over WebSocket.

app.module.ts - Увімкнення підписокtypescript
GraphQLModule.forRoot<ApolloDriverConfig>({
  driver: ApolloDriver,
  autoSchemaFile: true,
  subscriptions: {
    'graphql-ws': true, // Сучасний протокол
    'subscriptions-transport-ws': false, // Застарілий legacy-протокол
  },
}),
posts.resolver.tstypescript
import { Resolver, Subscription } from '@nestjs/graphql';
import { PubSub } from 'graphql-subscriptions';
import { Post } from './post.entity';

const pubSub = new PubSub(); // Використовуйте Redis PubSub для багатоекземплярних розгортань

@Resolver(() => Post)
export class PostsResolver {
  @Subscription(() => Post, {
    filter: (payload, variables) => 
      payload.postCreated.userId === variables.userId, // Фільтрація на стороні клієнта
  })
  postCreated() {
    return pubSub.asyncIterableIterator('postCreated');
  }

  @Mutation(() => Post)
  async createPost(@Args('input') input: CreatePostInput): Promise<Post> {
    const post = await this.postsService.create(input);
    pubSub.publish('postCreated', { postCreated: post }); // Викликати підписку
    return post;
  }
}

Для продакшн-розгортань на кількох екземплярах in-memory PubSub слід замінити на graphql-redis-subscriptions для трансляції подій по всьому кластеру.

Автентифікація та Авторизація в GraphQL

Гарди NestJS безперешкодно працюють з резолверами GraphQL. Контекст виконання відрізняється від REST — використовуйте GqlExecutionContext для отримання запиту.

guards/gql-auth.guard.tstypescript
import { Injectable, ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class GqlAuthGuard extends AuthGuard('jwt') {
  getRequest(context: ExecutionContext) {
    const ctx = GqlExecutionContext.create(context);
    return ctx.getContext().req; // Отримати запит з контексту GraphQL
  }
}

Гарди застосовуються на рівні резолвера або методу. Авторизація на рівні полів використовує @ResolveField() з умовною логікою на основі ролей користувача.

users.resolver.tstypescript
@UseGuards(GqlAuthGuard)
@Resolver(() => User)
export class UsersResolver {
  @Query(() => User)
  me(@CurrentUser() user: User): User {
    return user; // Повернути автентифікованого користувача
  }

  @ResolveField(() => String, { nullable: true })
  email(@Parent() user: User, @CurrentUser() currentUser: User): string | null {
    // Повертати email лише при перегляді власного профілю або якщо admin
    if (user.id === currentUser.id || currentUser.role === 'ADMIN') {
      return user.email;
    }
    return null;
  }
}

Для складної авторизації варто розглянути GraphQL Shield або вбудовану інтеграцію CASL в NestJS. Стаття Гарди та Інтерцептори в NestJS детально розглядає ці патерни.

Поширені Питання на Співбесіді NestJS GraphQL

Технічні співбесіди на позиції NestJS часто включають питання, специфічні для GraphQL. Ось патерни, які оцінюють менеджери з найму.

П: Як NestJS справляється з проблемою N+1 у GraphQL?

DataLoader групує виклики резолверів полів у межах одного запиту. Коли кілька батьківських об'єктів запитують одне поле, DataLoader збирає всі ключі, виконує один пакетний запит і розподіляє результати. Loader має бути обмежений до області запиту, щоб запобігти проблемам з кешуванням між запитами.

П: Яка різниця між code-first та schema-first у NestJS GraphQL?

Code-first генерує схему GraphQL з декораторів TypeScript під час виконання, автоматично синхронізуючи типи та схему. Schema-first парсить SDL-файли .graphql, вимагаючи ручних визначень типів. Code-first підходить командам, які нативно працюють з TypeScript; schema-first краще працює, коли схема є контрактом між frontend та backend командами.

П: Як реалізувати дозволи на рівні полів?

Існує три підходи: (1) @ResolveField() з умовними поверненнями на основі контексту користувача, (2) Власні декоратори, що перевіряють дозволи перед розв'язанням поля, (3) Директиви схеми на кшталт @auth(requires: ADMIN), що обробляються трансформером директив. Перший підхід пропонує найбільшу гнучкість; директиви забезпечують найчистішу документацію схеми.

П: Поясніть контекст GraphQL у NestJS.

Об'єкт контексту проходить через усі резолвери в межах запиту. NestJS за замовчуванням заповнює його HTTP-запитом. Власний контекст налаштовується в GraphQLModule.forRoot() через опцію context — корисно для додавання екземплярів DataLoader, автентифікованого користувача або з'єднань з базою даних.

П: Як підписки масштабуються на кількох екземплярах сервера?

In-memory PubSub працює лише для одноекземплярних розгортань. Багатоекземплярні архітектури вимагають зовнішнього брокера — Redis PubSub є стандартом. Кожен екземпляр сервера підписується на канали Redis; коли будь-який екземпляр публікує подію, всі екземпляри її отримують і передають підключеним WebSocket-клієнтам.

Для додаткової підготовки до співбесід з NestJS дивіться модуль Middleware та Інтерцептори, що охоплює патерни життєвого циклу запиту.

Висновок

  • NestJS GraphQL підтримує обидва підходи: code-first та schema-first — code-first спрощує проекти TypeScript, schema-first підходить для робочих процесів на основі SDL
  • DataLoader усуває N+1 запити шляхом групування запитів резолверів полів у межах однієї операції
  • Екземпляри DataLoader, обмежені до області запиту, запобігають забрудненню кешу між одночасними запитами
  • GqlExecutionContext з'єднує Гарди NestJS з контекстом резолверів GraphQL
  • Продакшн-підписки вимагають Redis PubSub для розподілу подій між кількома екземплярами
  • Авторизація на рівні полів поєднує @ResolveField() з перевірками контексту користувача

Починай практикувати!

Перевір свої знання з нашими симуляторами співбесід та технічними тестами.

Теги

#nestjs
#graphql
#node.js
#typescript
#api

Поділитися

Пов'язані статті