NestJS i GraphQL w 2026: Schematy, Resolvery i Pytania Rekrutacyjne

Kompletny poradnik NestJS GraphQL omawiający podejścia schema-first i code-first, resolvery, DataLoader oraz pytania rekrutacyjne w 2026 roku.

NestJS i GraphQL w 2026: Schematy, Resolvery i Pytania Rekrutacyjne

Integracja NestJS z GraphQL oferuje strukturalne podejście do tworzenia bezpiecznych typowo API, wykorzystujących dekoratory TypeScript i język zapytań GraphQL. Ten poradnik omawia podejścia schema-first i code-first, wzorce resolverów oraz pytania rekrutacyjne zadawane przez menedżerów podczas rozmów kwalifikacyjnych w 2026 roku.

Code-First vs Schema-First

NestJS 11 domyślnie używa GraphQL code-first, generując schemat z klas TypeScript. Podejście schema-first pozostaje dostępne dla zespołów posiadających istniejące pliki .graphql lub pracujących z SDL.

Konfiguracja NestJS GraphQL z Apollo Server

NestJS integruje się z Apollo Server poprzez pakiet @nestjs/graphql. Konfiguracja różni się w zależności od wybranego podejścia — code-first generuje SDL z dekoratorów, podczas gdy schema-first parsuje pliki .graphql bezpośrednio.

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: generuje schemat
      sortSchema: true, // Alfabetyczne sortowanie dla czytelności
      playground: process.env.NODE_ENV !== 'production', // Wyłącz w produkcji
      introspection: process.env.NODE_ENV !== 'production',
    }),
  ],
})
export class AppModule {}

Opcja autoSchemaFile włącza tryb code-first. Ustawienie wartości na true generuje schemat w pamięci bez zapisu na dysk — przydatne dla wdrożeń serverless, gdzie dostęp do systemu plików może być ograniczony.

Definiowanie Typów GraphQL z Dekoratorami Code-First

Podejście code-first definiuje typy GraphQL przy użyciu klas TypeScript ozdobionych dekoratorem @ObjectType(). Każde pole używa @Field() do określenia typu GraphQL i dopuszczalności wartości null.

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

@ObjectType({ description: 'Użytkownik aplikacji' }) // Opis pojawia się w dokumentacji schematu
export class User {
  @Field(() => ID) // Mapuje do skalaru GraphQL ID
  id: string;

  @Field()
  email: string;

  @Field({ nullable: true }) // Opcjonalne pole w GraphQL
  displayName?: string;

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

  @Field(() => [Post], { nullable: 'itemsAndList' }) // Zarówno lista, jak i elementy mogą być null
  posts?: Post[];

  // Pola bez @Field() są wykluczone ze schematu GraphQL
  passwordHash: string;
}

Opcja nullable przyjmuje trzy wartości: true (pole opcjonalne), 'items' (elementy listy mogą być null) oraz 'itemsAndList' (zarówno lista, jak i elementy mogą być null). Ta granularność dokładnie odpowiada semantyce null w GraphQL.

Tworzenie Resolverów dla Zapytań i Mutacji

Resolvery obsługują przychodzące operacje GraphQL. NestJS używa @Resolver() do oznaczenia klasy jako resolvera, z dekoratorami metod określającymi typy operacji.

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) // Wiąże resolver z typem User do rozwiązywania pól
export class UsersResolver {
  constructor(private readonly usersService: UsersService) {}

  @Query(() => [User], { name: 'users' }) // Jawna nazwa zapytania
  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);
  }
}

Dekorator @Resolver(() => User) ustala kontekst dla dekoratorów @ResolveField(), umożliwiając rozwiązywanie na poziomie pól dla danych obliczanych lub powiązanych.

Typy Input i Walidacja z class-validator

Typy input GraphQL definiują payload mutacji. Połączenie @InputType() z dekoratorami class-validator umożliwia walidację na poziomie schematu i w czasie wykonania.

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: 'Nieprawidłowy format email' })
  email: string;

  @Field()
  @MinLength(8, { message: 'Hasło musi mieć co najmniej 8 znaków' })
  @Matches(/[A-Z]/, { message: 'Hasło musi zawierać wielką literę' })
  password: string;

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

Walidację należy włączyć globalnie, dodając ValidationPipe w pliku main.ts. Błędy GraphQL zawierają komunikaty walidacji w polu extensions, zachowując kompatybilność z klientami.

Gotowy na rozmowy o Node.js / NestJS?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Rozwiązywanie Powiązanych Danych z @ResolveField i DataLoader

Resolvery pól obsługują relacje między typami. Bez optymalizacji, pobieranie listy użytkowników wraz z ich postami wywołuje zapytania N+1 — jedno dla użytkowników, a następnie jedno na każdego użytkownika dla postów.

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 grupuje żądania: jedno zapytanie dla wszystkich ID użytkowników
    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 grupuje i cachuje żądania w ramach pojedynczej operacji GraphQL. W NestJS DataLoader powinien być ograniczony do zakresu żądania przy użyciu @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 }) // Nowa instancja dla każdego żądania
export class PostsLoader {
  constructor(private readonly postsService: PostsService) {}

  public readonly batchByUserId = new DataLoader<string, Post[]>(
    async (userIds: readonly string[]) => {
      // Pojedyncze zapytanie: SELECT * FROM posts WHERE user_id IN (...)
      const posts = await this.postsService.findByUserIds([...userIds]);
      // Mapuj wyniki z powrotem do kolejności wejściowej
      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 redukuje zapytania N+1 do pojedynczego zgrupowanego zapytania — kluczowe dla API GraphQL, gdzie klienci kontrolują głębokość zapytań. Więcej informacji o wzorcach architektonicznych NestJS można znaleźć w module pytania rekrutacyjne o moduły i Dependency Injection w NestJS.

Subskrypcje dla Danych w Czasie Rzeczywistym

Subskrypcje GraphQL przesyłają dane do klientów przez połączenia WebSocket. NestJS wykorzystuje bibliotekę graphql-ws, która implementuje protokół GraphQL over WebSocket.

app.module.ts - Włączenie subskrypcjitypescript
GraphQLModule.forRoot<ApolloDriverConfig>({
  driver: ApolloDriver,
  autoSchemaFile: true,
  subscriptions: {
    'graphql-ws': true, // Nowoczesny protokół
    'subscriptions-transport-ws': false, // Przestarzały protokół legacy
  },
}),
posts.resolver.tstypescript
import { Resolver, Subscription } from '@nestjs/graphql';
import { PubSub } from 'graphql-subscriptions';
import { Post } from './post.entity';

const pubSub = new PubSub(); // Użyj Redis PubSub dla wdrożeń wieloinstancyjnych

@Resolver(() => Post)
export class PostsResolver {
  @Subscription(() => Post, {
    filter: (payload, variables) => 
      payload.postCreated.userId === variables.userId, // Filtrowanie po stronie klienta
  })
  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 }); // Wywołaj subskrypcję
    return post;
  }
}

Dla wdrożeń produkcyjnych na wielu instancjach, pamięciowy PubSub należy zastąpić biblioteką graphql-redis-subscriptions, aby transmitować zdarzenia w całym klastrze.

Uwierzytelnianie i Autoryzacja w GraphQL

Guardy NestJS współpracują bezproblemowo z resolverami GraphQL. Kontekst wykonania różni się od REST — należy użyć GqlExecutionContext do wyodrębnienia żądania.

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; // Wyodrębnij żądanie z kontekstu GraphQL
  }
}

Guardy stosuje się na poziomie resolvera lub metody. Autoryzacja na poziomie pól wykorzystuje @ResolveField() z logiką warunkową opartą na rolach użytkownika.

users.resolver.tstypescript
@UseGuards(GqlAuthGuard)
@Resolver(() => User)
export class UsersResolver {
  @Query(() => User)
  me(@CurrentUser() user: User): User {
    return user; // Zwróć uwierzytelnionego użytkownika
  }

  @ResolveField(() => String, { nullable: true })
  email(@Parent() user: User, @CurrentUser() currentUser: User): string | null {
    // Zwróć email tylko przy przeglądaniu własnego profilu lub jako admin
    if (user.id === currentUser.id || currentUser.role === 'ADMIN') {
      return user.email;
    }
    return null;
  }
}

Dla złożonej autoryzacji warto rozważyć GraphQL Shield lub wbudowaną integrację CASL w NestJS. Artykuł Guardy i Interceptory w NestJS szczegółowo omawia te wzorce.

Popularne Pytania Rekrutacyjne NestJS GraphQL

Rozmowy techniczne na stanowiska NestJS często zawierają pytania specyficzne dla GraphQL. Oto wzorce oceniane przez menedżerów rekrutujących.

P: Jak NestJS radzi sobie z problemem N+1 w GraphQL?

DataLoader grupuje wywołania resolverów pól w ramach pojedynczego żądania. Gdy wiele obiektów nadrzędnych żąda tego samego pola, DataLoader zbiera wszystkie klucze, wykonuje jedno zgrupowane zapytanie i dystrybuuje wyniki. Loader musi być ograniczony do zakresu żądania, aby zapobiec problemom z cachowaniem między żądaniami.

P: Jaka jest różnica między code-first a schema-first w NestJS GraphQL?

Code-first generuje schemat GraphQL z dekoratorów TypeScript w czasie wykonania, automatycznie synchronizując typy i schemat. Schema-first parsuje pliki SDL .graphql, wymagając ręcznych definicji typów. Code-first pasuje do zespołów natywnie pracujących z TypeScript; schema-first sprawdza się lepiej, gdy schemat jest kontraktem między zespołami frontend i backend.

P: Jak zaimplementować uprawnienia na poziomie pól?

Istnieją trzy podejścia: (1) @ResolveField() z warunkowymi zwrotami opartymi na kontekście użytkownika, (2) Własne dekoratory sprawdzające uprawnienia przed rozwiązaniem pola, (3) Dyrektywy schematu jak @auth(requires: ADMIN) przetwarzane przez transformer dyrektyw. Pierwsze podejście oferuje największą elastyczność; dyrektywy zapewniają najczystszą dokumentację schematu.

P: Wyjaśnij kontekst GraphQL w NestJS.

Obiekt kontekstu przechodzi przez wszystkie resolvery w ramach żądania. NestJS domyślnie wypełnia go żądaniem HTTP. Własny kontekst konfiguruje się w GraphQLModule.forRoot() poprzez opcję context — przydatne do dodawania instancji DataLoader, uwierzytelnionego użytkownika lub połączeń z bazą danych.

P: Jak subskrypcje skalują się na wielu instancjach serwera?

Pamięciowy PubSub działa tylko dla wdrożeń jednoinstancyjnych. Architektury wieloinstancyjne wymagają zewnętrznego brokera — standardem jest Redis PubSub. Każda instancja serwera subskrybuje kanały Redis; gdy dowolna instancja publikuje zdarzenie, wszystkie instancje je odbierają i przesyłają do połączonych klientów WebSocket.

Dodatkowe przygotowanie do rozmów o NestJS można znaleźć w module Middleware i Interceptory omawiającym wzorce cyklu życia żądań.

Podsumowanie

  • NestJS GraphQL wspiera oba podejścia: code-first i schema-first — code-first upraszcza projekty TypeScript, schema-first pasuje do pracy opartej na SDL
  • DataLoader eliminuje zapytania N+1 poprzez grupowanie żądań resolverów pól w ramach pojedynczej operacji
  • Instancje DataLoader ograniczone do zakresu żądania zapobiegają zanieczyszczeniu cache między równoczesnymi żądaniami
  • GqlExecutionContext łączy Guardy NestJS z kontekstem resolverów GraphQL
  • Produkcyjne subskrypcje wymagają Redis PubSub dla dystrybucji zdarzeń między wieloma instancjami
  • Autoryzacja na poziomie pól łączy @ResolveField() ze sprawdzaniem kontekstu użytkownika

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Tagi

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

Udostępnij

Powiązane artykuły