Use Cases (अपडेट: 2/6/2026)

Claude Code से GraphQL विकास: SDL, typed resolvers, DataLoader और tests

Claude Code से GraphQL API बनाएं: SDL, typed resolvers, N+1, auth boundaries, complexity limit, errors और tests।

Claude Code से GraphQL विकास: SDL, typed resolvers, DataLoader और tests

Claude Code से GraphQL API बनवाते समय पहली चीज type names नहीं हैं। पहले यह तय करें कि public contract कहाँ रहेगा, authorization किस layer में होगा, N+1 कैसे रुकेगा, बहुत deep या expensive query कैसे reject होगी, और error shape तथा tests production readiness कैसे साबित करेंगे। अगर prompt सिर्फ “GraphQL API बना दो” है, तो demo चल सकता है, लेकिन production में slow queries, auth leak, type drift और unclear errors बच सकते हैं।

यह guide Claude Code को one-shot code generator नहीं, बल्कि implementation partner मानती है। SDL यानी schema definition language, GraphQL schema का text contract है। resolver वह function है जो field की value लाता है। DataLoader request के अंदर similar reads को batch करने का tool है, जिससे N+1 कम होता है।

Official references हमेशा check करें: GraphQL Schemas and Types, Pagination, Authorization, Security, specification Response Format, DataLoader README, GraphQL Code Generator TypeScript Resolvers, और Claude Code docs। Related guides: API development, API testing, prompt techniques

schema-first या resolver-first चुनें

Schema-first में पहले SDL लिखा जाता है और उसे public contract माना जाता है। फिर resolver और tests बनते हैं। Resolver-first में existing services, TypeScript functions या database access से शुरू करके schema बनाया जाता है। New public API, mobile API और multi-team API के लिए schema-first review करना आसान है। Existing REST या domain layer migration में resolver-first practical हो सकता है।

तीन common use cases हैं। SaaS dashboard में team, billing और audit log boundaries पहले दिखनी चाहिए। Mobile app API network calls कम करती है, पर pagination, operation limits और stable errors चाहिए। Internal analytics tool में existing services reuse हो सकते हैं, लेकिन tenant और sensitive data authorization business logic में रहना चाहिए।

Claude Code को design choice के साथ prompt दें।

Design this GraphQL API schema-first.
Requirements:
- Put SDL in src/graphql/schema.graphql
- Type TypeScript resolvers with GraphQL Code Generator
- Use cursor-based connections for list fields
- Keep authorization in repo/service functions, not scattered through resolvers
- Create DataLoader instances per request inside createContext
- Include depth and complexity validation, safe error formatting, and Vitest tests
- Report verification commands and remaining pitfalls after implementation

Server library अलग हो सकती है, लेकिन contract, authorization, demand control और tests explicit होने चाहिए।

SDL को contract बनाएं

नीचे product review API का छोटा SDL है। यह GraphQL pagination docs के connection model को follow करता है: edges, node, cursor, pageInfoProduct.reviews जैसा nested field useful है, पर N+1 और deep query का entry point भी है।

npm install graphql @graphql-tools/schema dataloader
npm install -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-resolvers vitest tsx typescript
# src/graphql/schema.graphql
interface Node {
  id: ID!
}

type Product implements Node {
  id: ID!
  title: String!
  isPublic: Boolean!
  averageRating: Float
  reviews(first: Int = 10, after: String): ReviewConnection!
  createdAt: String!
}

type Review implements Node {
  id: ID!
  product: Product!
  rating: Int!
  body: String!
  authorId: ID!
  createdAt: String!
}

type ProductEdge {
  node: Product!
  cursor: String!
}

type ProductConnection {
  edges: [ProductEdge!]!
  pageInfo: PageInfo!
}

type ReviewEdge {
  node: Review!
  cursor: String!
}

type ReviewConnection {
  edges: [ReviewEdge!]!
  pageInfo: PageInfo!
}

type PageInfo {
  hasNextPage: Boolean!
  endCursor: String
}

input CreateReviewInput {
  productId: ID!
  rating: Int!
  body: String!
}

type Query {
  product(id: ID!): Product
  products(first: Int = 10, after: String): ProductConnection!
}

type Mutation {
  createReview(input: CreateReviewInput!): Review!
}

GraphQL type पूरी validation नहीं है। rating: Int! सिर्फ integer बताता है, 1 से 5 की range नहीं। Text length, unsafe HTML और private product visibility service या resolver में validate होते हैं।

typed resolvers generate करें

TypeScript GraphQL में parent, args, context को any बनाना आसान है, लेकिन बाद में schema changes टूटते हैं। GraphQL Code Generator का typescript-resolvers SDL से resolver signatures generate करता है।

// codegen.ts
import type { CodegenConfig } from "@graphql-codegen/cli";

const config: CodegenConfig = {
  schema: "src/graphql/schema.graphql",
  generates: {
    "src/graphql/generated/resolvers-types.ts": {
      plugins: ["typescript", "typescript-resolvers"],
      config: {
        contextType: "../context#GraphQLContext",
        mappers: {
          Product: "../models#ProductRow",
          Review: "../models#ReviewRow",
        },
        useIndexSignature: true,
      },
    },
  },
};

export default config;
{
  "scripts": {
    "codegen": "graphql-codegen --config codegen.ts",
    "test:graphql": "npm run codegen && vitest run test/graphql.test.ts"
  }
}

mappers बताते हैं कि GraphQL type और database row अलग shape हैं। Schema का Product reviews और averageRating expose करता है, पर ProductRow में ये fields होना जरूरी नहीं।

DataLoader से N+1 रोकें

N+1 में list के बाद हर item के लिए अलग backend read चलता है। 10 products के बाद 10 review queries। DataLoader same request की .load() calls को batch करता है और keys के order में result देता है। इसका cache request-scoped है; Redis replacement या global cache नहीं।

// src/graphql/models.ts
export type Viewer = { id: string; role: "user" | "admin" } | null;

export type ProductRow = {
  id: string;
  title: string;
  ownerId: string;
  isPublic: boolean;
  createdAt: string;
};

export type ReviewRow = {
  id: string;
  productId: string;
  rating: number;
  body: string;
  authorId: string;
  createdAt: string;
};

const products: ProductRow[] = [
  { id: "prod_1", title: "Claude Code Playbook", ownerId: "user_1", isPublic: true, createdAt: "2026-06-02T00:00:00.000Z" },
  { id: "prod_private", title: "Internal Prompt Pack", ownerId: "user_2", isPublic: false, createdAt: "2026-06-02T00:00:00.000Z" },
];

const reviews: ReviewRow[] = [
  { id: "rev_1", productId: "prod_1", rating: 5, body: "Practical.", authorId: "user_1", createdAt: "2026-06-02T00:10:00.000Z" },
  { id: "rev_2", productId: "prod_1", rating: 4, body: "Useful for reviews.", authorId: "user_2", createdAt: "2026-06-02T00:20:00.000Z" },
];

function canSeeProduct(viewer: Viewer, product: ProductRow) {
  return product.isPublic || viewer?.role === "admin" || viewer?.id === product.ownerId;
}

export const repo = {
  productById(id: string, viewer: Viewer) {
    const product = products.find((item) => item.id === id);
    return product && canSeeProduct(viewer, product) ? product : null;
  },
  products(viewer: Viewer) {
    return products.filter((product) => canSeeProduct(viewer, product));
  },
  reviewsByProductIds(productIds: readonly string[], viewer: Viewer) {
    const visibleIds = new Set(
      products.filter((product) => productIds.includes(product.id) && canSeeProduct(viewer, product)).map((product) => product.id),
    );
    return reviews.filter((review) => visibleIds.has(review.productId));
  },
  createReview(input: { productId: string; rating: number; body: string }, viewer: NonNullable<Viewer>) {
    const product = products.find((item) => item.id === input.productId);
    if (!product || !canSeeProduct(viewer, product)) return null;
    if (input.rating < 1 || input.rating > 5) return null;

    const review: ReviewRow = {
      id: `rev_${reviews.length + 1}`,
      productId: input.productId,
      rating: input.rating,
      body: input.body.slice(0, 1000),
      authorId: viewer.id,
      createdAt: new Date().toISOString(),
    };
    reviews.push(review);
    return review;
  },
};
// src/graphql/context.ts
import { randomUUID } from "node:crypto";
import DataLoader from "dataloader";
import { repo, type ReviewRow, type Viewer } from "./models";

function createLoaders(viewer: Viewer) {
  return {
    reviewsByProductId: new DataLoader<string, ReviewRow[]>(async (productIds) => {
      const rows = repo.reviewsByProductIds(productIds, viewer);
      const grouped = new Map<string, ReviewRow[]>();

      for (const productId of productIds) grouped.set(productId, []);
      for (const review of rows) grouped.get(review.productId)?.push(review);

      return productIds.map((productId) => grouped.get(productId) ?? []);
    }),
  };
}

export type GraphQLContext = {
  viewer: Viewer;
  requestId: string;
  loaders: ReturnType<typeof createLoaders>;
};

export function createContext(options: { viewer?: Viewer; requestId?: string } = {}): GraphQLContext {
  const viewer = options.viewer ?? null;
  return {
    viewer,
    requestId: options.requestId ?? randomUUID(),
    loaders: createLoaders(viewer),
  };
}
// src/graphql/resolvers.ts
import { GraphQLError } from "graphql";
import type { Resolvers } from "./generated/resolvers-types";
import { repo, type ReviewRow } from "./models";

function encodeCursor(id: string) {
  return Buffer.from(id).toString("base64url");
}

function decodeCursor(cursor?: string | null) {
  if (!cursor) return null;
  try {
    return Buffer.from(cursor, "base64url").toString("utf8");
  } catch {
    return null;
  }
}

function connectionFromRows<T extends { id: string }>(rows: T[], args: { first?: number | null; after?: string | null }) {
  const first = Math.min(Math.max(args.first ?? 10, 1), 50);
  const afterId = decodeCursor(args.after);
  const startIndex = afterId ? rows.findIndex((row) => row.id === afterId) + 1 : 0;
  const slice = rows.slice(Math.max(startIndex, 0), Math.max(startIndex, 0) + first + 1);
  const page = slice.slice(0, first);

  return {
    edges: page.map((node) => ({ node, cursor: encodeCursor(node.id) })),
    pageInfo: {
      hasNextPage: slice.length > first,
      endCursor: page.length > 0 ? encodeCursor(page[page.length - 1].id) : null,
    },
  };
}

export const resolvers: Resolvers = {
  Node: {
    __resolveType(parent) {
      return "rating" in parent ? "Review" : "Product";
    },
  },
  Query: {
    product: (_parent, args, context) => repo.productById(args.id, context.viewer),
    products: (_parent, args, context) => connectionFromRows(repo.products(context.viewer), args),
  },
  Product: {
    reviews: async (product, args, context) => {
      const rows = await context.loaders.reviewsByProductId.load(product.id);
      return connectionFromRows(rows, args);
    },
    averageRating: async (product, _args, context) => {
      const rows = await context.loaders.reviewsByProductId.load(product.id);
      if (rows.length === 0) return null;
      return rows.reduce((sum: number, review: ReviewRow) => sum + review.rating, 0) / rows.length;
    },
  },
  Review: {
    product: (review, _args, context) => {
      const product = repo.productById(review.productId, context.viewer);
      if (!product) {
        throw new GraphQLError("Product is not available.", { extensions: { code: "FORBIDDEN" } });
      }
      return product;
    },
  },
  Mutation: {
    createReview: (_parent, args, context) => {
      if (!context.viewer) {
        throw new GraphQLError("Login required.", { extensions: { code: "UNAUTHENTICATED" } });
      }

      const review = repo.createReview(args.input, context.viewer);
      if (!review) {
        throw new GraphQLError("Review could not be created.", { extensions: { code: "BAD_USER_INPUT" } });
      }
      return review;
    },
  },
};

Authorization repo layer में है। GraphQL docs भी production code में business logic layer को single source of truth बनाने की सलाह देते हैं।

depth और complexity limit करें

GraphQL flexible है, लेकिन deep nesting, बड़ा first, aliases और batch requests expensive हो सकते हैं। Security docs trusted documents, pagination, depth limiting, breadth/batch limiting, rate limiting और complexity analysis को साथ रखने की सलाह देते हैं।

// src/graphql/demandControl.ts
import {
  GraphQLError,
  Kind,
  type ASTVisitor,
  type FieldNode,
  type FragmentDefinitionNode,
  type SelectionSetNode,
  type ValidationContext,
} from "graphql";

type Limits = {
  maxDepth: number;
  maxComplexity: number;
  defaultListSize: number;
  maxListSize: number;
};

const defaultLimits: Limits = {
  maxDepth: 6,
  maxComplexity: 300,
  defaultListSize: 10,
  maxListSize: 50,
};

export function depthAndComplexityRule(limits: Partial<Limits> = {}) {
  const merged = { ...defaultLimits, ...limits };

  return (context: ValidationContext): ASTVisitor => {
    const fragments = new Map<string, FragmentDefinitionNode>();

    return {
      FragmentDefinition(node) {
        fragments.set(node.name.value, node);
      },
      OperationDefinition(node) {
        const score = scoreSelectionSet(node.selectionSet, fragments, new Set(), 0, 1, merged);

        if (score.depth > merged.maxDepth) {
          context.reportError(new GraphQLError(`GraphQL operation is too deep: ${score.depth}`, { nodes: node }));
        }
        if (score.complexity > merged.maxComplexity) {
          context.reportError(
            new GraphQLError(`GraphQL operation is too complex: ${score.complexity}`, { nodes: node }),
          );
        }
      },
    };
  };
}

function listMultiplier(field: FieldNode, limits: Limits) {
  const arg = field.arguments?.find((item) => item.name.value === "first" || item.name.value === "last");
  if (!arg) return 1;
  if (arg.value.kind !== Kind.INT) return limits.defaultListSize;
  return Math.min(Number.parseInt(arg.value.value, 10), limits.maxListSize);
}

function scoreSelectionSet(
  selectionSet: SelectionSetNode,
  fragments: Map<string, FragmentDefinitionNode>,
  seenFragments: Set<string>,
  depth: number,
  multiplier: number,
  limits: Limits,
) {
  let maxDepth = depth;
  let complexity = 0;

  for (const selection of selectionSet.selections) {
    if (selection.kind === Kind.FIELD) {
      const nextMultiplier = multiplier * listMultiplier(selection, limits);
      complexity += nextMultiplier;

      if (selection.selectionSet) {
        const child = scoreSelectionSet(selection.selectionSet, fragments, seenFragments, depth + 1, nextMultiplier, limits);
        maxDepth = Math.max(maxDepth, child.depth);
        complexity += child.complexity;
      }
    }

    if (selection.kind === Kind.INLINE_FRAGMENT) {
      const child = scoreSelectionSet(selection.selectionSet, fragments, seenFragments, depth, multiplier, limits);
      maxDepth = Math.max(maxDepth, child.depth);
      complexity += child.complexity;
    }

    if (selection.kind === Kind.FRAGMENT_SPREAD) {
      const fragmentName = selection.name.value;
      if (seenFragments.has(fragmentName)) continue;
      const fragment = fragments.get(fragmentName);
      if (!fragment) continue;

      seenFragments.add(fragmentName);
      const child = scoreSelectionSet(fragment.selectionSet, fragments, seenFragments, depth, multiplier, limits);
      maxDepth = Math.max(maxDepth, child.depth);
      complexity += child.complexity;
    }
  }

  return { depth: maxDepth, complexity };
}

यह rule literal first और last पढ़ता है। Variables के लिए resolver में clamp जरूरी है। First-party clients में trusted documents और भी मजबूत विकल्प हो सकते हैं।

errors और tests standardize करें

GraphQL response में data, errors, extensions होते हैं। Specification request errors और field errors अलग करती है। Production में DB exception या stack trace न लौटाएं; extensions.code और requestId दें।

// src/graphql/schema.ts
import { readFileSync } from "node:fs";
import { makeExecutableSchema } from "@graphql-tools/schema";
import { resolvers } from "./resolvers";

export const schema = makeExecutableSchema({
  typeDefs: readFileSync(new URL("./schema.graphql", import.meta.url), "utf8"),
  resolvers,
});
// src/graphql/execute.ts
import {
  GraphQLError,
  execute,
  parse,
  specifiedRules,
  validate,
  type ExecutionResult,
} from "graphql";
import { createContext, type GraphQLContext } from "./context";
import { depthAndComplexityRule } from "./demandControl";
import { schema } from "./schema";
import type { Viewer } from "./models";

type ExecuteOptions = {
  source: string;
  variables?: Record<string, unknown>;
  viewer?: Viewer;
  requestId?: string;
};

export async function executeGraphQL(options: ExecuteOptions): Promise<ExecutionResult> {
  const context = createContext({ viewer: options.viewer, requestId: options.requestId });
  const document = parse(options.source);
  const validationErrors = validate(schema, document, [
    ...specifiedRules,
    depthAndComplexityRule({ maxDepth: 6, maxComplexity: 300 }),
  ]);

  if (validationErrors.length > 0) {
    return { errors: validationErrors.map((error) => maskError(error, context)) };
  }

  const result = await execute({
    schema,
    document,
    variableValues: options.variables,
    contextValue: context,
  });

  return {
    data: result.data,
    errors: result.errors?.map((error) => maskError(error, context)),
  };
}

function maskError(error: GraphQLError, context: GraphQLContext) {
  const code = typeof error.extensions.code === "string" ? error.extensions.code : "INTERNAL_SERVER_ERROR";
  const message = process.env.NODE_ENV === "production" ? "GraphQL request failed." : error.message;

  return new GraphQLError(message, {
    nodes: error.nodes,
    path: error.path,
    originalError: error.originalError,
    extensions: {
      code,
      requestId: context.requestId,
    },
  });
}
// test/graphql.test.ts
import { describe, expect, it } from "vitest";
import { executeGraphQL } from "../src/graphql/execute";

const viewer = { id: "user_1", role: "user" as const };

describe("GraphQL API", () => {
  it("returns products with reviews through typed resolvers", async () => {
    const result = await executeGraphQL({
      viewer,
      source: `
        query Products {
          products(first: 10) {
            edges {
              node {
                id
                title
                averageRating
                reviews(first: 5) {
                  edges { node { id rating body } }
                }
              }
            }
          }
        }
      `,
    });

    expect(result.errors).toBeUndefined();
    expect(result.data?.products.edges[0].node.title).toBe("Claude Code Playbook");
  });

  it("does not expose a private product to another user", async () => {
    const result = await executeGraphQL({
      viewer,
      source: `query PrivateProduct { product(id: "prod_private") { id title } }`,
    });

    expect(result.errors).toBeUndefined();
    expect(result.data?.product).toBeNull();
  });

  it("requires login for mutations", async () => {
    const result = await executeGraphQL({
      source: `
        mutation CreateReview {
          createReview(input: { productId: "prod_1", rating: 5, body: "Great" }) { id }
        }
      `,
    });

    expect(result.errors?.[0].extensions?.code).toBe("UNAUTHENTICATED");
  });

  it("rejects overly deep operations before execution", async () => {
    const result = await executeGraphQL({
      viewer,
      source: `
        query TooDeep {
          products(first: 50) {
            edges { node { reviews(first: 50) { edges { node { product { reviews(first: 50) { edges { node { id } } } } } } } } }
          }
        }
      `,
    });

    expect(result.errors?.[0].message).toContain("too");
  });
});

Tests में happy path के अलावा unauthenticated mutation, private data, invalid input, deep query, pagination cap और extensions.code रखें।

Safe Claude Code prompts

Good prompt scope, rules और acceptance criteria देता है।

Improve the GraphQL implementation in this repository.
Allowed files:
- src/graphql/**
- test/graphql.test.ts

Design rules:
- Schema-first; schema.graphql is the public contract
- Use GraphQL Code Generator's Resolvers type; do not add any `any` types
- Keep authorization in repo/service functions, not scattered across resolvers
- Create DataLoader instances inside createContext for each request
- Clamp list fields to first between 1 and 50 and return connection objects
- Add depth and complexity validation
- Shape errors as message, path, extensions.code, extensions.requestId
- Add Vitest coverage for happy path, unauthenticated mutation, private data, and overly deep queries

At the end, report changed files, commands run, remaining risks, and official docs to re-check.

Team में ये rules CLAUDE.md best practices में रख दें।

Release से पहले pitfalls

Global DataLoader user cache mix कर सकता है। SDL default first को सच में limit नहीं करता। Resolver में scattered authorization drift हो जाता है। Depth limit बिना complexity wide queries नहीं रोकता। Raw internal errors tables या stack traces leak करते हैं। CI में Codegen न हो तो schema और resolver types अलग हो जाते हैं।

GraphQL quality revenue path भी बचाती है: paid content, templates, Gumroad links, B2B forms और admin dashboards reliable authorization और predictable API cost पर depend करते हैं। पहले free checklist से Claude Code review flow fix करें, reusable prompts के लिए products/templates देखें, और team-level schema, auth, tests, CI के लिए Claude Code training/consultation लें।

Hands-on result

Masa ने छोटे product review API पर यह flow test किया। सबसे बड़ा gain DataLoader snippet नहीं था; gain था कि initial prompt में authorization boundary, first cap, depth/complexity, error shape और test names पहले से थे। पहले Product.reviews बनता था और N+1 बाद में दिखता था। अब schema, resolvers, validation और tests एक साथ review हुए।

Summary

Claude Code के साथ GraphQL बनाते समय schema-first या resolver-first जानबूझकर चुनें, SDL को contract रखें, typed resolvers generate करें, DataLoader request scope में बनाएं, authorization business logic में रखें, और depth तथा complexity limit करें। Stable errors और failure-path tests जोड़ें। GraphQL flexible है; production के लिए prompt में clear guardrails चाहिए।

#Claude Code #GraphQL #API development #TypeScript #DataLoader
मुफ़्त

मुफ़्त PDF: Claude Code cheatsheet

Email डालें और commands, review habits तथा safe workflow वाली एक-page PDF पाएँ.

हम आपका data सुरक्षित रखते हैं और spam नहीं भेजते.

Masa

लेखक के बारे में

Masa

Claude Code workflow और team adoption पर काम करने वाला engineer.