NestJS dan GraphQL di Tahun 2026: Schema, Resolver, dan Pertanyaan Interview

Panduan lengkap integrasi NestJS GraphQL dengan pendekatan schema-first dan code-first, pola resolver, serta pertanyaan interview untuk posisi senior developer.

Arsitektur NestJS GraphQL API dengan resolver dan schema

Integrasi NestJS GraphQL menyediakan pendekatan terstruktur untuk membangun API yang type-safe dengan memanfaatkan decorator TypeScript dan bahasa query GraphQL. Tutorial ini membahas pendekatan schema-first dan code-first, pola resolver, serta pertanyaan interview yang sering diajukan hiring manager di tahun 2026.

Code-First vs Schema-First

NestJS 11 secara default menggunakan GraphQL code-first, yang menghasilkan schema dari class TypeScript. Schema-first tetap tersedia untuk tim yang sudah memiliki file .graphql atau workflow berbasis SDL.

Konfigurasi NestJS GraphQL dengan Apollo Server

NestJS terintegrasi dengan Apollo Server melalui package @nestjs/graphql. Konfigurasi berbeda berdasarkan pendekatan yang dipilih—code-first menghasilkan SDL dari decorator, sementara schema-first mem-parse file .graphql secara langsung.

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: generates schema
      sortSchema: true, // Alphabetical ordering for readability
      playground: process.env.NODE_ENV !== 'production', // Disable in prod
      introspection: process.env.NODE_ENV !== 'production',
    }),
  ],
})
export class AppModule {}

Opsi autoSchemaFile mengaktifkan mode code-first. Jika diatur ke true, schema akan dihasilkan di memory tanpa menulis ke disk—berguna untuk deployment serverless di mana akses filesystem mungkin terbatas.

Mendefinisikan Tipe GraphQL dengan Decorator Code-First

Code-first mendefinisikan tipe GraphQL menggunakan class TypeScript yang didekorasi dengan @ObjectType(). Setiap field menggunakan @Field() untuk menentukan tipe GraphQL dan nullability-nya.

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

@ObjectType({ description: 'Application user' }) // Description appears in schema docs
export class User {
  @Field(() => ID) // Maps to GraphQL ID scalar
  id: string;

  @Field()
  email: string;

  @Field({ nullable: true }) // Optional field in GraphQL
  displayName?: string;

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

  @Field(() => [Post], { nullable: 'itemsAndList' }) // Both list and items can be null
  posts?: Post[];

  // Fields without @Field() are excluded from GraphQL schema
  passwordHash: string;
}

Opsi nullable menerima tiga nilai: true (field bersifat opsional), 'items' (item dalam list bisa null), dan 'itemsAndList' (baik list maupun item bisa null). Granularitas ini sesuai dengan semantik nullability GraphQL secara presisi.

Membangun Resolver untuk Query dan Mutation

Resolver menangani operasi GraphQL yang masuk. NestJS menggunakan @Resolver() untuk menandai class sebagai resolver, dengan method decorator yang menentukan tipe operasi.

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) // Binds resolver to User type for field resolution
export class UsersResolver {
  constructor(private readonly usersService: UsersService) {}

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

Decorator @Resolver(() => User) menetapkan konteks untuk decorator @ResolveField(), memungkinkan resolusi tingkat field untuk data yang dihitung atau terkait.

Input Type dan Validasi dengan class-validator

Input type GraphQL mendefinisikan payload mutation. Menggabungkan @InputType() dengan decorator class-validator memungkinkan validasi di tingkat schema dan runtime.

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

  @Field()
  @MinLength(8, { message: 'Password must be at least 8 characters' })
  @Matches(/[A-Z]/, { message: 'Password must contain uppercase letter' })
  password: string;

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

Aktifkan validasi secara global dengan menambahkan ValidationPipe di main.ts. Error GraphQL menyertakan pesan validasi di field extensions, menjaga kompatibilitas dengan client.

Siap menguasai wawancara Node.js / NestJS Anda?

Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.

Menyelesaikan Data Terkait dengan @ResolveField dan DataLoader

Field resolver menangani relasi antar tipe. Tanpa optimasi, mengambil daftar user beserta post-nya akan memicu N+1 query—satu untuk user, lalu satu per user untuk post.

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 batches requests: one query for all user IDs
    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 mengelompokkan dan meng-cache request dalam satu operasi GraphQL. Untuk NestJS, scope DataLoader ke level request menggunakan @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 }) // New instance per request
export class PostsLoader {
  constructor(private readonly postsService: PostsService) {}

  public readonly batchByUserId = new DataLoader<string, Post[]>(
    async (userIds: readonly string[]) => {
      // Single query: SELECT * FROM posts WHERE user_id IN (...)
      const posts = await this.postsService.findByUserIds([...userIds]);
      // Map results back to input order
      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 mengurangi N+1 query menjadi satu query batch, sangat penting untuk API GraphQL di mana client mengontrol kedalaman query. Untuk pembahasan lebih mendalam tentang pola arsitektur NestJS, lihat pertanyaan interview NestJS Modules & Dependency Injection.

Subscription untuk Data Real-Time

Subscription GraphQL mengirimkan data ke client melalui koneksi WebSocket. NestJS menggunakan library graphql-ws, yang mengimplementasikan protokol GraphQL over WebSocket.

app.module.ts - Enable subscriptionstypescript
GraphQLModule.forRoot<ApolloDriverConfig>({
  driver: ApolloDriver,
  autoSchemaFile: true,
  subscriptions: {
    'graphql-ws': true, // Modern protocol
    'subscriptions-transport-ws': false, // Deprecated legacy protocol
  },
}),
posts.resolver.tstypescript
import { Resolver, Subscription } from '@nestjs/graphql';
import { PubSub } from 'graphql-subscriptions';
import { Post } from './post.entity';

const pubSub = new PubSub(); // Use Redis PubSub for multi-instance deployments

@Resolver(() => Post)
export class PostsResolver {
  @Subscription(() => Post, {
    filter: (payload, variables) => 
      payload.postCreated.userId === variables.userId, // Client-side filtering
  })
  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 }); // Trigger subscription
    return post;
  }
}

Untuk deployment produksi di beberapa instance, ganti PubSub in-memory dengan graphql-redis-subscriptions untuk menyiarkan event ke seluruh cluster.

Autentikasi dan Autorisasi di GraphQL

NestJS Guard bekerja dengan lancar dengan GraphQL resolver. Execution context berbeda dari REST—gunakan GqlExecutionContext untuk mengekstrak request.

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; // Extract request from GraphQL context
  }
}

Terapkan guard di level resolver atau method. Autorisasi tingkat field menggunakan @ResolveField() dengan logika kondisional berdasarkan role user.

users.resolver.tstypescript
@UseGuards(GqlAuthGuard)
@Resolver(() => User)
export class UsersResolver {
  @Query(() => User)
  me(@CurrentUser() user: User): User {
    return user; // Return authenticated user
  }

  @ResolveField(() => String, { nullable: true })
  email(@Parent() user: User, @CurrentUser() currentUser: User): string | null {
    // Only return email if viewing own profile or admin
    if (user.id === currentUser.id || currentUser.role === 'ADMIN') {
      return user.email;
    }
    return null;
  }
}

Untuk autorisasi yang kompleks, pertimbangkan GraphQL Shield atau integrasi CASL bawaan NestJS. Artikel NestJS Guards and Interceptors membahas pola-pola ini secara mendalam.

Pertanyaan Interview NestJS GraphQL yang Umum

Interview teknis untuk posisi NestJS sering menyertakan pertanyaan spesifik tentang GraphQL. Berikut adalah pola-pola yang dievaluasi oleh hiring manager.

T: Bagaimana NestJS menangani masalah N+1 di GraphQL?

DataLoader mengelompokkan panggilan field resolver dalam satu request. Ketika beberapa objek parent meminta field yang sama, DataLoader mengumpulkan semua key, mengeksekusi satu query batch, dan mendistribusikan hasilnya. Loader harus ber-scope request untuk mencegah masalah caching lintas request.

T: Apa perbedaan antara code-first dan schema-first di NestJS GraphQL?

Code-first menghasilkan schema GraphQL dari decorator TypeScript saat runtime, menjaga tipe dan schema tetap sinkron secara otomatis. Schema-first mem-parse file SDL .graphql, memerlukan definisi tipe manual. Code-first cocok untuk tim yang native TypeScript; schema-first lebih baik ketika schema menjadi kontrak antara tim frontend dan backend.

T: Bagaimana cara mengimplementasikan permission tingkat field?

Ada tiga pendekatan: (1) @ResolveField() dengan return kondisional berdasarkan konteks user, (2) Custom decorator yang memeriksa permission sebelum resolusi field, (3) Schema directive seperti @auth(requires: ADMIN) yang diproses oleh directive transformer. Pendekatan pertama menawarkan fleksibilitas paling tinggi; directive memberikan dokumentasi schema yang paling bersih.

T: Jelaskan GraphQL context di NestJS.

Objek context melewati semua resolver dalam satu request. NestJS mengisinya dengan HTTP request secara default. Context kustom dikonfigurasi di GraphQLModule.forRoot() melalui opsi context—berguna untuk menambahkan instance DataLoader, user yang terautentikasi, atau koneksi database.

T: Bagaimana subscription diskalakan di beberapa instance server?

PubSub in-memory hanya berfungsi untuk deployment single-instance. Arsitektur multi-instance memerlukan broker eksternal—Redis PubSub adalah standarnya. Setiap instance server subscribe ke channel Redis; ketika instance mana pun mempublikasikan event, semua instance menerimanya dan push ke client WebSocket yang terhubung.

Untuk persiapan interview NestJS tambahan, eksplorasi modul Middleware dan Interceptors yang membahas pola request lifecycle.

Kesimpulan

  • NestJS GraphQL mendukung pendekatan code-first dan schema-first—code-first menyederhanakan proyek TypeScript, schema-first cocok untuk workflow berbasis SDL
  • DataLoader mengeliminasi N+1 query dengan mengelompokkan request field resolver dalam satu operasi
  • Instance DataLoader dengan scope request mencegah polusi cache di antara request konkuren
  • GqlExecutionContext menjembatani NestJS Guard dengan konteks resolver GraphQL
  • Subscription produksi memerlukan Redis PubSub untuk distribusi event multi-instance
  • Autorisasi tingkat field menggabungkan @ResolveField() dengan pemeriksaan konteks user

Mulai berlatih!

Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.

Tag

#nestjs
#graphql
#nodejs
#typescript
#api
#tutorial

Bagikan

Artikel terkait