☰ Mục lục series
Tài liệu NestJS tiếng Việt
  1. Nhập môn (Overview)
    1. Giới thiệu
    2. First steps
    3. Controllers
    4. Providers
    5. Modules
    6. Middleware
    7. Exception filters
    8. Pipes
    9. Guards
    10. Interceptors
    11. Custom decorators
  2. Nền tảng (Fundamentals)
    1. Custom providers
    2. Asynchronous providers
    3. Dynamic modules
    4. Injection scopes
    5. Circular dependency
    6. Module reference
    7. Lazy-loading modules
    8. Execution context
    9. Lifecycle events
    10. Discovery service
    11. Platform agnosticism
    12. Testing
  3. Kỹ thuật (Techniques)
    1. Configuration
    2. Database (SQL)
    3. MongoDB
    4. Validation
    5. Caching
    6. Serialization
    7. Versioning
    8. Task scheduling
    9. Queues
    10. Logging
    11. Cookies
    12. Events
    13. Compression
    14. File upload
    15. Streaming files
    16. HTTP module
    17. Session
    18. MVC
    19. Performance (Fastify)
    20. Server-Sent Events
  4. Bảo mật (Security)
    1. Authentication
    2. Authorization
    3. Encryption & Hashing
    4. Helmet
    5. CORS
    6. CSRF
    7. Rate limiting
    8. Vòng đời request
  5. OpenAPI (Swagger)
    1. Introduction
    2. Types and parameters
    3. Operations
    4. Security
    5. Mapped types
    6. Decorators
    7. CLI plugin
    8. Other features
  6. Recipes
    1. REPL
    2. CRUD generator
    3. SWC (fast compiler)
    4. Passport (auth)
    5. Hot reload
    6. MikroORM
    7. Router module
    8. Health checks (Terminus)
    9. CQRS
    10. Prisma
    11. Serve static
    12. Commander
    13. Async local storage
  7. WebSockets
    1. Gateways
    2. Exception filters
    3. Pipes
    4. Guards
    5. Interceptors
    6. Adapters
  8. Microservices
    1. Overview
    2. Redis
    3. MQTT
    4. NATS
    5. RabbitMQ
    6. Kafka
    7. gRPC
    8. Custom transporters
    9. Exception filters
    10. Pipes
    11. Pre-request hooks
    12. Guards
    13. Interceptors
  9. GraphQL
    1. Quick start
    2. Resolvers
    3. Mutations
    4. Subscriptions
    5. Scalars
    6. Directives
    7. Interfaces
    8. Unions and enums
    9. Field middleware
    10. Mapped types
    11. Plugins
    12. Complexity
    13. Extensions
    14. CLI plugin
    15. Generating SDL
    16. Sharing models
    17. Other features
    18. Federation
  10. CLI & triển khai
    1. CLI overview
    2. Workspaces
    3. Libraries
    4. Usage
    5. Scripts
    6. Deployment
    7. Standalone apps
  11. FAQ
    1. Serverless
    2. HTTP adapter
    3. Keep-Alive connections
    4. Global path prefix
    5. Raw body
    6. Hybrid application
    7. HTTPS & multiple servers
    8. Common errors
  12. Phụ lục
    1. Bảng thuật ngữ
GraphQL

Resolver

Bản dịch từ docs.nestjs.com/graphql/resolvers

Resolver cung cấp chỉ dẫn để biến một thao tác GraphQL (query, mutation, hoặc subscription) thành dữ liệu thực tế. Chúng trả về đúng hình dạng dữ liệu mà ta khai báo trong schema – có thể là đồng bộ hoặc dưới dạng promise resolve ra kết quả có hình dạng đó. Thông thường, bạn phải tự tay tạo resolver map. Còn package @nestjs/graphql thì tự động sinh resolver map dựa trên metadata mà các decorator bạn dùng để đánh dấu (annotate) class cung cấp. Để minh họa cách dùng các tính năng của package này để xây dựng một GraphQL API, chúng ta sẽ tạo một API authors đơn giản.

Code first (thiết kế theo code)

Trong cách tiếp cận code first, ta không đi theo quy trình thông thường là viết GraphQL schema bằng tay dưới dạng SDL. Thay vào đó, ta dùng decorator của TypeScript để sinh SDL từ các định nghĩa class TypeScript. Package @nestjs/graphql đọc metadata được khai báo qua các decorator đó và tự động sinh schema cho bạn.

Object type

Phần lớn các định nghĩa trong một GraphQL schema là object type. Mỗi object type bạn định nghĩa nên đại diện cho một đối tượng domain (domain object) mà client của ứng dụng có thể cần tương tác. Ví dụ, API mẫu của chúng ta cần lấy được danh sách author và các post của họ, nên ta sẽ định nghĩa type Author và type Post để phục vụ chức năng này.

Nếu dùng cách tiếp cận schema first, ta sẽ định nghĩa schema đó bằng SDL như sau:

type Author {
  id: Int!
  firstName: String
  lastName: String
  posts: [Post!]!
}

Ở đây, dùng cách tiếp cận code first, ta định nghĩa schema bằng class TypeScript và dùng decorator TypeScript để đánh dấu các field của class đó. Đoạn SDL ở trên tương đương với đoạn code first sau:

import { Field, Int, ObjectType } from '@nestjs/graphql';
import { Post } from './post.js';

@ObjectType()
export class Author {
  @Field(type => Int)
  id: number;

  @Field({ nullable: true })
  firstName?: string;

  @Field({ nullable: true })
  lastName?: string;

  @Field(type => [Post])
  posts: Post[];
}

Gợi ý Hệ thống reflection metadata của TypeScript có một số giới hạn khiến nó không thể xác định một class gồm những property nào, hay nhận biết một property có phải là optional hay không. Vì những giới hạn này, ta buộc phải dùng tường minh decorator @Field() trong class định nghĩa schema để cung cấp metadata về GraphQL type và tính optional của từng field, hoặc dùng một CLI plugin để tự sinh các thông tin này giúp ta.

Object type Author, giống như mọi class khác, gồm một tập hợp các field, mỗi field khai báo một type. Type của một field ứng với một GraphQL type. GraphQL type của một field có thể là một object type khác hoặc một scalar type. Scalar type trong GraphQL là một kiểu nguyên thủy (như ID, String, Boolean, hoặc Int) resolve ra một giá trị đơn.

Gợi ý Ngoài các scalar type có sẵn của GraphQL, bạn có thể định nghĩa scalar type tùy chỉnh (đọc thêm).

Định nghĩa object type Author ở trên sẽ khiến Nest sinh ra đoạn SDL đã trình bày phía trên:

type Author {
  id: Int!
  firstName: String
  lastName: String
  posts: [Post!]!
}

Decorator @Field() nhận một type function tùy chọn (ví dụ type => Int), và tùy chọn thêm một object chứa các option.

Type function là bắt buộc khi có khả năng gây nhầm lẫn giữa hệ thống type của TypeScript và hệ thống type của GraphQL. Cụ thể: nó không bắt buộc với kiểu string và boolean; nhưng bắt buộc với number (vì cần map sang GraphQL Int hoặc Float). Type function chỉ đơn giản trả về GraphQL type mong muốn (như trong các ví dụ ở các chương này).

Object chứa option có thể có các cặp key/value sau:

  • nullable: chỉ định field có nullable hay không (trong @nestjs/graphql, mỗi field mặc định là non-nullable); kiểu boolean
  • description: đặt mô tả cho field; kiểu string
  • deprecationReason: đánh dấu field là deprecated; kiểu string

Ví dụ:

@Field({ description: `Book title`, deprecationReason: 'Not useful in v2 schema' })
title: string;

Gợi ý Bạn cũng có thể thêm mô tả, hoặc đánh dấu deprecated, cho toàn bộ object type: @ObjectType({ description: 'Author model' }). Tương tự, nếu ứng dụng của bạn phục vụ nhiều GraphQL endpoint, bạn có thể giới hạn một type vào một module cụ thể: @ObjectType({ registerIn: () => AuthorsModule }).

Khi field là một mảng, ta phải tự tay chỉ rõ type của mảng trong type function của decorator Field(), như bên dưới:

@Field(type => [Post])
posts: Post[];

Gợi ý Dùng ký hiệu ngoặc vuông mảng ([ ]), ta có thể chỉ định độ sâu của mảng. Ví dụ, dùng [[Int]] sẽ đại diện cho một ma trận số nguyên.

Để khai báo rằng các phần tử của mảng (chứ không phải bản thân mảng) là nullable, đặt property nullable thành 'items' như bên dưới:

@Field(type => [Post], { nullable: 'items' })
posts: Post[];

Gợi ý Nếu cả mảng lẫn các phần tử của nó đều nullable, hãy đặt nullable thành 'itemsAndList'.

Object type Author đã được tạo xong, giờ hãy định nghĩa object type Post.

import { Field, Int, ObjectType } from '@nestjs/graphql';

@ObjectType()
export class Post {
  @Field(type => Int)
  id: number;

  @Field()
  title: string;

  @Field(type => Int, { nullable: true })
  votes?: number;
}

Object type Post sẽ sinh ra phần sau đây của GraphQL schema dưới dạng SDL:

type Post {
  id: Int!
  title: String!
  votes: Int
}

Resolver trong Code first (Code first resolver)

Đến đây, ta đã định nghĩa xong các object (định nghĩa type) có thể tồn tại trong data graph, nhưng client vẫn chưa có cách nào để tương tác với các object đó. Để giải quyết, ta cần tạo một resolver class. Trong cách tiếp cận code first, một resolver class vừa định nghĩa các hàm resolver vừa sinh ra Query type. Điều này sẽ rõ hơn khi ta xem ví dụ dưới đây:

@Resolver(() => Author)
export class AuthorsResolver {
  constructor(
    private authorsService: AuthorsService,
    private postsService: PostsService,
  ) {}

  @Query(() => Author)
  async author(@Args('id', { type: () => Int }) id: number) {
    return this.authorsService.findOneById(id);
  }

  @ResolveField()
  async posts(@Parent() author: Author) {
    const { id } = author;
    return this.postsService.findAll({ authorId: id });
  }
}

Ghi chú người dịch Field resolver posts ở trên chính là nơi rất dễ dính bẫy N+1: nếu bạn query nhiều author cùng lúc, GraphQL engine sẽ gọi hàm posts() riêng lẻ cho từng author -> N lần gọi postsService.findAll() thay vì 1 lần gọi gộp. Đây là lỗi hiệu năng kinh điển khi mới làm GraphQL (tương tự N+1 query khi lazy-load quan hệ trong ORM như TypeORM/Eloquent). Cách khắc phục phổ biến là dùng DataLoader để gộp (batch) và cache các lệnh gọi trong cùng một request.

Gợi ý Tất cả decorator (ví dụ @Resolver, @ResolveField, @Args, v.v.) đều được export từ package @nestjs/graphql.

Bạn có thể định nghĩa nhiều resolver class. Nest sẽ gộp (combine) chúng lại khi chạy. Xem thêm phần module bên dưới để biết cách tổ chức code.

Lưu ý Logic bên trong class AuthorsService và PostsService có thể đơn giản hay phức tạp tùy nhu cầu. Mục đích chính của ví dụ này là minh họa cách xây dựng resolver và cách chúng tương tác với các provider khác.

Trong ví dụ trên, ta đã tạo AuthorsResolver, định nghĩa một hàm resolver cho query và một hàm resolver cho field. Để tạo một resolver, ta tạo một class với các hàm resolver là method, và đánh dấu class đó bằng decorator @Resolver().

Trong ví dụ này, ta định nghĩa một query handler để lấy object author dựa trên id gửi kèm trong request. Để chỉ định method là một query handler, dùng decorator @Query().

Tham số truyền vào decorator @Resolver() là tùy chọn, nhưng sẽ có ý nghĩa khi graph của bạn trở nên phức tạp hơn. Nó dùng để cung cấp parent object mà các hàm field resolver sử dụng khi duyệt xuống qua một object graph.

Trong ví dụ của ta, vì class có chứa một hàm field resolver (cho property posts của object type Author), ta bắt buộc phải truyền cho decorator @Resolver() một giá trị để chỉ ra class nào là parent type (tức tên class ObjectType tương ứng) cho tất cả field resolver được định nghĩa trong class này. Như đã thấy rõ trong ví dụ, khi viết một hàm field resolver, ta cần truy cập vào parent object (object mà field đang được resolve là thành viên của nó). Trong ví dụ này, ta điền mảng posts của author bằng một field resolver gọi một service nhận id của author làm tham số. Vì vậy cần xác định parent object trong decorator @Resolver(). Lưu ý cách dùng tương ứng của decorator tham số method @Parent() để lấy ra tham chiếu tới parent object đó trong field resolver.

Ta có thể định nghĩa nhiều hàm resolver @Query() (cả trong class này lẫn trong bất kỳ resolver class nào khác), và chúng sẽ được gộp lại thành một định nghĩa Query type duy nhất trong SDL sinh ra, cùng với các entry tương ứng trong resolver map. Điều này cho phép bạn định nghĩa query ngay gần các model và service mà chúng sử dụng, và giữ chúng gọn gàng trong từng module.

Gợi ý Nest CLI cung cấp một generator (schematic) tự động sinh toàn bộ code khung (boilerplate) giúp bạn khỏi phải làm những việc này, và làm trải nghiệm phát triển đơn giản hơn nhiều. Đọc thêm về tính năng này tại đây.

Tên Query type (Query type names)

Trong các ví dụ trên, decorator @Query() sinh ra tên query type trong GraphQL schema dựa trên tên method. Ví dụ, xét đoạn code sau từ ví dụ trên:

@Query(() => Author)
async author(@Args('id', { type: () => Int }) id: number) {
  return this.authorsService.findOneById(id);
}

Đoạn này sinh ra entry sau cho query author trong schema (query type dùng chung tên với tên method):

type Query {
  author(id: Int!): Author
}

Gợi ý Tìm hiểu thêm về GraphQL query tại đây.

Theo quy ước, ta thường thích tách rời các tên này; ví dụ, ta thích dùng tên như getAuthor() cho method query handler, nhưng vẫn dùng author làm tên query type. Điều tương tự áp dụng cho field resolver. Ta có thể dễ dàng làm việc này bằng cách truyền tên ánh xạ (mapping name) làm tham số cho decorator @Query() và @ResolveField(), như bên dưới:

@Resolver(() => Author)
export class AuthorsResolver {
  constructor(
    private authorsService: AuthorsService,
    private postsService: PostsService,
  ) {}

  @Query(() => Author, { name: 'author' })
  async getAuthor(@Args('id', { type: () => Int }) id: number) {
    return this.authorsService.findOneById(id);
  }

  @ResolveField('posts', () => [Post])
  async getPosts(@Parent() author: Author) {
    const { id } = author;
    return this.postsService.findAll({ authorId: id });
  }
}

Method handler getAuthor ở trên sẽ sinh ra phần sau đây của GraphQL schema dưới dạng SDL:

type Query {
  author(id: Int!): Author
}

Tùy chọn của decorator @Query() (Query decorator options)

Object option của decorator @Query() (nơi ta truyền {name: 'author'} ở trên) nhận một số cặp key/value:

  • name: tên của query; kiểu string
  • description: mô tả dùng để sinh tài liệu cho GraphQL schema (ví dụ trong GraphQL playground); kiểu string
  • deprecationReason: đặt metadata cho query để hiển thị query đã deprecated (ví dụ trong GraphQL playground); kiểu string
  • nullable: query có thể trả về phản hồi dữ liệu null hay không; kiểu boolean hoặc 'items' hoặc 'itemsAndList' (xem phần trên để biết chi tiết về 'items' và 'itemsAndList')

Tùy chọn của decorator @Args() (Args decorator options)

Dùng decorator @Args() để trích xuất tham số từ request để dùng trong method handler. Cách này hoạt động khá tương tự trích xuất tham số route trong REST.

Thông thường, decorator @Args() của bạn sẽ đơn giản và không cần tham số kiểu object, như thấy trong method getAuthor() ở trên. Ví dụ, nếu type của một identifier là string, đoạn code sau là đủ, và chỉ đơn giản lấy field được đặt tên ra từ GraphQL request gửi đến để dùng làm tham số method.

@Args('id') id: string

Trong trường hợp getAuthor(), kiểu number được dùng, và điều này tạo ra một thách thức. Kiểu TypeScript number không cho ta đủ thông tin về biểu diễn GraphQL mong muốn (ví dụ Int so với Float). Do đó, ta phải truyền tường minh type reference. Ta làm điều này bằng cách truyền tham số thứ hai cho decorator Args(), chứa các option của argument, như bên dưới:

@Query(() => Author, { name: 'author' })
async getAuthor(@Args('id', { type: () => Int }) id: number) {
  return this.authorsService.findOneById(id);
}

Object option cho phép ta chỉ định các cặp key-value tùy chọn sau:

  • type: một function trả về GraphQL type
  • defaultValue: giá trị mặc định; kiểu any
  • description: metadata mô tả; kiểu string
  • deprecationReason: để deprecated một field và cung cấp metadata mô tả lý do; kiểu string
  • nullable: field có nullable hay không

Method query handler có thể nhận nhiều tham số. Giả sử ta muốn lấy một author dựa trên firstName và lastName. Trong trường hợp này, ta có thể gọi @Args hai lần:

getAuthor(
  @Args('firstName', { nullable: true }) firstName?: string,
  @Args('lastName', { defaultValue: '' }) lastName?: string,
) {}

Gợi ý Với trường hợp firstName, một field nullable trong GraphQL, không cần thiết phải thêm các kiểu non-value null hoặc undefined vào type của field này. Chỉ cần lưu ý rằng bạn sẽ cần type guard cho các kiểu non-value có thể xảy ra này trong resolver của mình, vì một field nullable trong GraphQL sẽ cho phép các kiểu đó lọt qua tới resolver của bạn.

Class Args riêng (Dedicated arguments class)

Với các lệnh gọi @Args() inline, code như ví dụ trên sẽ trở nên cồng kềnh. Thay vào đó, bạn có thể tạo một class arguments riêng GetAuthorArgs và truy cập nó trong method handler như sau:

@Args() args: GetAuthorArgs

Tạo class GetAuthorArgs bằng @ArgsType() như bên dưới:

import { MinLength } from 'class-validator';
import { Field, ArgsType } from '@nestjs/graphql';

@ArgsType()
class GetAuthorArgs {
  @Field({ nullable: true })
  firstName?: string;

  @Field({ defaultValue: '' })
  @MinLength(3)
  lastName: string;
}

Gợi ý Một lần nữa, vì giới hạn của hệ thống reflection metadata trong TypeScript, ta cần dùng decorator @Field để tự khai báo type và tính optional, hoặc dùng một CLI plugin. Ngoài ra, với trường hợp firstName, một field nullable trong GraphQL, không cần thiết phải thêm các kiểu non-value null hoặc undefined vào type của field này. Chỉ cần lưu ý rằng bạn sẽ cần type guard cho các kiểu non-value có thể xảy ra này trong resolver của mình, vì một field nullable trong GraphQL sẽ cho phép các kiểu đó lọt qua tới resolver của bạn.

Đoạn này sẽ sinh ra phần sau đây của GraphQL schema dưới dạng SDL:

type Query {
  author(firstName: String, lastName: String = ''): Author
}

Gợi ý Lưu ý rằng các class argument như GetAuthorArgs phối hợp rất tốt với ValidationPipe (đọc thêm).

Kế thừa class (Class inheritance)

Bạn có thể dùng cơ chế kế thừa class chuẩn của TypeScript để tạo các base class chứa các tính năng type tiện ích dùng chung (field, property của field, validation, v.v.) để mở rộng về sau. Ví dụ, bạn có thể có một tập hợp tham số liên quan đến phân trang luôn bao gồm các field chuẩn offset và limit, nhưng cũng có các field index khác đặc thù theo từng type. Bạn có thể thiết lập một hệ thống phân cấp class như bên dưới.

Base class @ArgsType():

@ArgsType()
class PaginationArgs {
  @Field(() => Int)
  offset: number = 0;

  @Field(() => Int)
  limit: number = 10;
}

Sub-class đặc thù theo type, kế thừa base class @ArgsType():

@ArgsType()
class GetAuthorArgs extends PaginationArgs {
  @Field({ nullable: true })
  firstName?: string;

  @Field({ defaultValue: '' })
  @MinLength(3)
  lastName: string;
}

Cách tiếp cận tương tự có thể áp dụng cho object @ObjectType(). Định nghĩa các property dùng chung trên base class:

@ObjectType()
class Character {
  @Field(() => Int)
  id: number;

  @Field()
  name: string;
}

Thêm các property đặc thù theo type trên sub-class:

@ObjectType()
class Warrior extends Character {
  @Field()
  level: number;
}

Bạn cũng có thể dùng kế thừa với resolver. Bạn có thể đảm bảo type safety bằng cách kết hợp kế thừa và generic của TypeScript. Ví dụ, để tạo một base class với một query findAll chung, dùng cấu trúc như sau:

function BaseResolver<T extends Type<unknown>>(classRef: T): any {
  @Resolver({ isAbstract: true })
  abstract class BaseResolverHost {
    @Query(() => [classRef], { name: `findAll${classRef.name}` })
    async findAll(): Promise<T[]> {
      return [];
    }
  }
  return BaseResolverHost;
}

Ghi chú người dịch BaseResolver ở đây là một hàm nhận vào một class và trả về một class mới (kỹ thuật gọi là mixin trong TypeScript) – khác với generic kiểu Go (trước Go 1.18 không có generic thật, thường phải dùng interface{} và code sinh tự động; từ 1.18 Go có type parameter nhưng không có kiểu “mixin trả về class” như thế này). Nếu quen Go hoặc PHP, cứ hiểu đơn giản: đây là cách TypeScript tạo ra “class cha dùng chung logic CRUD” mà vẫn giữ được type cụ thể cho từng entity, tránh phải copy-paste code resolver findAll cho mỗi loại.

Lưu ý những điểm sau:

  • Cần khai báo tường minh kiểu trả về (any ở trên); nếu không, TypeScript sẽ báo lỗi vì dùng một định nghĩa class private. Khuyến nghị: định nghĩa một interface thay vì dùng any.
  • Type được import từ package @nestjs/common
  • Property isAbstract: true chỉ ra rằng SDL (Schema Definition Language) không nên được sinh ra cho class này. Lưu ý, bạn cũng có thể đặt property này cho các type khác để bỏ qua việc sinh SDL.

Đây là cách bạn có thể sinh ra một subclass cụ thể của BaseResolver:

@Resolver(() => Recipe)
export class RecipesResolver extends BaseResolver(Recipe) {
  constructor(private recipesService: RecipesService) {
    super();
  }
}

Cấu trúc này sẽ sinh ra SDL sau:

type Query {
  findAllRecipe: [Recipe!]!
}

Generic (Generics)

Ta đã thấy một cách dùng generic ở trên. Tính năng mạnh mẽ này của TypeScript có thể dùng để tạo các lớp trừu tượng (abstraction) hữu ích. Ví dụ, đây là một cài đặt mẫu cho phân trang kiểu cursor-based, dựa trên tài liệu này:

import { Field, ObjectType, Int } from '@nestjs/graphql';
import { Type } from '@nestjs/common';

interface IEdgeType<T> {
  cursor: string;
  node: T;
}

export interface IPaginatedType<T> {
  edges: IEdgeType<T>[];
  nodes: T[];
  totalCount: number;
  hasNextPage: boolean;
}

export function Paginated<T>(classRef: Type<T>): Type<IPaginatedType<T>> {
  @ObjectType(`${classRef.name}Edge`)
  abstract class EdgeType {
    @Field(() => String)
    cursor: string;

    @Field(() => classRef)
    node: T;
  }

  @ObjectType({ isAbstract: true })
  abstract class PaginatedType implements IPaginatedType<T> {
    @Field(() => [EdgeType], { nullable: true })
    edges: EdgeType[];

    @Field(() => [classRef], { nullable: true })
    nodes: T[];

    @Field(() => Int)
    totalCount: number;

    @Field()
    hasNextPage: boolean;
  }
  return PaginatedType as Type<IPaginatedType<T>>;
}

Với base class ở trên, giờ ta có thể dễ dàng tạo ra các type chuyên biệt kế thừa hành vi này. Ví dụ:

@ObjectType()
class PaginatedAuthor extends Paginated(Author) {}

Schema first (thiết kế theo schema)

Như đã đề cập ở chương trước, trong cách tiếp cận schema first, ta bắt đầu bằng cách tự tay định nghĩa các schema type bằng SDL (đọc thêm). Xét các định nghĩa type SDL sau.

Gợi ý Để tiện trong chương này, ta đã gộp toàn bộ SDL vào một nơi (ví dụ, một file .graphql duy nhất, như bên dưới). Trong thực tế, bạn có thể thấy phù hợp hơn khi tổ chức code theo kiểu module hóa. Ví dụ, sẽ hữu ích nếu tạo các file SDL riêng chứa định nghĩa type đại diện cho từng domain entity, cùng với service, resolver code liên quan, và class định nghĩa Nest module, trong một thư mục riêng cho entity đó. Nest sẽ gộp tất cả các định nghĩa schema type riêng lẻ này lại khi chạy.

type Author {
  id: Int!
  firstName: String
  lastName: String
  posts: [Post]
}

type Post {
  id: Int!
  title: String!
  votes: Int
}

type Query {
  author(id: Int!): Author
}

Resolver trong Schema first (Schema first resolver)

Schema ở trên expose một query duy nhất - author(id: Int!): Author.

Gợi ý Tìm hiểu thêm về GraphQL query tại đây.

Giờ hãy tạo một class AuthorsResolver để resolve query author:

@Resolver('Author')
export class AuthorsResolver {
  constructor(
    private authorsService: AuthorsService,
    private postsService: PostsService,
  ) {}

  @Query()
  async author(@Args('id') id: number) {
    return this.authorsService.findOneById(id);
  }

  @ResolveField()
  async posts(@Parent() author) {
    const { id } = author;
    return this.postsService.findAll({ authorId: id });
  }
}

Gợi ý Tất cả decorator (ví dụ @Resolver, @ResolveField, @Args, v.v.) đều được export từ package @nestjs/graphql.

Lưu ý Logic bên trong class AuthorsService và PostsService có thể đơn giản hay phức tạp tùy nhu cầu. Mục đích chính của ví dụ này là minh họa cách xây dựng resolver và cách chúng tương tác với các provider khác.

Decorator @Resolver() là bắt buộc. Nó nhận một tham số string tùy chọn là tên một class. Tên class này bắt buộc phải có bất cứ khi nào class chứa decorator @ResolveField(), để báo cho Nest biết method được đánh dấu đó gắn với một parent type (type Author trong ví dụ hiện tại). Ngoài ra, thay vì đặt @Resolver() ở đầu class, việc này có thể làm cho từng method riêng lẻ:

@Resolver('Author')
@ResolveField()
async posts(@Parent() author) {
  const { id } = author;
  return this.postsService.findAll({ authorId: id });
}

Trong trường hợp này (decorator @Resolver() ở cấp method), nếu bạn có nhiều decorator @ResolveField() trong một class, bạn phải thêm @Resolver() cho tất cả các decorator đó. Đây không được xem là best practice (vì nó tạo thêm overhead).

Gợi ý Bất kỳ tham số tên class nào truyền vào @Resolver() không ảnh hưởng đến query (decorator @Query()) hay mutation (decorator @Mutation()).

Cảnh báo Dùng decorator @Resolver ở cấp method không được hỗ trợ với cách tiếp cận code first.

Trong các ví dụ trên, decorator @Query() và @ResolveField() được gắn với các type trong GraphQL schema dựa trên tên method. Ví dụ, xét đoạn code sau từ ví dụ trên:

@Query()
async author(@Args('id') id: number) {
  return this.authorsService.findOneById(id);
}

Đoạn này sinh ra entry sau cho query author trong schema (query type dùng chung tên với tên method):

type Query {
  author(id: Int!): Author
}

Theo quy ước, ta sẽ thích tách rời các tên này, dùng tên như getAuthor() hoặc getPosts() cho method resolver. Ta có thể dễ dàng làm việc này bằng cách truyền tên ánh xạ (mapping name) làm tham số cho decorator, như bên dưới:

@Resolver('Author')
export class AuthorsResolver {
  constructor(
    private authorsService: AuthorsService,
    private postsService: PostsService,
  ) {}

  @Query('author')
  async getAuthor(@Args('id') id: number) {
    return this.authorsService.findOneById(id);
  }

  @ResolveField('posts')
  async getPosts(@Parent() author) {
    const { id } = author;
    return this.postsService.findAll({ authorId: id });
  }
}

Gợi ý Nest CLI cung cấp một generator (schematic) tự động sinh toàn bộ code khung (boilerplate) giúp bạn khỏi phải làm những việc này, và làm trải nghiệm phát triển đơn giản hơn nhiều. Đọc thêm về tính năng này tại đây.

Sinh type tự động (Generating types)

Giả sử ta dùng cách tiếp cận schema first và đã bật tính năng sinh typings (với outputAs: 'class' như trong chương trước), khi chạy ứng dụng, nó sẽ sinh ra file sau (tại vị trí bạn chỉ định trong method GraphQLModule.forRoot()). Ví dụ, trong src/graphql.ts:

export class Author {
  id: number;
  firstName?: string;
  lastName?: string;
  posts?: Post[];
}
export class Post {
  id: number;
  title: string;
  votes?: number;
}

export abstract class IQuery {
  abstract author(id: number): Author | Promise<Author>;
}

Ghi chú người dịch So sánh nhanh với hai cách tiếp cận REST bạn đã quen: code first giống như định nghĩa model bằng class/decorator rồi để framework tự sinh ra “hợp đồng” (schema) – tương tự cách TypeORM entity sinh ra database schema. Schema first giống hơn với thiết kế API-first bằng OpenAPI/Protobuf: bạn viết hợp đồng (SDL) trước, rồi công cụ sinh ra type/class cho bạn code theo. Không có cách nào “đúng” tuyệt đối, chọn tùy quy trình làm việc của team (schema first hợp khi nhiều team FE/BE cùng thống nhất hợp đồng trước).

Bằng cách sinh ra class (thay vì kỹ thuật mặc định là sinh interface), bạn có thể dùng các decorator validation khai báo kết hợp với cách tiếp cận schema first, đây là một kỹ thuật cực kỳ hữu ích (đọc thêm). Ví dụ, bạn có thể thêm decorator class-validator vào class CreatePostInput được sinh ra như bên dưới để ép buộc độ dài chuỗi tối thiểu và tối đa cho field title:

import { MinLength, MaxLength } from 'class-validator';

export class CreatePostInput {
  @MinLength(3)
  @MaxLength(50)
  title: string;
}

Lưu ý Để bật tính năng tự động validate input (và tham số) của bạn, dùng ValidationPipe. Đọc thêm về validation tại đây và cụ thể hơn về pipe tại đây.

Tuy nhiên, nếu bạn thêm decorator trực tiếp vào file được tự động sinh ra, chúng sẽ bị ghi đè mỗi lần file được sinh lại. Thay vào đó, hãy tạo một file riêng và đơn giản là extend class được sinh ra.

import { MinLength, MaxLength } from 'class-validator';
import { Post } from '../../graphql.js';

export class CreatePostInput extends Post {
  @MinLength(3)
  @MaxLength(50)
  title: string;
}

Decorator tham số GraphQL (GraphQL argument decorators)

Ta có thể truy cập các tham số resolver chuẩn của GraphQL bằng các decorator chuyên biệt. Dưới đây là bảng so sánh giữa decorator của Nest và các tham số Apollo thuần mà chúng đại diện.

@Root() và @Parent() root/parent
@Context(param?: string) context / context[param]
@Info(param?: string) info / info[param]
@Args(param?: string) args / args[param]

Các tham số này có ý nghĩa như sau:

  • root: một object chứa kết quả trả về từ resolver ở field cha, hoặc, trong trường hợp field Query cấp cao nhất, là rootValue được truyền từ cấu hình server.
  • context: một object dùng chung cho tất cả resolver trong một query cụ thể; thường dùng để chứa state theo từng request.
  • info: một object chứa thông tin về trạng thái thực thi của query.
  • args: một object chứa các tham số được truyền vào field trong query.

Module

Sau khi hoàn tất các bước trên, ta đã khai báo đầy đủ mọi thông tin mà GraphQLModule cần để sinh ra resolver map. GraphQLModule dùng reflection để đọc metadata được cung cấp qua các decorator, và tự động chuyển các class thành resolver map đúng chuẩn.

Việc còn lại duy nhất bạn cần làm là provide (tức liệt kê như một provider trong module nào đó) các resolver class (AuthorsResolver), và import module (AuthorsModule) ở đâu đó, để Nest có thể sử dụng nó.

Ví dụ, ta có thể làm việc này trong một AuthorsModule, module này cũng có thể provide các service khác cần dùng trong ngữ cảnh này. Nhớ import AuthorsModule ở đâu đó (ví dụ, trong root module, hoặc một module khác được root module import).

@Module({
  imports: [PostsModule],
  providers: [AuthorsService, AuthorsResolver],
})
export class AuthorsModule {}

Gợi ý Sẽ hữu ích nếu bạn tổ chức code theo cái gọi là domain model (tương tự cách bạn tổ chức entry point trong một REST API). Với cách tiếp cận này, giữ model (class ObjectType), resolver và service cùng nhau trong một Nest module đại diện cho domain model. Giữ tất cả các thành phần này trong một thư mục riêng cho mỗi module. Khi làm như vậy, và dùng Nest CLI để sinh từng phần tử, Nest sẽ tự động kết nối tất cả các phần này lại với nhau (đặt file vào đúng thư mục, sinh entry trong mảng provider và imports, v.v.) cho bạn.

Nội dung gốc: NestJS Documentation © Kamil Myśliwiec, giấy phép MIT. Bản dịch và ghi chú: Lê Văn Dũng.