WisarWisar
Dasturlash kitobi/8-QISM — NestJS26 daqiqa

8.17-bob: GraphQL — NestJS GraphQL

8-QISM — NestJS (chuqur) · 17-mavzu


1. Kirish va motivatsiya

Mikroservislarni 8.16-bob bildik. Endi REST'ga (5.7) muqobil API uslubiniGraphQL ni — o'rganamiz. Shu paytgacha barcha API'larimiz REST edi (endpoint'lar — GET /users, POST /orders). GraphQL — boshqa falsafa: bitta endpoint, va mijoz aniq kerakli ma'lumotni so'raydi (ortiqcha emas, kam emas). Bu — Facebook yaratgan, ko'p kompaniya (GitHub, Shopify) ishlatadigan kuchli yondashuv. NestJS GraphQL'ni birinchi darajada qo'llab-quvvatlaydi — @nestjs/graphql bilan TypeScript dekoratorlardan (code-first) avtomatik schema.

REST'ning ikki muammosi: over-fetching (kerak bo'lmagan maydonlar ham keladi — /users butun user obyektini beradi, sizga faqat ism kerak bo'lsa ham) va under-fetching (bir sahifa uchun ko'p endpoint — user + uning buyurtmalari + har buyurtma mahsulotlari = 3+ so'rov). GraphQL buni hal qiladi: mijoz bitta so'rovda aniq nima kerakligini aytadi (qaysi maydonlar, qaysi bog'liqliklar), server aynan shuni beradi. Frontend uchun juda qulay (11-QISM).

Bu bob: GraphQL nima va nega, REST vs GraphQL, code-first vs schema-first, resolver (@Query/@Mutation), @ObjectType/@Field, scalar va custom scalar, argumentlar (@Args/ArgsType), field resolver (bog'liqlik), subscriptions (real-time), N+1 muammosi (DataLoader — eng muhim), pagination (cursor/Relay), Apollo Sandbox, guards/interceptors/pipes GraphQL'da, xato boshqaruvi va federation (qisqacha). Bu bob 5.7 (REST), 8.13 (DB), 5.13 (real-time), 8.16 (mikroservis) bilan bog'liq. GraphQL — moslashuvchan, mijoz-boshqaruvchi API.

O'xshatish: REST — belgilangan menyu (har taom — alohida tovoq, alohida buyurtma; "kombo" olsang ham kerak bo'lmagan narsa keladi). GraphQLbuffet bilan aniq talab: bitta stolga (endpoint) borasiz va aynan nima xohlashingizni aytasiz ("faqat osh va non, salatsiz") — ortiqcha tovoq yo'q, kam emas, bir martada hammasi. Oshpaz (server) aniq so'rovingizni tayyorlaydi. Bu — mijozga kuch beradi (nima kerakligini o'zi belgilaydi), lekin oshpaz (server) buni to'g'ri uddalashi kerak (N+1 — 2.12).

Nega muhim?

  • Aniq ma'lumot — over/under-fetching yo'q (mijoz so'raydi).
  • Bitta so'rov — bog'liq ma'lumot birga (frontend qulay).
  • Tipli schema — o'zini hujjatlaydi (introspection).
  • NestJS code-first — dekoratordan avtomatik (tur xavfsiz).

2. Nazariya — chuqur tushuntirish

2.1. GraphQL nima (asoslar)

text
  GraphQL — so'rov tili (query language) API uchun:
  - BITTA endpoint (/graphql) — REST'da ko'p 5.7-bob
  - Mijoz AYNAN kerakli ma'lumotni so'raydi
  - 3 amal turi:
    Query        — o'qish (GET kabi)
    Mutation     — o'zgartirish (POST/PUT/DELETE kabi)
    Subscription — real-time (WebSocket — 5.13)

  So'rov:                    Javob (aynan so'ralgani):
  query {                    {
    user(id: 1) {              "user": {
      ism                        "ism": "Ali",
      email                      "email": "ali@a.uz",
      buyurtmalar { summa }      "buyurtmalar": [{ "summa": 50000 }]
    }                          }
  }                          }

GraphQL — API so'rov tili: bitta endpoint (/graphql), mijoz aniq maydonlarni so'raydi. 3 amal: Query (o'qish), Mutation (o'zgartirish), Subscription (real-time — 5.13). Javob — so'rov shakliga mos (aynan so'ralgan maydonlar). Bu — REST'dan tubdan farq (mijoz boshqaradi).

2.2. REST vs GraphQL (taqqoslash)

text
  REST 5.7-bob                      GraphQL
  Ko'p endpoint                   Bitta endpoint (/graphql)
  Server javob shaklini belgilar  Mijoz so'raydi (aniq)
  Over/under-fetching             Aynan kerakli
  HTTP status/metodlar            Query/Mutation/Subscription
  Oddiy, kesh oson (HTTP)         Moslashuvchan, lekin kesh qiyin
  Fayl, oddiy CRUD — qulay        Murakkab bog'liq ma'lumot — qulay

   GraphQL REST'ni ALMASHTIRMAYDI — har biri o'rni bor

REST vs GraphQL: REST — ko'p endpoint, server belgilaydi, oddiy/kesh oson; GraphQL — bitta endpoint, mijoz so'raydi, moslashuvchan/murakkab bog'liqlik uchun. GraphQL REST'dan "yaxshiroq" emas — boshqacha (har biri o'rni bor). Murakkab, bog'liq, mijoz-har-xil (mobil+veb) — GraphQL; oddiy CRUD, fayl, kesh muhim — REST.

2.3. Code-first vs Schema-first

text
  CODE-FIRST (NestJS afzal):
  TypeScript klass/dekorator  schema AVTOMATIK generatsiya
   Tur xavfsiz (7), DRY, refactor oson
   @ObjectType, @Field, @Query (2.4, 2.5)

  SCHEMA-FIRST:
  GraphQL SDL (.graphql fayl) qo'lda  TS tur generatsiya
   Til-agnostik, frontend bilan kelishish oson

Code-first vs Schema-first: code-first (NestJS afzal) — TypeScript dekoratorlardan schema avtomatik (tur xavfsiz — 7, DRY — DTO kabi 8.5). Schema-first — SDL (.graphql) qo'lda yoziladi, TS tur generatsiya. Code-first — TS dunyosida tabiiy (kitobda shuni ishlatamiz). Ikkalasi ham NestJS'da.

Solishtirish uchun — bir xil User turi va query ikki uslubda:

ts
// ── CODE-FIRST (kitobda shu — dekoratordan schema chiqadi) ──
@ObjectType()
export class User {
  @Field(() => ID) id: number;
  @Field() ism: string;
}
@Resolver(() => User)
export class UsersResolver {
  @Query(() => User) user(@Args("id", { type: () => ID }) id: number) { /* ... */ }
}
// autoSchemaFile: "schema.gql"  schema AVTOMATIK yoziladi (2.4)
graphql
# ── SCHEMA-FIRST (SDL fayl qo'lda — user.graphql) ──
type User {
  id: ID!
  ism: String!
}
type Query {
  user(id: ID!): User
}
# typePaths: ["**/*.graphql"]  bu SDL'dan TS tur generatsiya
# Resolver'da @Resolver("User") + @Query("user") NOM bo'yicha bog'lanadi

Ikki uslub farqi (kod): code-first — TS klass manba (.gql chiqadi); schema-first — SDL manba (TS tur chiqadi — nest generate yoki definitions). Schema-first GraphQLModule.forRoot({ typePaths: ["**/*.graphql"] }). Aralash jamoada (frontend SDL'ni oldin belgilaydi) — schema-first; sof TS jamoa — code-first. Kitobda code-first (2.4+).

2.4. GraphQLModule sozlash (code-first)

ts
import { GraphQLModule } from "@nestjs/graphql";
import { ApolloDriver, ApolloDriverConfig } from "@nestjs/apollo";

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: "schema.gql",                   // code-first: avtomatik schema (2.3)
      sortSchema: true,
      playground: true,                               // GraphQL Playground (interaktiv — dev)
    }),
  ],
})
export class AppModule {}

GraphQLModule: driver (Apollo — eng keng), autoSchemaFilecode-first (dekoratorlardan schema avtomatik — 2.3), playground (interaktiv test UI — Swagger kabi — 8.8, faqat dev — 14). Bu — GraphQL'ning markaziy sozlamasi. Mercurius driver ham (Fastify).

2.5. ObjectType va Field (GraphQL tur)

ts
import { ObjectType, Field, Int, ID } from "@nestjs/graphql";

@ObjectType()                                        // GraphQL tur (entity)
export class User {
  @Field(() => ID)                                   // ID tur
  id: number;

  @Field()                                           // String (avtomatik)
  ism: string;

  @Field()
  email: string;

  @Field(() => Int)                                  // raqam (aniq tur)
  yosh: number;

  @Field({ nullable: true })                         // ixtiyoriy
  avatar?: string;

  // parol — @Field YO'Q  GraphQL'da ko'rinmaydi (14)
  parol: string;

  @Field(() => [Order])                              // bog'liqlik (massiv)
  buyurtmalar: Order[];
}

@ObjectType/@Field — GraphQL turini belgilaydi (DTO 8.5 kabi). @Field() — schema'da ko'rinadigan maydon (tur: () => Int, ID, [Order]). nullable (ixtiyoriy). @Field yo'q maydon GraphQL'da ko'rinmaydi (parol — 14 — avtomatik yashirin). Code-first: bu klassdan schema avtomatik. Tur xavfsiz (7).

2.5a. Scalar turlar va custom scalar (Date, JSON)

ts
import { Field, Int, Float, ID, GraphQLISODateTime } from "@nestjs/graphql";

// ── O'RNATILGAN (built-in) scalar turlar ──
//   String   — matn (@Field() avtomatik)
//   Int      — butun son (@Field(() => Int))
//   Float    — kasr son (@Field(() => Float))
//   Boolean  — true/false (@Field() boolean uchun avtomatik)
//   ID       — unikal identifikator (@Field(() => ID) — String kabi seriyalanadi)

@ObjectType()
export class Hodisa {
  @Field(() => ID) id: number;
  @Field(() => GraphQLISODateTime)                   // NestJS'ning ISO Date scalar'i
  vaqt: Date;
}
ts
// ── CUSTOM SCALAR (masalan: Date'ni o'zimiz boshqarish) ──
import { Scalar, CustomScalar } from "@nestjs/graphql";
import { Kind, ValueNode } from "graphql";

@Scalar("Date", () => Date)                           // schema'da "Date" nomli scalar
export class DateScalar implements CustomScalar<number, Date> {
  description = "Date maxsus scalar turi (millisekund timestamp)";

  parseValue(value: number): Date {                   // mijoz  server (kirish)
    return new Date(value);
  }
  serialize(value: Date): number {                    // server  mijoz (chiqish)
    return value.getTime();
  }
  parseLiteral(ast: ValueNode): Date {                // inline literal (query ichida)
    if (ast.kind === Kind.INT) return new Date(parseInt(ast.value, 10));
    return null;
  }
}
// provider sifatida ro'yxatdan o'tkaziladi: providers: [DateScalar]

Scalar turlar — GraphQL'ning "atom" turlari: built-in (String, Int, Float, Boolean, ID) va NestJS qo'shimchalari (GraphQLISODateTime, GraphQLTimestamp). Standart GraphQL'da Date yo'q — shuning uchun GraphQLISODateTime yoki custom scalar (@Scalar + CustomScalar: serialize chiqish, parseValue/parseLiteral kirish). JSON, Upload, EmailAddress kabilar uchun graphql-scalars kutubxonasi. Custom scalar — o'z formatingizni schema'ga qo'shish.

2.6. Resolver va Query (o'qish)

ts
import { Resolver, Query, Args, Int } from "@nestjs/graphql";

@Resolver(() => User)                                // User uchun resolver (controller kabi)
export class UsersResolver {
  constructor(private usersService: UsersService) {} // DI (8.2)

  @Query(() => [User])                               // ro'yxat query
  users() {
    return this.usersService.hammasi();
  }

  @Query(() => User, { nullable: true })             // bitta query
  user(@Args("id", { type: () => Int }) id: number) {
    return this.usersService.bitta(id);
  }
}

@Resolver/@Query — resolver = GraphQL "controller" 8.1-bob. @Query(() => Tur) — o'qish amali (GET kabi). @Args — argument (id). Service'ni chaqiradi (DI — controller kabi yupqa — 8.1: 2.7). Mijoz query { users { ism } } deydi faqat ism keladi. Bu — GraphQL'ning yuragi.

2.7. Mutation (o'zgartirish)

ts
import { Mutation, Args } from "@nestjs/graphql";

@Mutation(() => User)
yaratUser(@Args("input") input: CreateUserInput) {   // input tur (2.8)
  return this.usersService.yarat(input);
}

@Mutation(() => User)
yangilaUser(
  @Args("id", { type: () => Int }) id: number,
  @Args("input") input: UpdateUserInput,
) {
  return this.usersService.yangila(id, input);
}

@Mutation(() => Boolean)
ochirUser(@Args("id", { type: () => Int }) id: number) {
  return this.usersService.ochir(id);
}

@Mutation — o'zgartirish amali (POST/PUT/DELETE kabi — 5.7). Qaytaradi (o'zgartirilgan obyekt — mijoz kerakli maydonni so'raydi). @Args("input")InputType 2.8-bob. Mijoz mutation { yaratUser(input: {...}) { id ism } }. Query (o'qish) va Mutation (yozish) — GraphQL'da ajratilgan.

2.8. InputType va ArgsType (kirish — validatsiya)

ts
import { InputType, Field } from "@nestjs/graphql";
import { IsEmail, MinLength } from "class-validator";

@InputType()                                         // kirish turi (DTO — 8.5)
export class CreateUserInput {
  @Field()
  @MinLength(2)                                      // validatsiya (8.5 — bir xil!)
  ism: string;

  @Field()
  @IsEmail()
  email: string;

  @Field()
  @MinLength(8)
  parol: string;
}

@InputType — mutatsiya kirish turi (DTO 8.5 GraphQL versiyasi). class-validator dekoratorlari bir xil ishlaydi (@IsEmail, @MinLength — 8.5)! ValidationPipe (global — 8.5) GraphQL'da ham. Bir manba (tur + validatsiya — DRY). Bu — NestJS'ning GraphQL+validatsiya integratsiyasi.

2.9. Field Resolver (bog'liqlik — lazy)

ts
import { ResolveField, Parent } from "@nestjs/graphql";

@Resolver(() => User)
export class UsersResolver {
  @Query(() => [User])
  users() { return this.usersService.hammasi(); }

  // Field resolver — user.buyurtmalar FAQAT so'ralganda yuklanadi (lazy)
  @ResolveField(() => [Order])
  buyurtmalar(@Parent() user: User) {                 // ota obyekt
    return this.ordersService.userBuyurtmalari(user.id);
  }
}
// query { users { ism } }             buyurtmalar YUKLANMAYDI
// query { users { ism buyurtmalar }}  buyurtmalar yuklanadi (kerak bo'lsa)

@ResolveField — bog'liq maydonni faqat so'ralganda yuklash (lazy — over-fetching yo'q). @Parent() — ota obyekt (user). Mijoz buyurtmalar so'ramasa — yuklanmaydi (tejamkor). Lekin bu N+1 muammosini keltiradi (2.12 — har user uchun alohida so'rov) — DataLoader bilan hal qilinadi 2.13-bob.

2.10. Subscriptions (real-time — 5.13)

ts
import { Subscription } from "@nestjs/graphql";
import { PubSub } from "graphql-subscriptions";

const pubSub = new PubSub();

@Resolver()
export class NotificationsResolver {
  @Subscription(() => Notification)
  yangiBildirishnoma() {
    return pubSub.asyncIterator("yangiBildirishnoma");   // kanal
  }

  @Mutation(() => Notification)
  async yubor(@Args("matn") matn: string) {
    const xabar = { matn, vaqt: new Date() };
    await pubSub.publish("yangiBildirishnoma", { yangiBildirishnoma: xabar });   // e'lon
    return xabar;
  }
}

@Subscription — real-time (server mijoz push — WebSocket — 5.13). PubSub (publish/subscribe — 5.13: 2.10). Mijoz subscription { yangiBildirishnoma { matn } } — kanal ochiladi, har publishda xabar keladi. Chat, bildirishnoma, jonli yangilanish. Resurs talab (ochiq ulanish — ehtiyot). Production'da Redis PubSub (5.13, 8.15).

2.11. Guards/Pipes GraphQL'da (8.5, 8.6)

ts
import { UseGuards } from "@nestjs/common";
import { GqlExecutionContext } from "@nestjs/graphql";

// GraphQL uchun guard (context boshqacha)
@Injectable()
export class GqlAuthGuard extends AuthGuard("jwt") {
  getRequest(context: ExecutionContext) {
    const ctx = GqlExecutionContext.create(context);   // GraphQL context
    return ctx.getContext().req;
  }
}

@Query(() => User)
@UseGuards(GqlAuthGuard)                              // himoyalangan (8.9)
me(@CurrentUser() user) {
  return user;
}

Guards/Pipes GraphQL'da (8.5, 8.6) — ishlaydi, lekin context boshqacha (HTTP emas — GqlExecutionContext). Guard getRequest override. ValidationPipe 8.5-bob, auth guard 8.9-bob — moslashtirilgan. Enhancer'lar (guard/interceptor) field resolver'da ishlamaydi (faqat top-level Query/Mutation — performance). NestJS imkoniyatlari GraphQL'da ham (moslashtirilgan).

2.12. N+1 muammosi (eng muhim performance)

text
  N+1 MUAMMOSI (field resolver — 2.9):
  query { users { ism buyurtmalar { summa } } }

  1 so'rov:  SELECT * FROM users            (N ta user)
  N so'rov:  har user uchun  SELECT * FROM orders WHERE user=?
   1 + N so'rov (100 user  101 so'rov!) — DB ni o'ldiradi

  Yechim: DataLoader 2.13-bob — barcha buyurtmani BITTA so'rovda (batch)

N+1 muammosi (GraphQL'ning eng mashhur muammosi): field resolver 2.9-bob — N ta ota uchun N ta qo'shimcha so'rov (1+N). 100 user 101 DB so'rov (juda sekin!). REST'da kamroq (server bitta JOIN — 6.7), lekin GraphQL'da har field alohida. DataLoader 2.13-bob — buni hal qiladi (batch + cache). Bu — GraphQL'da MUTLAQ bilish kerak.

2.13. DataLoader (N+1 yechimi — batch)

ts
import * as DataLoader from "dataloader";

// DataLoader — ko'p .load() ni BITTA batch so'rovga (2.12 yechimi)
@Injectable()
export class OrdersLoader {
  createLoader() {
    return new DataLoader<number, Order[]>(async (userIds: number[]) => {
      // BITTA so'rov — barcha user uchun (batch!)
      const orders = await this.ordersService.userlarBuyurtmalari(userIds);
      // Har user'ga moslash
      return userIds.map((id) => orders.filter((o) => o.userId === id));
    });
  }
}

// Field resolver — DataLoader bilan (N+1 yo'q)
@ResolveField(() => [Order])
buyurtmalar(@Parent() user: User, @Context() ctx) {
  return ctx.loaders.ordersLoader.load(user.id);     // batch (1 so'rov)
}

DataLoader (N+1 yechimi — docs): ko'p .load(id) chaqiruvini bitta batch so'rovga to'playdi (1+N 2 so'rov). Har so'rovga yangi instance (request-scoped — kesh izolyatsiya — best practice). GraphQL context'da (ctx.loaders). Batch + cache. Bu — GraphQL performance'ning kaliti (N+1'siz).

2.13a. Pagination (Relay / cursor-based)

text
  ICKI USUL:
  1) OFFSET (sahifa/limit) — oddiy, lekin katta ma'lumotda sekin (2.12: filter kabi)
     users(sahifa: 2, limit: 20)
  2) CURSOR (Relay) — GraphQL "rasmiy" standarti (barqaror, cheksiz skroll)
     users(first: 20, after: "cursor")    edges + pageInfo
ts
import { ObjectType, Field, Int, ArgsType } from "@nestjs/graphql";

// ── Relay-uslub connection (cursor) turlari ──
@ObjectType()
export class PageInfo {
  @Field() hasNextPage: boolean;
  @Field() hasPreviousPage: boolean;
  @Field({ nullable: true }) startCursor?: string;
  @Field({ nullable: true }) endCursor?: string;
}

@ObjectType()
export class UserEdge {
  @Field() cursor: string;                            // base64(id) — barqaror ko'rsatkich
  @Field(() => User) node: User;                      // haqiqiy element
}

@ObjectType()
export class UserConnection {
  @Field(() => [UserEdge]) edges: UserEdge[];
  @Field(() => PageInfo) pageInfo: PageInfo;
  @Field(() => Int) totalCount: number;
}

@ArgsType()
export class PaginationArgs {
  @Field(() => Int, { defaultValue: 20 }) first: number;   // nechta
  @Field({ nullable: true }) after?: string;               // qaysi cursor'dan keyin
}

@Query(() => UserConnection)
usersConnection(@Args() { first, after }: PaginationArgs) {
  return this.usersService.sahifala(first, after);    // edges + pageInfo qaytaradi
}

Pagination: ikki yo'l — offset (sahifa/limit — oddiy, lekin element qo'shilsa siljish bo'ladi) va cursor/Relay (first/after edges + pageInfo — barqaror, cheksiz skroll uchun ideal — 11). Relay standarti: cursor odatda base64(id), pageInfo.hasNextPage — davomi bormi. Katta ro'yxat, real-time qo'shiluvchi ma'lumot (feed, chat) cursor afzal (offset dublikat/o'tkazib yuborishga olib keladi). Apollo Client cursor pagination'ni tabiiy qo'llaydi (11).

2.13b. Apollo Sandbox va Playground (interaktiv IDE)

ts
import { ApolloDriver, ApolloDriverConfig } from "@nestjs/apollo";
import { ApolloServerPluginLandingPageLocalDefault } from "@apollo/server/plugin/landingPage/default";

GraphQLModule.forRoot<ApolloDriverConfig>({
  driver: ApolloDriver,
  autoSchemaFile: "schema.gql",
  playground: false,                                  // eski Playground O'CHIQ
  plugins: [ApolloServerPluginLandingPageLocalDefault()],   // yangi Apollo Sandbox
});
//  /graphql ochilganda Apollo Sandbox UI (introspection, query yozish, tarix)

Apollo Sandbox — Apollo Server 4+ (@nestjs/apollo yangi) dagi zamonaviy interaktiv IDE (eski playground: true — GraphQL Playground endi eskirgan/deprecated). ApolloServerPluginLandingPageLocalDefault plagini bilan yoqiladi. /graphqlga brauzerdan kirilganda ochiladi: schema ko'rish (introspection), query/mutation yozish va yuborish, o'zgaruvchilar, tarix. Swagger'ning 8.8-bob GraphQL ekvivalenti. Production'da o'chirish (schema oshkor bo'ladi — 14): plugins: [ApolloServerPluginLandingPageDisabled()].

2.13c. Federation (mikroservis GraphQL — qisqacha)

text
  FEDERATION — ko'p GraphQL servisni BITTA schema'ga birlashtirish 8.16-bob:
  ┌─ users-service   (User turi — "egasi")
  ├─ orders-service  (Order turi + User'ga @extends bilan maydon qo'shadi)
  └─ Apollo Gateway   hammasini bitta /graphql qilib ko'rsatadi (mijoz uchun yagona)
ts
// ── users-service (subgraph) ──
import { ObjectType, Field, ID, Directive } from "@nestjs/graphql";

@ObjectType()
@Directive('@key(fields: "id")')                      // federation: bu tur kaliti
export class User {
  @Field(() => ID) id: number;
  @Field() ism: string;
}
// module: GraphQLModule.forRoot({ driver: ApolloFederationDriver, autoSchemaFile: { federation: 2 } })

// ── orders-service (User'ni KENGAYTIRADI — @extends) ──
@Resolver(() => User)
export class UsersResolver {
  @ResolveReference()                                 // Gateway boshqa servisdan User so'raganda
  resolveReference(ref: { __typename: string; id: number }) {
    return { id: ref.id };
  }
  @ResolveField(() => [Order])                        // User'ga "buyurtmalar" maydonini qo'shadi
  buyurtmalar(@Parent() user: User) {
    return this.ordersService.userBuyurtmalari(user.id);
  }
}

Federation (qisqacha) — bir nechta GraphQL subgraph (har mikroservis — 8.16) ni Apollo Gateway yordamida bitta birlashgan schema'ga yig'ish. Har servis o'z turini "egalik qiladi" (@key), boshqasi uni @extends/@ResolveReference bilan kengaytiradi (masalan orders-service Userga buyurtmalar qo'shadi). NestJS: ApolloFederationDriver (subgraph) va ApolloGatewayDriver (gateway). Bu — katta, ko'p-jamoali tizim uchun (kichik loyihada ortiqcha murakkablik). Monolitda oddiy modul kifoya. Chuqurroq — 8.16 (mikroservislar).

2.14. Best practices (GraphQL)

text
   Code-first (tur xavfsiz — 2.3, 2.4)
   @ObjectType/@Field; parol @Field'siz (yashirin — 14, 2.5)
   Resolver yupqa  service (8.1: 2.7, 2.6)
   InputType + class-validator (DRY — 8.5, 2.8)
   Field resolver (lazy bog'liqlik — 2.9) + DataLoader (N+1 — 2.12, 2.13)
   Subscription ehtiyotkorlik (resurs — Redis PubSub prod — 2.10)
   Guard GqlExecutionContext (auth — 2.11)
   Cursor pagination katta ro'yxatga (Relay — 2.13a)
   Custom scalar Date/JSON uchun (GraphQLISODateTime — 2.5a)
   Apollo Sandbox faqat dev (14, 2.13b); query depth/complexity limit (DoS)
   Federation faqat ko'p-servisli katta tizimga (2.13c, 8.16)

3. Sintaksis — tez ma'lumotnoma

ts
// Module 2.4-bob: GraphQLModule.forRoot({ driver: ApolloDriver, autoSchemaFile: "schema.gql" })

// Tur 2.5-bob: @ObjectType() class User { @Field(() => ID) id: number; }
// Input 2.8-bob: @InputType() class CreateUserInput { @Field() @IsEmail() email: string; }

// Resolver (2.6, 2.7, 2.9)
@Resolver(() => User)
@Query(() => [User])  users() {}
@Mutation(() => User)  yarat(@Args("input") input) {}
@ResolveField(() => [Order])  buyurtmalar(@Parent() user) {}

// Subscription 2.10-bob: @Subscription(() => X)  pubSub.asyncIterator("kanal")
// DataLoader 2.13-bob: loader.load(id)  // batch

// Scalar (2.5a): @Field(() => Float/GraphQLISODateTime); custom: @Scalar("Date") implements CustomScalar
// Pagination (2.13a): @Query(() => UserConnection) usersConnection(@Args() { first, after })
// Sandbox (2.13b): plugins: [ApolloServerPluginLandingPageLocalDefault()]
// Federation (2.13c): @Directive('@key(fields: "id")'); ApolloFederationDriver
// Enum (Misol 8): registerEnumType(OrderStatus, { name: "OrderStatus" })

4. Batafsil kod namunalari

Misol 1 — GraphQLModule + tur (2.4, 2.5)

ts
// app.module.ts
@Module({
  imports: [
    GraphQLModule.forRootAsync<ApolloDriverConfig>({
      driver: ApolloDriver,
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        autoSchemaFile: "schema.gql",
        sortSchema: true,
        playground: config.get("app.env") !== "production",   // faqat dev (14)
        context: ({ req }) => ({ req }),               // auth uchun (2.11)
      }),
    }),
  ],
})
export class AppModule {}

// users/entities/user.entity.ts
@ObjectType()
export class User {
  @Field(() => ID) id: number;
  @Field() ism: string;
  @Field() email: string;
  @Field(() => Int, { nullable: true }) yosh?: number;
  @Field() faol: boolean;
  // parol — @Field yo'q (yashirin — 14)
  parol: string;
  @Field(() => [Order], { nullable: true }) buyurtmalar?: Order[];
}

Misol 2 — Resolver (Query + Mutation — 2.6, 2.7)

ts
@Resolver(() => User)
export class UsersResolver {
  constructor(private usersService: UsersService) {}

  @Query(() => [User], { name: "users" })
  hammasi() {
    return this.usersService.hammasi();
  }

  @Query(() => User, { name: "user", nullable: true })
  bitta(@Args("id", { type: () => Int }) id: number) {
    return this.usersService.bitta(id);
  }

  @Mutation(() => User)
  yaratUser(@Args("input") input: CreateUserInput) {
    return this.usersService.yarat(input);
  }

  @Mutation(() => User)
  yangilaUser(
    @Args("id", { type: () => Int }) id: number,
    @Args("input") input: UpdateUserInput,
  ) {
    return this.usersService.yangila(id, input);
  }

  @Mutation(() => Boolean)
  ochirUser(@Args("id", { type: () => Int }) id: number) {
    return this.usersService.ochir(id).then(() => true);
  }
}

Misol 3 — InputType + validatsiya (2.8)

ts
@InputType()
export class CreateUserInput {
  @Field()
  @IsString() @MinLength(2)
  ism: string;

  @Field()
  @IsEmail()
  email: string;

  @Field()
  @MinLength(8) @Matches(/[A-Z]/, { message: "Katta harf kerak" })
  parol: string;

  @Field(() => Int, { nullable: true })
  @IsOptional() @IsInt() @Min(18)
  yosh?: number;
}

// PartialType (yangilash — 8.5)
import { PartialType } from "@nestjs/graphql";
@InputType()
export class UpdateUserInput extends PartialType(CreateUserInput) {}

// main.ts — ValidationPipe (GraphQL'da ham ishlaydi — 8.5)
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));

Misol 4 — Field resolver (bog'liqlik — 2.9)

ts
@Resolver(() => User)
export class UsersResolver {
  constructor(
    private usersService: UsersService,
    private ordersService: OrdersService,
  ) {}

  @Query(() => [User])
  users() { return this.usersService.hammasi(); }

  // buyurtmalar faqat so'ralganda (lazy — 2.9)
  @ResolveField(() => [Order])
  buyurtmalar(@Parent() user: User) {
    return this.ordersService.userBuyurtmalari(user.id);
  }

  // Hisoblangan maydon (DB'da yo'q)
  @ResolveField(() => Int)
  buyurtmalarSoni(@Parent() user: User) {
    return this.ordersService.soni(user.id);
  }
}
// query { users { ism buyurtmalar { summa } buyurtmalarSoni } }

Misol 5 — DataLoader (N+1 yechimi — 2.13)

ts
// loaders/orders.loader.ts
import DataLoader from "dataloader";

@Injectable({ scope: Scope.REQUEST })                // har so'rovga yangi (2.13)
export class OrdersDataLoader {
  constructor(private ordersService: OrdersService) {}

  public readonly batchOrders = new DataLoader<number, Order[]>(
    async (userIds: readonly number[]) => {
      // BITTA so'rov — barcha userId uchun (batch — 2.12 yechimi)
      const orders = await this.ordersService.userlarBuyurtmalari([...userIds]);
      // userId tartibida moslash (DataLoader talab)
      const map = new Map<number, Order[]>();
      orders.forEach((o) => {
        if (!map.has(o.userId)) map.set(o.userId, []);
        map.get(o.userId).push(o);
      });
      return userIds.map((id) => map.get(id) || []);
    },
  );
}

// Resolver — DataLoader bilan
@ResolveField(() => [Order])
buyurtmalar(@Parent() user: User, @Context() { loaders }) {
  return loaders.ordersLoader.batchOrders.load(user.id);   // 1 so'rov (N+1 yo'q!)
}

Misol 6 — Subscription (real-time — 2.10)

ts
import { PubSub } from "graphql-subscriptions";
const pubSub = new PubSub();                          // prod: Redis PubSub (8.15)

@Resolver(() => Message)
export class ChatResolver {
  @Query(() => [Message])
  xabarlar() { return this.chatService.hammasi(); }

  @Mutation(() => Message)
  async xabarYubor(@Args("input") input: SendMessageInput) {
    const xabar = await this.chatService.saqla(input);
    await pubSub.publish("xabarQoshildi", { xabarQoshildi: xabar });   // e'lon (5.13)
    return xabar;
  }

  @Subscription(() => Message, {
    filter: (payload, variables) =>                  // faqat shu xona (5.13)
      payload.xabarQoshildi.xonaId === variables.xonaId,
  })
  xabarQoshildi(@Args("xonaId") xonaId: number) {
    return pubSub.asyncIterator("xabarQoshildi");
  }
}
// subscription { xabarQoshildi(xonaId: 1) { matn } }

Misol 7 — Guard va auth (2.11)

ts
// gql-auth.guard.ts
@Injectable()
export class GqlAuthGuard extends AuthGuard("jwt") {
  getRequest(context: ExecutionContext) {
    return GqlExecutionContext.create(context).getContext().req;   // GraphQL req
  }
}

// CurrentUser decorator (GraphQL)
export const CurrentUser = createParamDecorator(
  (data: unknown, context: ExecutionContext) =>
    GqlExecutionContext.create(context).getContext().req.user,
);

// Resolver
@Query(() => User)
@UseGuards(GqlAuthGuard)
me(@CurrentUser() user: User) {
  return this.usersService.bitta(user.id);
}

@Mutation(() => Order)
@UseGuards(GqlAuthGuard)
buyurtmaBer(@Args("input") input: CreateOrderInput, @CurrentUser() user: User) {
  return this.ordersService.yarat(user.id, input);
}

Misol 8 — Enum va interface (GraphQL turlar)

ts
import { registerEnumType } from "@nestjs/graphql";

// Enum (8.5, 7)
export enum OrderStatus {
  YANGI = "yangi",
  TAYYORLANMOQDA = "tayyorlanmoqda",
  YETKAZILDI = "yetkazildi",
}
registerEnumType(OrderStatus, { name: "OrderStatus" });   // GraphQL'ga ro'yxat

@ObjectType()
export class Order {
  @Field(() => ID) id: number;
  @Field(() => OrderStatus) holat: OrderStatus;       // enum maydon
  @Field(() => Float) summa: number;
}

// Filter argument (ArgsType)
@ArgsType()
export class OrdersFilter {
  @Field(() => OrderStatus, { nullable: true }) holat?: OrderStatus;
  @Field(() => Int, { defaultValue: 1 }) sahifa: number;
  @Field(() => Int, { defaultValue: 20 }) limit: number;
}

Misol 9 — Error handling GraphQL (2.11)

ts
// GraphQL'da xato — built-in exception ishlaydi (8.1)
@Query(() => User)
async user(@Args("id", { type: () => Int }) id: number) {
  const user = await this.usersService.bitta(id);
  if (!user) throw new NotFoundException("Foydalanuvchi topilmadi");   // GraphQL error
  return user;
}

// Custom GraphQL error formatlash (module)
GraphQLModule.forRoot({
  driver: ApolloDriver,
  formatError: (error) => ({                          // toza xato (14 — ichki yashirin)
    message: error.message,
    code: error.extensions?.code,
  }),
});

Misol 10 — Custom scalar (Date — 2.5a)

ts
// scalars/date.scalar.ts
import { Scalar, CustomScalar } from "@nestjs/graphql";
import { Kind, ValueNode } from "graphql";

@Scalar("Date", () => Date)
export class DateScalar implements CustomScalar<number, Date> {
  description = "Date scalar (millisekund timestamp)";

  serialize(value: Date): number {                    // server  mijoz
    return value instanceof Date ? value.getTime() : null;
  }
  parseValue(value: number): Date {                   // mijoz o'zgaruvchisi  server
    return new Date(value);
  }
  parseLiteral(ast: ValueNode): Date {                // query ichidagi literal  server
    return ast.kind === Kind.INT ? new Date(parseInt(ast.value, 10)) : null;
  }
}

// app.module.ts — provider sifatida ro'yxat
@Module({ providers: [DateScalar] })
export class AppModule {}

// Ishlatish — endi @Field(() => Date) millisekund timestamp bo'lib seriyalanadi
@ObjectType()
export class Buyurtma {
  @Field(() => ID) id: number;
  @Field(() => Date) yaratilgan: Date;                // custom scalar orqali
}
// Eslatma: oddiy holatda NestJS'ning tayyor GraphQLISODateTime yetarli (2.5a)

Misol 11 — Cursor pagination (Relay — 2.13a)

ts
// pagination/paginated.ts — generic connection (har tur uchun)
import { Type } from "@nestjs/common";
import { ObjectType, Field, Int } from "@nestjs/graphql";

@ObjectType()
export class PageInfo {
  @Field() hasNextPage: boolean;
  @Field({ nullable: true }) endCursor?: string;
}

// Generic factory — Paginated(User) => UserConnection
export function Paginated<T>(TClass: Type<T>): any {
  @ObjectType({ isAbstract: true })
  abstract class EdgeType {
    @Field() cursor: string;
    @Field(() => TClass) node: T;
  }
  @ObjectType({ isAbstract: true })
  abstract class PaginatedType {
    @Field(() => [EdgeType]) edges: EdgeType[];
    @Field(() => PageInfo) pageInfo: PageInfo;
    @Field(() => Int) totalCount: number;
  }
  return PaginatedType;
}

@ObjectType()
export class UserConnection extends Paginated(User) {}

// service — cursor mantiq (base64(id))
async sahifala(first: number, after?: string) {
  const afterId = after ? Number(Buffer.from(after, "base64").toString()) : 0;
  const rows = await this.repo.find({
    where: { id: MoreThan(afterId) },
    take: first + 1,                                   // 1 ta ortiq  keyingi bor-yo'qni bilish
    order: { id: "ASC" },
  });
  const hasNextPage = rows.length > first;
  const items = hasNextPage ? rows.slice(0, first) : rows;
  return {
    edges: items.map((u) => ({
      cursor: Buffer.from(String(u.id)).toString("base64"),
      node: u,
    })),
    pageInfo: {
      hasNextPage,
      endCursor: items.length
        ? Buffer.from(String(items[items.length - 1].id)).toString("base64")
        : null,
    },
    totalCount: await this.repo.count(),
  };
}

// resolver
@Query(() => UserConnection)
usersConnection(@Args() { first, after }: PaginationArgs) {
  return this.usersService.sahifala(first, after);
}
// query { usersConnection(first: 20) { edges { node { ism } cursor } pageInfo { hasNextPage endCursor } } }

Misol 12 — To'liq tuzilma (2.14)

text
  GraphQL modul tuzilishi:
  src/
  ├── app.module.ts          (GraphQLModule + Sandbox — Misol 1, 2.13b)
  ├── scalars/date.scalar.ts (custom scalar — Misol 10, 2.5a)
  ├── pagination/paginated.ts(generic connection — Misol 11, 2.13a)
  ├── users/
  │   ├── entities/user.entity.ts    (@ObjectType — Misol 1)
  │   ├── dto/create-user.input.ts   (@InputType — Misol 3)
  │   ├── users.resolver.ts          (@Resolver — Misol 2, 4)
  │   ├── users.service.ts           (biznes mantiq — 8.1)
  │   └── loaders/orders.loader.ts   (DataLoader — Misol 5)
  └── schema.gql             (avtomatik generatsiya — code-first)

   Resolver (GraphQL "controller") + service (mantiq) + entity (@ObjectType) + input (@InputType)
   REST controller'siz (yoki birga — GraphQL + REST hybrid)

5. To'g'ri va noto'g'ri holatlar

1) Field resolver DataLoader'siz

text
 N+1 (100 user  101 so'rov — 2.12)
 DataLoader (batch — 2.13)

2) parol @Field bilan

text
 parol GraphQL'da ko'rinadi (14)
 @Field yo'q (avtomatik yashirin — 2.5)

3) Mantiq resolver'da

text
 DB/biznes resolver'da (8.1: 2.7)
 service'da (resolver yupqa)

4) Playground production'da

text
 playground: true (prod — schema oshkor — 14)
 faqat dev (env tekshir — 2.4)

5) Validatsiyasiz input

text
 InputType validatsiyasiz (8.5)
 class-validator + ValidationPipe (DRY)

6. Keng tarqalgan xatolar va yechimlari

Xato 1 — Schema generatsiya bo'lmaydi

Sababi: @ObjectType/@Field yo'q, yoki autoSchemaFile (2.4, 2.5). Yechimi: dekoratorlar; autoSchemaFile yo'l.

Xato 2 — Cannot determine GraphQL type

Sababi: @Field tur aniq emas 2.5-bob. Yechimi: @Field(() => Int/[Order]) aniq tur.

Xato 3 — N+1 (sekin)

Sababi: field resolver DataLoader'siz 2.12-bob. Yechimi: DataLoader 2.13-bob.

Xato 4 — Guard ishlamaydi

Sababi: context HTTP (GraphQL boshqacha — 2.11). Yechimi: GqlExecutionContext; getRequest override.

Xato 5 — Validatsiya ishlamaydi

Sababi: ValidationPipe yo'q 2.8-bob. Yechimi: global ValidationPipe; @InputType.

Xato 6 — Subscription ulanmaydi

Sababi: subscription transport sozlanmagan. Yechimi: installSubscriptionHandlers / graphql-ws.

Xato 7 — Date cannot represent... (scalar)

Sababi: GraphQL'da standart Date yo'q (2.5a). Yechimi: @Field(() => GraphQLISODateTime) yoki custom scalar (Misol 10).

Xato 8 — Playground eskirgan/ochilmaydi

Sababi: Apollo Server 4+ eski Playground'ni olib tashladi (2.13b). Yechimi: ApolloServerPluginLandingPageLocalDefault (Apollo Sandbox).


7. Integratsiya — bu mavzu stack'ning qayerida uchraydi

  • REST 5.7-bob: muqobil API uslubi.
  • DTO/validatsiya 8.5-bob: InputType + class-validator.
  • DB (8.13, 8.3): resolver service DB.
  • Real-time 5.13-bob: subscriptions.
  • Auth 8.9-bob: GqlAuthGuard.
  • N+1 6.10-bob: DataLoader.
  • Frontend (11): Apollo Client (cursor pagination — 2.13a).
  • Cache 8.15-bob: GraphQL cache (qiyin).
  • Mikroservis 8.16-bob: Federation (subgraph + Apollo Gateway — 2.13c).

8. Eng yaxshi amaliyotlar (best practices)

  • Code-first (tur xavfsiz — 2.3, 2.4).
  • @ObjectType/@Field; parol @Field'siz (yashirin — 14, 2.5).
  • Resolver yupqa service (8.1: 2.7).
  • InputType + class-validator (DRY — 8.5, 2.8).
  • Field resolver (lazy — 2.9) + DataLoader (N+1 — 2.12, 2.13).
  • Subscription ehtiyotkorlik (resurs — Redis PubSub prod — 2.10).
  • Guard GqlExecutionContext (auth — 2.11).
  • Cursor pagination (katta ro'yxatga — Relay — 2.13a, Misol 11).
  • Custom scalar (Date/JSON — GraphQLISODateTime — 2.5a, Misol 10).
  • Apollo Sandbox faqat dev (14, 2.13b); query depth/complexity limit (DoS).
  • Federation (faqat ko'p-servisli katta tizim — 2.13c, 8.16).
  • formatError (ichki xato yashirin — 14, Misol 9).
  • REST bilan tanlov (har biri o'rni — 2.2).

9. Amaliy loyiha: "GraphQL API (NestJS)"

NestJS GraphQL'ni mustahkamlash.

Maqsad

NestJS'da to'liq GraphQL API: User/Order/Product — query, mutation, field resolver, DataLoader, subscription, auth.

Talablar (requirements)

  1. GraphQLModule: code-first, playground dev (Misol 1, 2.4).
  2. ObjectType: User/Order/Product (@Field, parol yashirin — Misol 1, 2.5).
  3. Resolver: Query (ro'yxat/bitta), Mutation (CRUD) — Misol 2, 2.6, 2.7.
  4. InputType: validatsiya (class-validator — Misol 3, 2.8).
  5. Field resolver: user.buyurtmalar, hisoblangan (Misol 4, 2.9).
  6. DataLoader: N+1 yechimi (Misol 5, 2.13).
  7. Subscription: real-time chat/bildirishnoma (Misol 6, 2.10).
  8. Auth: GqlAuthGuard, CurrentUser (Misol 7, 2.11).
  9. Enum + filter: OrderStatus, ArgsType (Misol 8).
  10. Error: NotFound, formatError (Misol 9, 14).
  11. Pagination: cursor/Relay usersConnection (Misol 11, 2.13a).
  12. Custom scalar: Date (yoki GraphQLISODateTime — Misol 10, 2.5a).
  13. Sandbox: Apollo Sandbox dev'da yoqilgan (2.13b).

Maslahatlar (hint)

  • @Field tur aniq (2.5, 2-xato).
  • DataLoader N+1 (2.13, 3-xato).
  • parol @Field'siz (14, 2-holat).
  • Guard GqlExecutionContext (2.11, 4-xato).
  • ValidationPipe + InputType (2.8, 5-xato).
  • Playground dev (14, 4-holat).

"Tayyor" mezonlari (acceptance criteria)

  • GraphQLModule (code-first).
  • ObjectType (parol yashirin).
  • Query + Mutation (CRUD).
  • InputType (validatsiya).
  • Field resolver.
  • DataLoader (N+1 yo'q).
  • Subscription.
  • Auth (GqlAuthGuard).
  • Enum + filter.
  • Error handling.
  • Cursor pagination (Relay connection).
  • Custom scalar (Date) yoki GraphQLISODateTime.
  • Apollo Sandbox (dev).

Yechim kodi ataylab berilmagan — bu loyihani o'zingiz yozib ko'ring.


10. Xulosa va keyingi bobga ko'prik

Bu bobda NestJS GraphQL'ni chuqur o'rgandik:

  • GraphQL (bitta endpoint, mijoz so'raydi — 2.1); REST vs GraphQL 2.2-bob; code-first vs schema-first (2.3, 2.4).
  • @ObjectType/@Field (tur — 2.5); scalar + custom scalar (2.5a); resolver (@Query/@Mutation — 2.6, 2.7); InputType/ArgsType (validatsiya — 2.8).
  • Field resolver (lazy bog'liqlik — 2.9); subscriptions (real-time — 2.10); guards/pipes (GqlExecutionContext — 2.11).
  • N+1 muammosi 2.12-bob + DataLoader (yechimi — 2.13); pagination (cursor/Relay — 2.13a); Apollo Sandbox (2.13b); federation (2.13c); best practices 2.14-bob.

Keyingi bob — 8.18-bob: WebSockets gateway va Task scheduling (cron). GraphQL'ni bildik; endi 8-QISM'ning so'nggi ikki amaliy mavzusini — NestJS'da WebSockets (gateway — 5.13 — real-time) va task scheduling (cron — 5.22 — rejalashtirilgan vazifalar) — ko'ramiz. Bu — chat, bildirishnoma (WebSocket) va davriy vazifalar (cron — hisobot, tozalash).


Foydalanilgan rasmiy/ishonchli manbalar

  • docs.nestjs.com/graphql/quick-start; /resolvers (code-first, @Resolver, @Query, @ObjectType)
  • docs.nestjs.com/graphql/subscriptions (PubSub, @Subscription)
  • docs.nestjs.com/graphql/dataloader; github.com/graphql/dataloader (N+1, batch, request-scoped)
  • docs.nestjs.com/graphql/other-features (guards, interceptors, GqlExecutionContext)
  • graphql.org/learn (Query/Mutation/Subscription, schema, scalar turlar)

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
8.17-bob: GraphQL — NestJS GraphQL — Wisar