NestJS และ GraphQL ในปี 2026: Schema, Resolver และคำถามสัมภาษณ์

คู่มือครบถ้วนสำหรับการผสานรวม NestJS GraphQL ด้วยแนวทาง schema-first และ code-first, รูปแบบ resolver และคำถามสัมภาษณ์สำหรับตำแหน่ง senior developer

สถาปัตยกรรม NestJS GraphQL API พร้อม resolver และ schema

การผสานรวม NestJS GraphQL มอบแนวทางที่มีโครงสร้างในการสร้าง API ที่ type-safe โดยใช้ประโยชน์จาก decorator ของ TypeScript และภาษา query ของ GraphQL บทความนี้ครอบคลุมทั้งแนวทาง schema-first และ code-first, รูปแบบ resolver รวมถึงคำถามสัมภาษณ์ที่ hiring manager มักถามในปี 2026

Code-First vs Schema-First

NestJS 11 ใช้ GraphQL แบบ code-first เป็นค่าเริ่มต้น โดยสร้าง schema จาก TypeScript class แนวทาง schema-first ยังคงมีให้ใช้สำหรับทีมที่มีไฟล์ .graphql อยู่แล้วหรือ workflow ที่ใช้ SDL

การตั้งค่า NestJS GraphQL กับ Apollo Server

NestJS ผสานรวมกับ Apollo Server ผ่าน package @nestjs/graphql การตั้งค่าจะแตกต่างกันตามแนวทางที่เลือก—code-first สร้าง SDL จาก decorator ในขณะที่ schema-first parse ไฟล์ .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: 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 {}

ตัวเลือก autoSchemaFile เปิดใช้งานโหมด code-first หากตั้งค่าเป็น true จะสร้าง schema ในหน่วยความจำโดยไม่เขียนลง disk—มีประโยชน์สำหรับการ deploy แบบ serverless ที่การเข้าถึง filesystem อาจถูกจำกัด

การกำหนด GraphQL Type ด้วย Decorator แบบ Code-First

Code-first กำหนด GraphQL type โดยใช้ TypeScript class ที่ตกแต่งด้วย @ObjectType() แต่ละ field ใช้ @Field() เพื่อระบุ GraphQL type และ nullability

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;
}

ตัวเลือก nullable รับค่าสามค่า: true (field เป็น optional), 'items' (item ใน list สามารถเป็น null), และ 'itemsAndList' (ทั้ง list และ item สามารถเป็น null) ความละเอียดนี้ตรงกับ semantic ของ nullability ใน GraphQL อย่างแม่นยำ

การสร้าง Resolver สำหรับ Query และ Mutation

Resolver จัดการ operation ของ GraphQL ที่เข้ามา NestJS ใช้ @Resolver() เพื่อทำเครื่องหมาย class เป็น resolver โดยมี method decorator ระบุประเภทของ operation

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) กำหนดบริบทสำหรับ decorator @ResolveField() ทำให้สามารถ resolve ในระดับ field สำหรับข้อมูลที่คำนวณหรือเกี่ยวข้อง

Input Type และ Validation ด้วย class-validator

GraphQL input type กำหนด payload สำหรับ mutation การรวม @InputType() กับ decorator ของ class-validator ช่วยให้สามารถ validate ทั้งในระดับ schema และ 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;
}

เปิดใช้งาน validation ทั่วทั้งระบบโดยเพิ่ม ValidationPipe ใน main.ts error ของ GraphQL จะรวมข้อความ validation ใน field extensions เพื่อรักษาความเข้ากันได้กับ client

พร้อมที่จะพิชิตการสัมภาษณ์ Node.js / NestJS แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

การ Resolve ข้อมูลที่เกี่ยวข้องด้วย @ResolveField และ DataLoader

Field resolver จัดการความสัมพันธ์ระหว่าง type หากไม่มีการเพิ่มประสิทธิภาพ การดึงรายชื่อ user พร้อมกับ post จะทำให้เกิด N+1 query—หนึ่งสำหรับ user แล้วอีกหนึ่งต่อ user สำหรับ 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 รวมกลุ่มและ cache request ภายใน operation GraphQL เดียว สำหรับ NestJS ให้กำหนด scope ของ DataLoader เป็นระดับ request โดยใช้ @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 ลด N+1 query ให้เหลือ query batch เดียว ซึ่งสำคัญมากสำหรับ API GraphQL ที่ client ควบคุมความลึกของ query สำหรับการศึกษาเพิ่มเติมเกี่ยวกับรูปแบบสถาปัตยกรรม NestJS ดู คำถามสัมภาษณ์ NestJS Modules & Dependency Injection

Subscription สำหรับข้อมูล Real-Time

GraphQL subscription ส่งข้อมูลไปยัง client ผ่านการเชื่อมต่อ WebSocket NestJS ใช้ library graphql-ws ซึ่ง implement โปรโตคอล 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;
  }
}

สำหรับการ deploy แบบ production บนหลาย instance ให้แทนที่ PubSub ในหน่วยความจำด้วย graphql-redis-subscriptions เพื่อกระจาย event ไปทั่วทั้ง cluster

Authentication และ Authorization ใน GraphQL

NestJS Guard ทำงานร่วมกับ GraphQL resolver ได้อย่างราบรื่น execution context แตกต่างจาก REST—ใช้ GqlExecutionContext เพื่อแยก 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
  }
}

ใช้ guard ในระดับ resolver หรือ method การ authorize ในระดับ field ใช้ @ResolveField() กับ logic เงื่อนไขตาม 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;
  }
}

สำหรับการ authorize ที่ซับซ้อน พิจารณาใช้ GraphQL Shield หรือการผสานรวม CASL ที่มีอยู่ใน NestJS บทความ NestJS Guards and Interceptors อธิบายรูปแบบเหล่านี้อย่างละเอียด

คำถามสัมภาษณ์ NestJS GraphQL ที่พบบ่อย

การสัมภาษณ์เทคนิคสำหรับตำแหน่ง NestJS มักรวมคำถามเฉพาะเกี่ยวกับ GraphQL ต่อไปนี้คือรูปแบบที่ hiring manager ประเมิน

ถ: NestJS จัดการปัญหา N+1 ใน GraphQL อย่างไร?

DataLoader รวมกลุ่มการเรียก field resolver ภายใน request เดียว เมื่อหลาย parent object ร้องขอ field เดียวกัน DataLoader จะรวบรวม key ทั้งหมด execute query batch เดียว และแจกจ่ายผลลัพธ์ loader ต้องมี scope เป็น request เพื่อป้องกันปัญหา caching ข้าม request

ถ: ความแตกต่างระหว่าง code-first และ schema-first ใน NestJS GraphQL คืออะไร?

Code-first สร้าง GraphQL schema จาก TypeScript decorator ในขณะ runtime โดยรักษา type และ schema ให้ซิงค์กันอัตโนมัติ Schema-first parse ไฟล์ SDL .graphql ซึ่งต้องกำหนด type ด้วยตนเอง Code-first เหมาะกับทีมที่ใช้ TypeScript เป็นหลัก schema-first ทำงานได้ดีกว่าเมื่อ schema เป็น contract ระหว่างทีม frontend และ backend

ถ: จะ implement permission ในระดับ field ได้อย่างไร?

มีสามแนวทาง: (1) @ResolveField() กับ return แบบมีเงื่อนไขตาม context ของ user, (2) custom decorator ที่ตรวจสอบ permission ก่อน resolve field, (3) schema directive เช่น @auth(requires: ADMIN) ที่ประมวลผลโดย directive transformer แนวทางแรกมีความยืดหยุ่นมากที่สุด directive ให้ documentation ของ schema ที่ชัดเจนที่สุด

ถ: อธิบาย GraphQL context ใน NestJS

object context ถูกส่งผ่านทุก resolver ภายใน request NestJS จะเติม HTTP request เป็นค่าเริ่มต้น context แบบกำหนดเองถูกตั้งค่าใน GraphQLModule.forRoot() ผ่านตัวเลือก context—มีประโยชน์สำหรับการเพิ่ม instance ของ DataLoader, user ที่ authenticate แล้ว หรือการเชื่อมต่อ database

ถ: subscription ถูก scale บนหลาย server instance ได้อย่างไร?

PubSub ในหน่วยความจำใช้งานได้เฉพาะการ deploy แบบ single-instance สถาปัตยกรรม multi-instance ต้องการ broker ภายนอก—Redis PubSub เป็นมาตรฐาน แต่ละ server instance subscribe ไปยัง Redis channel เมื่อ instance ใดก็ตาม publish event ทุก instance จะได้รับและส่งไปยัง WebSocket client ที่เชื่อมต่อ

สำหรับการเตรียมสัมภาษณ์ NestJS เพิ่มเติม ศึกษา module Middleware และ Interceptors ที่ครอบคลุมรูปแบบ request lifecycle

สรุป

  • NestJS GraphQL รองรับทั้งแนวทาง code-first และ schema-first—code-first ทำให้โปรเจกต์ TypeScript ง่ายขึ้น schema-first เหมาะกับ workflow ที่ใช้ SDL
  • DataLoader กำจัด N+1 query โดยรวมกลุ่ม request ของ field resolver ภายใน operation เดียว
  • DataLoader instance ที่มี scope เป็น request ป้องกันการปนเปื้อน cache ระหว่าง request ที่เกิดขึ้นพร้อมกัน
  • GqlExecutionContext เชื่อมต่อ NestJS Guard กับ context ของ GraphQL resolver
  • subscription สำหรับ production ต้องการ Redis PubSub สำหรับการกระจาย event แบบ multi-instance
  • การ authorize ในระดับ field รวม @ResolveField() กับการตรวจสอบ context ของ user

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

แท็ก

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

แชร์

บทความที่เกี่ยวข้อง