# NestJS và GraphQL năm 2026: Schema, Resolver và Câu hỏi Phỏng vấn > Hướng dẫn toàn diện về tích hợp NestJS GraphQL với phương pháp schema-first và code-first, các pattern resolver, cùng câu hỏi phỏng vấn cho vị trí senior developer. - Published: 2026-07-25 - Updated: 2026-07-25 - Author: SharpSkill - Tags: nestjs, graphql, nodejs, typescript, api, tutorial - Reading time: 12 min --- Tích hợp NestJS GraphQL cung cấp phương pháp có cấu trúc để xây dựng API type-safe bằng cách tận dụng decorator TypeScript và ngôn ngữ truy vấn GraphQL. Bài hướng dẫn này bao gồm cả phương pháp schema-first và code-first, các pattern resolver, cùng những câu hỏi phỏng vấn mà hiring manager thường hỏi trong năm 2026. > **Code-First vs Schema-First** > > NestJS 11 mặc định sử dụng GraphQL code-first, sinh schema từ các class TypeScript. Schema-first vẫn khả dụng cho các team đã có sẵn file `.graphql` hoặc workflow dựa trên SDL. ## Cấu hình NestJS GraphQL với Apollo Server NestJS tích hợp với [Apollo Server](https://www.apollographql.com/docs/apollo-server/) thông qua package `@nestjs/graphql`. Cấu hình khác nhau tùy thuộc vào phương pháp được chọn—code-first sinh SDL từ decorator, trong khi schema-first parse trực tiếp các file `.graphql`. ```typescript // app.module.ts import { Module } from '@nestjs/common'; import { GraphQLModule } from '@nestjs/graphql'; import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo'; import { join } from 'path'; @Module({ imports: [ GraphQLModule.forRoot({ 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 {} ``` Tùy chọn `autoSchemaFile` kích hoạt chế độ code-first. Đặt thành `true` sẽ sinh schema trong bộ nhớ mà không ghi ra disk—hữu ích cho các deployment serverless nơi quyền truy cập filesystem có thể bị hạn chế. ## Định nghĩa GraphQL Type với Decorator Code-First Code-first định nghĩa các GraphQL type bằng class TypeScript được trang trí với `@ObjectType()`. Mỗi field sử dụng `@Field()` để chỉ định GraphQL type và tính nullability. ```typescript // user.entity.ts 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; } ``` Tùy chọn `nullable` chấp nhận ba giá trị: `true` (field là tùy chọn), `'items'` (các item trong list có thể null), và `'itemsAndList'` (cả list và item đều có thể null). Độ chi tiết này phù hợp chính xác với [ngữ nghĩa nullability của GraphQL](https://graphql.org/learn/schema/#lists-and-non-null). ## Xây dựng Resolver cho Query và Mutation Resolver xử lý các thao tác GraphQL đến. NestJS sử dụng `@Resolver()` để đánh dấu một class là resolver, với các method decorator chỉ định loại thao tác. ```typescript // users.resolver.ts 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 { return this.usersService.findAll(); } @Query(() => User, { nullable: true }) user(@Args('id', { type: () => ID }) id: string): Promise { return this.usersService.findOne(id); } @Mutation(() => User) createUser(@Args('input') input: CreateUserInput): Promise { return this.usersService.create(input); } } ``` Decorator `@Resolver(() => User)` thiết lập ngữ cảnh cho các decorator `@ResolveField()`, cho phép phân giải ở cấp field cho dữ liệu được tính toán hoặc liên quan. ## Input Type và Validation với class-validator GraphQL input type định nghĩa payload cho mutation. Kết hợp `@InputType()` với các decorator [class-validator](https://github.com/typestack/class-validator) cho phép validation ở cả cấp schema và runtime. ```typescript // dto/create-user.input.ts 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; } ``` Kích hoạt validation toàn cục bằng cách thêm `ValidationPipe` trong `main.ts`. Các lỗi GraphQL bao gồm thông báo validation trong field `extensions`, duy trì khả năng tương thích với client. ## Phân giải Dữ liệu Liên quan với @ResolveField và DataLoader Field resolver xử lý các mối quan hệ giữa các type. Nếu không có tối ưu hóa, việc lấy danh sách user cùng với các bài post sẽ kích hoạt N+1 query—một cho user, sau đó một cho mỗi user để lấy post. ```typescript // users.resolver.ts 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 { // DataLoader batches requests: one query for all user IDs return this.postsLoader.batchByUserId.load(user.id); } @ResolveField(() => Int) async postCount(@Parent() user: User): Promise { const posts = await this.postsLoader.batchByUserId.load(user.id); return posts.length; } } ``` DataLoader nhóm và cache các request trong một thao tác GraphQL. Đối với NestJS, đặt scope của DataLoader ở cấp request bằng cách sử dụng `@Injectable({ scope: Scope.REQUEST })`. ```typescript // posts/posts.loader.ts 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( 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(); posts.forEach(post => { const existing = postsMap.get(post.userId) || []; postsMap.set(post.userId, [...existing, post]); }); return userIds.map(id => postsMap.get(id) || []); } ); } ``` DataLoader giảm N+1 query xuống còn một query batch duy nhất, rất quan trọng cho các API GraphQL nơi client kiểm soát độ sâu của query. Để tìm hiểu sâu hơn về các pattern kiến trúc NestJS, xem [câu hỏi phỏng vấn NestJS Modules & Dependency Injection](/technologies/node-nestjs/interview-questions/nestjs-modules-di). ## Subscription cho Dữ liệu Real-Time GraphQL subscription đẩy dữ liệu đến client qua kết nối WebSocket. NestJS sử dụng thư viện `graphql-ws`, triển khai [giao thức GraphQL over WebSocket](https://github.com/enisdenjo/graphql-ws). ```typescript // app.module.ts - Enable subscriptions GraphQLModule.forRoot({ driver: ApolloDriver, autoSchemaFile: true, subscriptions: { 'graphql-ws': true, // Modern protocol 'subscriptions-transport-ws': false, // Deprecated legacy protocol }, }), ``` ```typescript // posts.resolver.ts 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 { const post = await this.postsService.create(input); pubSub.publish('postCreated', { postCreated: post }); // Trigger subscription return post; } } ``` Đối với các deployment production trên nhiều instance, thay thế `PubSub` trong bộ nhớ bằng `graphql-redis-subscriptions` để broadcast event trên toàn cluster. ## Xác thực và Phân quyền trong GraphQL NestJS Guard hoạt động liền mạch với GraphQL resolver. Execution context khác với REST—sử dụng `GqlExecutionContext` để trích xuất request. ```typescript // guards/gql-auth.guard.ts 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 } } ``` Áp dụng guard ở cấp resolver hoặc method. Phân quyền cấp field sử dụng `@ResolveField()` với logic điều kiện dựa trên role của user. ```typescript // users.resolver.ts @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; } } ``` Đối với phân quyền phức tạp, cân nhắc sử dụng [GraphQL Shield](https://github.com/dimatill/graphql-shield) hoặc tích hợp CASL có sẵn của NestJS. Bài viết [NestJS Guards and Interceptors](/blog/node-nestjs/nestjs-guards-interceptors-modular-architecture) thảo luận chi tiết về các pattern này. ## Câu hỏi Phỏng vấn NestJS GraphQL Phổ biến Phỏng vấn kỹ thuật cho các vị trí NestJS thường bao gồm các câu hỏi cụ thể về GraphQL. Dưới đây là các pattern mà hiring manager đánh giá. **H: NestJS xử lý vấn đề N+1 trong GraphQL như thế nào?** DataLoader nhóm các lời gọi field resolver trong một request. Khi nhiều đối tượng parent yêu cầu cùng một field, DataLoader thu thập tất cả các key, thực thi một query batch, và phân phối kết quả. Loader phải có scope request để tránh các vấn đề caching giữa các request. **H: Sự khác biệt giữa code-first và schema-first trong NestJS GraphQL là gì?** Code-first sinh GraphQL schema từ các decorator TypeScript lúc runtime, giữ cho type và schema tự động đồng bộ. Schema-first parse các file SDL `.graphql`, yêu cầu định nghĩa type thủ công. Code-first phù hợp với các team native TypeScript; schema-first hoạt động tốt hơn khi schema là contract giữa các team frontend và backend. **H: Làm thế nào để triển khai permission ở cấp field?** Có ba cách tiếp cận: (1) `@ResolveField()` với return có điều kiện dựa trên context user, (2) Custom decorator kiểm tra permission trước khi phân giải field, (3) Schema directive như `@auth(requires: ADMIN)` được xử lý bởi directive transformer. Cách tiếp cận đầu tiên cung cấp tính linh hoạt cao nhất; directive cung cấp documentation schema rõ ràng nhất. **H: Giải thích GraphQL context trong NestJS.** Đối tượng context được truyền qua tất cả các resolver trong một request. NestJS mặc định điền HTTP request vào đó. Context tùy chỉnh được cấu hình trong `GraphQLModule.forRoot()` thông qua tùy chọn `context`—hữu ích để thêm các instance DataLoader, user đã xác thực, hoặc kết nối database. **H: Subscription được scale như thế nào trên nhiều instance server?** PubSub trong bộ nhớ chỉ hoạt động cho deployment single-instance. Kiến trúc multi-instance yêu cầu broker bên ngoài—Redis PubSub là tiêu chuẩn. Mỗi instance server subscribe vào các channel Redis; khi bất kỳ instance nào publish event, tất cả các instance đều nhận được và đẩy đến các client WebSocket đang kết nối. Để chuẩn bị phỏng vấn NestJS thêm, khám phá [module Middleware và Interceptors](/technologies/node-nestjs/interview-questions/middleware-interceptors) bao gồm các pattern request lifecycle. ## Kết luận - NestJS GraphQL hỗ trợ cả phương pháp code-first và schema-first—code-first đơn giản hóa các dự án TypeScript, schema-first phù hợp với workflow dựa trên SDL - DataLoader loại bỏ N+1 query bằng cách nhóm các request field resolver trong một thao tác - Các instance DataLoader có scope request ngăn chặn ô nhiễm cache giữa các request đồng thời - `GqlExecutionContext` kết nối NestJS Guard với context resolver GraphQL - Subscription production yêu cầu Redis PubSub để phân phối event multi-instance - Phân quyền cấp field kết hợp `@ResolveField()` với kiểm tra context user --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/vi/blog/node-nestjs/nestjs-graphql-schemas-resolvers-tutorial