Claude Skill

api-graphql-yoga

GraphQL Yoga v5 server, Envelop plugins, subscriptions, error masking

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download agents-inc-skills-dist_plugins_api-graphql-yoga_skills_api-graphql-yoga-3a51ef5.zip · 17 KB
Part of agents-inc/skills — 130 skills

Install

skills CLI npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/api-graphql-yoga/skills/api-graphql-yoga
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
Git git clone https://github.com/agents-inc/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

GraphQL Yoga Patterns

Quick Guide: Use createYoga + createSchema for a Fetch API-compatible GraphQL server that runs on any JS runtime. Yoga v5 uses Envelop for plugin composition, SSE for subscriptions by default, built-in error masking, and CORS out of the box. Import GraphQLError from graphql (not graphql-yoga) for intentional client-facing errors. Prefer Yoga-specific plugins over Envelop equivalents for HTTP-level optimizations.


<critical_requirements>

CRITICAL: Before Using This Skill

All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)

(You MUST import GraphQLError from 'graphql', NOT from 'graphql-yoga' -- it is the standard graphql-js export)

(You MUST prefer Yoga-specific plugins over Envelop equivalents -- Yoga plugins operate at the HTTP layer and can skip GraphQL execution entirely for cached/persisted results)

(You MUST use createSchema from 'graphql-yoga' for schema-first -- passing raw typeDefs/resolvers objects directly to createYoga is not supported in v5)

(You MUST use named constants for all numeric values -- timeouts, TTLs, port numbers, limits)

</critical_requirements>


Auto-detection: GraphQL Yoga, graphql-yoga, createYoga, createSchema, createPubSub, Envelop, useResponseCache, useCSRFPrevention, usePersistedOperations, GraphQL subscriptions SSE, error masking, maskedErrors, graphql-ws, Yoga plugin hooks, onRequest, onParams

When to use:

  • Building a GraphQL server that needs to run on Node.js, Bun, Deno, or Cloudflare Workers
  • APIs requiring subscriptions via SSE (default) or WebSocket
  • Extending GraphQL execution with Envelop plugins (caching, auth, logging)
  • File uploads using the GraphQL Multipart Request spec
  • Production APIs needing error masking, CORS, and CSRF protection

When NOT to use:

  • REST-only APIs without GraphQL needs
  • Simple CRUD where a framework's built-in route handlers suffice
  • When you need a federated gateway (consider a dedicated gateway solution)

Key patterns covered:

  • Server setup with createYoga and createSchema (schema-first)
  • Type-safe context with generics on createYoga<ServerContext>
  • Envelop plugin system: lifecycle hooks, custom plugins, Yoga-specific plugins
  • Subscriptions: SSE (default), WebSocket via graphql-ws, built-in PubSub
  • Error masking and intentional GraphQLError exposure
  • File uploads with WHATWG File scalar
  • Production hardening: CORS, CSRF prevention, GraphQL Armor, logging
  • Cross-runtime deployment: Node.js, Bun, Deno, Cloudflare Workers

Detailed Resources:




<decision_framework>

Decision Framework

Schema Approach

Need auto-generated types from SDL?
+-- YES --> createSchema (schema-first with typeDefs + resolvers)
+-- NO  --> Want full TypeScript inference in schema definition?
    +-- YES --> Code-first library (e.g. Pothos) -- pass resulting GraphQLSchema to Yoga
    +-- NO  --> Vanilla graphql-js GraphQLSchema

Subscription Transport

Need subscriptions?
+-- YES --> Do clients need bidirectional communication?
|   +-- YES --> WebSocket via graphql-ws (add ws + graphql-ws packages)
|   +-- NO  --> SSE (default, zero config, works through proxies)
+-- NO  --> No subscription setup needed

Plugin Selection

Feature available as Yoga-specific plugin?
+-- YES --> Use Yoga plugin (HTTP-level hooks, can skip execution)
+-- NO  --> Use Envelop plugin (GraphQL execution-level hooks)

Yoga-Specific Plugins (Prefer Over Envelop)

Plugin Package Why Yoga-specific
Response Cache @graphql-yoga/plugin-response-cache Skips execution for cached queries
Persisted Operations @graphql-yoga/plugin-persisted-operations Rejects unknown operations at HTTP layer
Defer/Stream @graphql-yoga/plugin-defer-stream Streams via HTTP chunked encoding
CSRF Prevention @graphql-yoga/plugin-csrf-prevention Requires custom header before parsing
GraphQL SSE @graphql-yoga/plugin-graphql-sse Single-connection SSE mode

</decision_framework>


<red_flags>

RED FLAGS

High Priority:

  • Importing GraphQLError from graphql-yoga instead of graphql -- wrong package, will fail
  • Passing typeDefs/resolvers object directly to createYoga without createSchema -- not supported in v5
  • Using an Envelop plugin when a Yoga-specific equivalent exists -- misses HTTP-level optimizations (the Yoga response cache skips parsing entirely; the Envelop equivalent cannot)
  • Throwing plain Error in resolvers expecting clients to see the message -- masked to "Unexpected error." in production

Medium Priority:

  • Not configuring CORS origins for production -- default is *, which should be locked down
  • Using in-memory PubSub across multiple server instances -- events won't propagate (use Redis-backed createRedisEventTarget)
  • Missing graphql peer dependency -- graphql-yoga requires graphql as a peer, install both
  • Calling createSchema with no schema at all -- Yoga requires a schema; it does not infer one

Gotchas & Edge Cases:

  • YogaInitialContext.request is a Fetch API Request, not a Node.js IncomingMessage -- use request.headers.get(), not req.headers
  • Plugin execution order changed in v5 -- plugins added via addPlugin in onPluginInit now execute immediately after the adding plugin, not last
  • useResponseCache session callback must return a string (user ID) for PRIVATE scope or null for public -- returning undefined breaks caching
  • SSE subscriptions go through HTTP (text/event-stream) -- some proxies may buffer events; set X-Accel-Buffering: no for Nginx
  • File scalar in uploads gives you a WHATWG File object -- use .text(), .arrayBuffer(), or .stream() methods (not Node.js Buffer directly)
  • Yoga's built-in GraphiQL is enabled by default -- disable with graphiql: false in production
  • maskedErrors set to false disables ALL masking including stack traces -- use custom maskError function instead for selective exposure
  • CORS credentials: true with origin: '*' is rejected by browsers per the Fetch spec -- specify exact origins

</red_flags>


<critical_reminders>

CRITICAL REMINDERS

All code must follow project conventions in CLAUDE.md

(You MUST import GraphQLError from 'graphql', NOT from 'graphql-yoga' -- it is the standard graphql-js export)

(You MUST prefer Yoga-specific plugins over Envelop equivalents -- Yoga plugins operate at the HTTP layer and can skip GraphQL execution entirely for cached/persisted results)

(You MUST use createSchema from 'graphql-yoga' for schema-first -- passing raw typeDefs/resolvers objects directly to createYoga is not supported in v5)

(You MUST use named constants for all numeric values -- timeouts, TTLs, port numbers, limits)

Failure to follow these rules will cause import errors, missed performance optimizations, and information leakage through unmasked errors.

</critical_reminders>

Files (skills)
  • examples
    • core.md 5.2 KB
      # Core Patterns
      
      > Server setup, schema definition, type-safe context, cross-runtime deployment, file uploads. Referenced from [SKILL.md](../SKILL.md).
      
      ---
      
      ## Pattern 1: Basic Server Setup (Node.js)
      
      ```typescript
      import { createYoga, createSchema } from "graphql-yoga";
      import { createServer } from "node:http";
      
      const PORT = 4000;
      
      const schema = createSchema({
        typeDefs: /* GraphQL */ `
          type Query {
            greeting(name: String!): String!
            health: Boolean!
          }
        `,
        resolvers: {
          Query: {
            greeting: (_, { name }: { name: string }) => `Hello, ${name}!`,
            health: () => true,
          },
        },
      });
      
      const yoga = createYoga({ schema });
      const server = createServer(yoga);
      
      server.listen(PORT, () => {
        console.info(`Server running on http://localhost:${PORT}/graphql`);
      });
      
      export { yoga };
      ```
      
      **Why good:** `createSchema` wraps `makeExecutableSchema`, yoga is exported for testing, schema uses SDL with typed resolver args
      
      ```typescript
      // Bad: passing typeDefs/resolvers directly (not supported in v5)
      const yoga = createYoga({
        typeDefs: `type Query { hello: String }`,
        resolvers: { Query: { hello: () => "world" } },
      });
      ```
      
      **Why bad:** v5 requires `createSchema` -- raw `typeDefs`/`resolvers` on `createYoga` config is not supported
      
      ---
      
      ## Pattern 2: Type-Safe Context
      
      Use the generic parameter on `createYoga` for server-specific context. The `context` factory receives `YogaInitialContext` and merges your return value into the resolver context.
      
      ```typescript
      import {
        createYoga,
        createSchema,
        type YogaInitialContext,
      } from "graphql-yoga";
      import { createServer } from "node:http";
      
      interface AppContext {
        user: { id: string; role: string } | null;
      }
      
      const yoga = createYoga<Record<string, never>>({
        schema: createSchema({
          typeDefs: /* GraphQL */ `
            type Query {
              me: String
            }
          `,
          resolvers: {
            Query: {
              me: (_, __, context: YogaInitialContext & AppContext) => {
                if (!context.user) return null;
                return context.user.id;
              },
            },
          },
        }),
        context: async ({ request }: YogaInitialContext): Promise<AppContext> => {
          const token = request.headers.get("authorization")?.replace("Bearer ", "");
          if (!token) return { user: null };
          // Verify token with your auth solution
          const user = await verifyToken(token);
          return { user };
        },
      });
      
      const server = createServer(yoga);
      ```
      
      **Why good:** generic flows to resolver context, uses standard Fetch `Request` API (not Node.js-specific `req`), context factory is async for DB/auth calls
      
      ```typescript
      // Bad: using Node.js req/res instead of Fetch API Request
      context: ({ req }) => {
        const token = req.headers.authorization; // Wrong: Node.js API
      };
      ```
      
      **Why bad:** `YogaInitialContext` provides `request` (Fetch API `Request`), not `req` (Node.js `IncomingMessage`) -- using `req` breaks on non-Node runtimes
      
      ---
      
      ## Pattern 3: Cross-Runtime Deployment
      
      Yoga's Fetch API core means the same schema code works on any runtime. Only the server bootstrap differs.
      
      #### Node.js
      
      ```typescript
      import { createServer } from "node:http";
      import { createYoga } from "graphql-yoga";
      
      const yoga = createYoga({ schema });
      const server = createServer(yoga);
      server.listen(4000);
      ```
      
      #### Bun
      
      ```typescript
      import { createYoga } from "graphql-yoga";
      
      const yoga = createYoga({ schema });
      Bun.serve({ fetch: yoga });
      ```
      
      #### Deno
      
      ```typescript
      import { createYoga } from "graphql-yoga";
      
      const yoga = createYoga({ schema });
      Deno.serve(yoga);
      ```
      
      #### Cloudflare Workers
      
      ```typescript
      import { createYoga } from "graphql-yoga";
      
      const yoga = createYoga({ schema });
      
      export default {
        fetch: yoga.fetch,
      };
      ```
      
      **Why good:** schema + resolvers + plugins are identical across runtimes, only the HTTP adapter changes
      
      ---
      
      ## Pattern 4: File Uploads
      
      Yoga supports the GraphQL Multipart Request spec. Use the `File` scalar to receive WHATWG `File` objects.
      
      ```typescript
      import { createYoga, createSchema } from "graphql-yoga";
      
      const MAX_FILE_NAME_LENGTH = 255;
      
      const schema = createSchema({
        typeDefs: /* GraphQL */ `
          scalar File
      
          type FileInfo {
            name: String!
            size: Int!
            type: String!
          }
      
          type Mutation {
            uploadFile(file: File!): FileInfo!
          }
      
          type Query {
            _empty: Boolean
          }
        `,
        resolvers: {
          Mutation: {
            uploadFile: async (_, { file }: { file: File }) => {
              const buffer = await file.arrayBuffer();
              // Save buffer to storage
              return {
                name: file.name.slice(0, MAX_FILE_NAME_LENGTH),
                size: buffer.byteLength,
                type: file.type,
              };
            },
          },
        },
      });
      
      const yoga = createYoga({ schema });
      ```
      
      **Why good:** uses standard WHATWG `File` API (same methods as browser: `.text()`, `.arrayBuffer()`, `.stream()`), no extra packages
      
      ```typescript
      // Bad: expecting Node.js stream/buffer
      uploadFile: async (_, { file }) => {
        const chunks = [];
        for await (const chunk of file) {
          chunks.push(chunk); // Wrong: File is not a Node.js stream
        }
      };
      ```
      
      **Why bad:** WHATWG `File` is not a Node.js readable stream -- use `.arrayBuffer()` or `.stream()` (web ReadableStream)
      
      #### Disabling Uploads
      
      ```typescript
      const yoga = createYoga({
        schema,
        multipart: false, // Reject all multipart requests
      });
      ```
      
    • error-handling.md 4 KB
      # Error Handling
      
      > Error masking, intentional GraphQLError, custom masking, development mode. Referenced from [SKILL.md](../SKILL.md).
      
      ---
      
      ## Pattern 1: Default Error Masking
      
      Yoga masks all unexpected errors by default. Clients see a generic "Unexpected error." message -- internals (DB errors, stack traces) never leak.
      
      ```typescript
      import { createYoga, createSchema } from "graphql-yoga";
      
      const schema = createSchema({
        typeDefs: /* GraphQL */ `
          type Query {
            secretData: String!
          }
        `,
        resolvers: {
          Query: {
            secretData: () => {
              // This error message will NOT reach the client
              throw new Error("Connection to database failed: password incorrect");
            },
          },
        },
      });
      
      // Client receives:
      // { "errors": [{ "message": "Unexpected error." }], "data": null }
      ```
      
      **Why good:** sensitive information (connection strings, passwords, internal paths) never reaches clients
      
      ---
      
      ## Pattern 2: Intentional Client-Facing Errors with GraphQLError
      
      Throw `GraphQLError` (imported from `graphql`, NOT `graphql-yoga`) to bypass masking and send structured errors to clients.
      
      ```typescript
      import { GraphQLError } from "graphql";
      
      const NOT_FOUND_CODE = "USER_NOT_FOUND";
      const FORBIDDEN_CODE = "FORBIDDEN";
      const VALIDATION_CODE = "VALIDATION_ERROR";
      
      // Simple error with code extension
      throw new GraphQLError("User not found", {
        extensions: { code: NOT_FOUND_CODE },
      });
      
      // Error with multiple extensions
      throw new GraphQLError("You do not have permission to access this resource", {
        extensions: {
          code: FORBIDDEN_CODE,
          requiredRole: "admin",
          currentRole: "viewer",
        },
      });
      
      // Validation error with field details
      throw new GraphQLError("Invalid input", {
        extensions: {
          code: VALIDATION_CODE,
          fields: {
            email: "Invalid email format",
            name: "Name is required",
          },
        },
      });
      ```
      
      **Why good:** `GraphQLError` from `graphql` bypasses masking, extensions carry machine-parseable error codes for client-side `switch` handling
      
      ```typescript
      // Bad: importing GraphQLError from wrong package
      import { GraphQLError } from "graphql-yoga"; // Wrong package
      
      // Bad: throwing plain Error expecting client to see message
      throw new Error("User not found"); // Will be masked to "Unexpected error."
      ```
      
      **Why bad:** `GraphQLError` must come from `graphql` package; plain `Error` is always masked in production
      
      ---
      
      ## Pattern 3: Custom Error Masking
      
      Override the default masking behavior for selective error exposure.
      
      ```typescript
      import { createYoga, maskError } from "graphql-yoga";
      
      const yoga = createYoga({
        schema,
        maskedErrors: {
          maskError(error, message, isDev) {
            // Let downstream service errors through
            if (error?.extensions?.code === "DOWNSTREAM_SERVICE_ERROR") {
              return error;
            }
            // Use default masking for everything else
            return maskError(error, message, isDev);
          },
        },
      });
      ```
      
      **Why good:** selective exposure without disabling masking entirely, `maskError` fallback preserves safe defaults
      
      ---
      
      ## Pattern 4: Development Mode
      
      Set `NODE_ENV=development` to see original error messages and stack traces in extensions -- without disabling masking in production.
      
      ```typescript
      // In development (NODE_ENV=development):
      // {
      //   "errors": [{
      //     "message": "Connection failed",
      //     "extensions": {
      //       "originalError": {
      //         "message": "Connection failed",
      //         "stack": "Error: Connection failed\n    at ..."
      //       }
      //     }
      //   }]
      // }
      
      // In production (NODE_ENV=production):
      // { "errors": [{ "message": "Unexpected error." }] }
      ```
      
      ---
      
      ## Pattern 5: Disabling Masking (Use Cautiously)
      
      ```typescript
      const yoga = createYoga({
        schema,
        maskedErrors: false, // ALL errors pass through, including stack traces
      });
      ```
      
      **When to use:** Internal-only APIs behind a VPN where all consumers are trusted. Never for public-facing APIs.
      
      **Why cautious:** `maskedErrors: false` exposes stack traces, database errors, and internal paths to any client -- use custom `maskError` for selective exposure instead.
      
    • plugins.md 6.6 KB
      # Envelop Plugin System
      
      > Plugin lifecycle hooks, custom plugins, Yoga-specific plugins. Referenced from [SKILL.md](../SKILL.md).
      
      ---
      
      ## Pattern 1: Plugin Lifecycle Hooks
      
      Yoga plugins have access to both HTTP-level and GraphQL execution-level hooks. HTTP hooks fire first and can short-circuit before any GraphQL processing.
      
      ```typescript
      import { createYoga, type Plugin } from "graphql-yoga";
      
      function useRequestTiming(): Plugin {
        return {
          // HTTP-level hooks (fire for ALL requests, including non-GraphQL)
          onRequest({ request }) {
            const start = performance.now();
            // Store timing data on the request for later
            (request as any).__startTime = start;
          },
          onResponse({ request, response }) {
            const start = (request as any).__startTime;
            if (start) {
              const duration = performance.now() - start;
              response.headers.set("X-Response-Time", `${duration.toFixed(2)}ms`);
            }
          },
      
          // GraphQL execution-level hooks
          onExecute({ args }) {
            const operationName = args.operationName ?? "anonymous";
            console.info(`Executing: ${operationName}`);
          },
        };
      }
      
      const yoga = createYoga({
        schema,
        plugins: [useRequestTiming()],
      });
      ```
      
      **Why good:** HTTP hooks can modify response headers, short-circuit with `endResponse`, or perform auth before parsing
      
      ---
      
      ## Pattern 2: Auth Plugin with HTTP Short-Circuit
      
      Use `onRequest` to reject unauthenticated requests before any GraphQL processing occurs.
      
      ```typescript
      import { createYoga, type Plugin } from "graphql-yoga";
      
      const UNAUTHORIZED_STATUS = 401;
      
      function useAuth(): Plugin {
        return {
          onRequest({ request, fetchAPI, endResponse }) {
            // Skip auth for GraphiQL and health checks
            const url = new URL(request.url);
            if (request.method === "GET" && url.searchParams.has("query") === false) {
              return;
            }
      
            const authHeader = request.headers.get("authorization");
            if (!authHeader) {
              endResponse(
                new fetchAPI.Response(JSON.stringify({ error: "Unauthorized" }), {
                  status: UNAUTHORIZED_STATUS,
                  headers: { "Content-Type": "application/json" },
                }),
              );
            }
          },
        };
      }
      ```
      
      **Why good:** `endResponse` short-circuits the entire pipeline -- no parsing, validation, or execution occurs for unauthenticated requests
      
      ---
      
      ## Pattern 3: All Available Hooks
      
      ```typescript
      import type { Plugin } from "graphql-yoga";
      
      function useAllHooks(): Plugin {
        return {
          // --- HTTP Layer ---
          onRequest({ request, fetchAPI, endResponse, url }) {
            // Fires for every HTTP request
            // Call endResponse(Response) to short-circuit
          },
          onResponse({ request, response, serverContext }) {
            // Fires after response is built, before sending
            // Modify response headers, log timing, etc.
          },
      
          // --- GraphQL Request Layer ---
          onRequestParse({ request, url, setRequestParser }) {
            // Before parsing GraphQL params from the HTTP request
          },
          onParams({ params, request, setParams, setResult }) {
            // After params are parsed (query, variables, operationName)
            // Call setResult() to skip execution entirely (e.g. cache hit)
          },
      
          // --- GraphQL Execution Layer ---
          onParse({ params, parseFn, setParseFn, setParsedDocument }) {
            // Before/after GraphQL document parsing
          },
          onValidate({ params, addValidationRule, setResult }) {
            // Before/after document validation
          },
          onContextBuilding({ context, extendContext }) {
            // Before context is finalized -- extend context here
          },
          onExecute({ args, setExecuteFn, setResultAndStopExecution }) {
            // Before query/mutation execution
            // Return { onExecuteDone } for post-execution hook
          },
          onSubscribe({ args, setSubscribeFn }) {
            // Before subscription initialization
            // Return { onSubscribeResult } for result handling
          },
      
          // --- Result Processing ---
          onExecutionResult({ result, setResult }) {
            // Called for each execution result
          },
          onResultProcess({
            result,
            request,
            acceptableMediaTypes,
            setResultProcessor,
          }) {
            // Before result is serialized to HTTP response
          },
      
          // --- Lifecycle ---
          onDispose() {
            // Server shutdown -- cleanup connections, flush logs
          },
        };
      }
      ```
      
      ---
      
      ## Pattern 4: Using Yoga-Specific Plugins
      
      Always prefer Yoga-specific plugins over Envelop equivalents. Yoga plugins hook into the HTTP layer and can skip the entire GraphQL execution pipeline.
      
      #### Response Caching
      
      ```typescript
      import { useResponseCache } from "@graphql-yoga/plugin-response-cache";
      
      const CACHE_TTL_MS = 5_000;
      const USER_TTL_MS = 1_000;
      
      const yoga = createYoga({
        schema,
        plugins: [
          useResponseCache({
            session: (request) => request.headers.get("authorization"),
            ttl: CACHE_TTL_MS,
            ttlPerType: {
              User: USER_TTL_MS,
            },
          }),
        ],
      });
      ```
      
      #### Persisted Operations
      
      ```typescript
      import { usePersistedOperations } from "@graphql-yoga/plugin-persisted-operations";
      
      const store: Record<string, string> = {
        "sha256:abc123": "query { me { id name } }",
        "sha256:def456": "mutation { logout }",
      };
      
      const yoga = createYoga({
        schema,
        plugins: [
          usePersistedOperations({
            getPersistedOperation(sha256Hash) {
              return store[sha256Hash] ?? null;
            },
          }),
        ],
      });
      ```
      
      #### CSRF Prevention
      
      ```typescript
      import { useCSRFPrevention } from "@graphql-yoga/plugin-csrf-prevention";
      
      const yoga = createYoga({
        schema,
        plugins: [
          useCSRFPrevention({
            requestHeaders: ["x-graphql-yoga-csrf"],
          }),
        ],
      });
      ```
      
      #### Defer/Stream
      
      ```typescript
      import { useDeferStream } from "@graphql-yoga/plugin-defer-stream";
      
      const yoga = createYoga({
        schema,
        plugins: [useDeferStream()],
      });
      ```
      
      ---
      
      ## Pattern 5: Combining Multiple Plugins
      
      Plugins execute in order. Place auth before caching to avoid caching unauthenticated responses.
      
      ```typescript
      import { createYoga } from "graphql-yoga";
      import { useResponseCache } from "@graphql-yoga/plugin-response-cache";
      import { useCSRFPrevention } from "@graphql-yoga/plugin-csrf-prevention";
      
      const CACHE_TTL_MS = 10_000;
      
      const yoga = createYoga({
        schema,
        plugins: [
          // 1. CSRF prevention (rejects requests without custom header)
          useCSRFPrevention({
            requestHeaders: ["x-graphql-yoga-csrf"],
          }),
          // 2. Auth (rejects unauthenticated requests)
          useAuth(),
          // 3. Caching (only caches authenticated, CSRF-safe requests)
          useResponseCache({
            session: (request) => request.headers.get("authorization"),
            ttl: CACHE_TTL_MS,
          }),
        ],
      });
      ```
      
      **Why good:** plugin order ensures security checks happen before caching, prevents caching error responses
      
    • production.md 6.2 KB
      # Production Hardening
      
      > CORS, CSRF prevention, response caching, security plugins, logging. Referenced from [SKILL.md](../SKILL.md).
      
      ---
      
      ## Pattern 1: CORS Configuration
      
      Yoga enables CORS with `Access-Control-Allow-Origin: *` by default. Lock to specific origins before production deployment.
      
      ```typescript
      import { createYoga } from "graphql-yoga";
      
      // Static origin list
      const yoga = createYoga({
        schema,
        cors: {
          origin: ["https://app.example.com", "https://admin.example.com"],
          credentials: true,
          allowedHeaders: ["Content-Type", "Authorization"],
          methods: ["POST"],
        },
      });
      ```
      
      #### Dynamic Origin
      
      ```typescript
      const ALLOWED_ORIGINS = new Set([
        "https://app.example.com",
        "https://admin.example.com",
      ]);
      
      const yoga = createYoga({
        schema,
        cors: (request) => {
          const origin = request.headers.get("origin") ?? "";
          return {
            origin: ALLOWED_ORIGINS.has(origin) ? origin : "",
            credentials: true,
            allowedHeaders: ["Content-Type", "Authorization"],
            methods: ["POST"],
          };
        },
      });
      ```
      
      **Why good:** dynamic function allows per-request origin validation from a Set (O(1) lookups), empty string for rejected origins
      
      #### Disabling CORS
      
      ```typescript
      const yoga = createYoga({
        schema,
        cors: false, // Remove all CORS headers
      });
      ```
      
      **When to use:** Same-origin only APIs where no cross-origin requests are expected.
      
      ---
      
      ## Pattern 2: CSRF Prevention
      
      Require a custom header on all requests to prevent cross-site request forgery.
      
      ```typescript
      import { useCSRFPrevention } from "@graphql-yoga/plugin-csrf-prevention";
      
      const yoga = createYoga({
        schema,
        plugins: [
          useCSRFPrevention({
            requestHeaders: ["x-graphql-yoga-csrf"], // Default header name
          }),
        ],
      });
      ```
      
      **How it works:** Custom headers cannot be sent by plain HTML forms or simple requests -- they require a CORS preflight. This transforms what would be a "simple" request into one that requires explicit CORS approval, preventing CSRF attacks from malicious forms.
      
      ---
      
      ## Pattern 3: Response Caching
      
      Use the Yoga-specific response cache plugin (not the Envelop equivalent) for HTTP-level caching that skips GraphQL execution entirely.
      
      ```typescript
      import {
        useResponseCache,
        createInMemoryCache,
      } from "@graphql-yoga/plugin-response-cache";
      
      const GLOBAL_TTL_MS = 5_000;
      const USER_TTL_MS = 1_000;
      
      const cache = createInMemoryCache();
      
      const yoga = createYoga({
        schema,
        plugins: [
          useResponseCache({
            session: (request) => request.headers.get("authorization"),
            ttl: GLOBAL_TTL_MS,
            ttlPerType: {
              User: USER_TTL_MS,
            },
            scopePerSchemaCoordinate: {
              "Query.me": "PRIVATE",
            },
            cache,
          }),
        ],
      });
      
      // Manual cache invalidation (e.g., after a mutation via webhook)
      cache.invalidate([{ typename: "User", id: "user-123" }]);
      ```
      
      **Key points:**
      
      - `session` must return a string (user ID) for PRIVATE-scoped queries, or `null` for public queries
      - `ttlPerType` overrides global TTL for specific types (lowest TTL wins when result contains multiple types)
      - `scopePerSchemaCoordinate` marks fields as PRIVATE (cached per session) or PUBLIC (shared across users)
      
      ---
      
      ## Pattern 4: Security with GraphQL Armor
      
      Use GraphQL Armor plugins to protect against query complexity attacks on public APIs.
      
      ```typescript
      import { createYoga } from "graphql-yoga";
      
      // Install: npm i @escape.tech/graphql-armor-cost-limit
      //          npm i @escape.tech/graphql-armor-max-tokens
      //          npm i @escape.tech/graphql-armor-max-depth
      //          npm i @escape.tech/graphql-armor-max-aliases
      //          npm i @escape.tech/graphql-armor-max-directives
      
      const MAX_QUERY_COST = 5000;
      const MAX_QUERY_TOKENS = 1000;
      const MAX_QUERY_DEPTH = 10;
      const MAX_QUERY_ALIASES = 15;
      const MAX_QUERY_DIRECTIVES = 50;
      
      const yoga = createYoga({
        schema,
        plugins: [
          costLimitPlugin({ maxCost: MAX_QUERY_COST }),
          maxTokensPlugin({ n: MAX_QUERY_TOKENS }),
          maxDepthPlugin({ n: MAX_QUERY_DEPTH }),
          maxAliasesPlugin({ n: MAX_QUERY_ALIASES }),
          maxDirectivesPlugin({ n: MAX_QUERY_DIRECTIVES }),
        ],
      });
      ```
      
      **When to use:** Public-facing APIs where untrusted clients can send arbitrary queries. For private APIs using persisted operations, these protections may be unnecessary.
      
      ---
      
      ## Pattern 5: Logging Configuration
      
      Yoga supports four log levels: `debug`, `info`, `warn`, `error`. Default is `info` (includes info, warn, error).
      
      ```typescript
      // Set log level
      const yoga = createYoga({
        schema,
        logging: "warn", // Only warnings and errors
      });
      
      // Custom logger integration
      const yoga = createYoga({
        schema,
        logging: {
          debug: (...args: unknown[]) => logger.debug(...args),
          info: (...args: unknown[]) => logger.info(...args),
          warn: (...args: unknown[]) => logger.warn(...args),
          error: (...args: unknown[]) => logger.error(...args),
        },
      });
      ```
      
      ---
      
      ## Pattern 6: Production Disable GraphiQL
      
      GraphiQL is enabled by default. Disable it in production.
      
      ```typescript
      const IS_PRODUCTION = process.env.NODE_ENV === "production";
      
      const yoga = createYoga({
        schema,
        graphiql: !IS_PRODUCTION,
      });
      ```
      
      ---
      
      ## Pattern 7: Complete Production Configuration
      
      ```typescript
      import { createYoga, createSchema } from "graphql-yoga";
      import { useResponseCache } from "@graphql-yoga/plugin-response-cache";
      import { useCSRFPrevention } from "@graphql-yoga/plugin-csrf-prevention";
      import { usePersistedOperations } from "@graphql-yoga/plugin-persisted-operations";
      
      const IS_PRODUCTION = process.env.NODE_ENV === "production";
      const CACHE_TTL_MS = 10_000;
      const PORT = 4000;
      
      const yoga = createYoga({
        schema,
        // Disable GraphiQL in production
        graphiql: !IS_PRODUCTION,
        // Lock CORS to known origins
        cors: {
          origin: IS_PRODUCTION
            ? ["https://app.example.com"]
            : ["http://localhost:3000"],
          credentials: true,
          methods: ["POST"],
        },
        plugins: [
          // CSRF protection
          useCSRFPrevention({
            requestHeaders: ["x-graphql-yoga-csrf"],
          }),
          // Response caching
          useResponseCache({
            session: (request) => request.headers.get("authorization"),
            ttl: CACHE_TTL_MS,
          }),
        ],
      });
      ```
      
      **Why good:** defense in depth (CORS + CSRF + caching), GraphiQL off in production, environment-aware configuration
      
    • subscriptions.md 6.7 KB
      # Subscriptions
      
      > SSE (default), WebSocket via graphql-ws, built-in PubSub, filtering. Referenced from [SKILL.md](../SKILL.md).
      
      ---
      
      ## Pattern 1: SSE Subscriptions (Default)
      
      Yoga uses Server-Sent Events (SSE) by default -- no WebSocket infrastructure needed. Use `AsyncGenerator` syntax in subscription resolvers.
      
      ```typescript
      import { createYoga, createSchema } from "graphql-yoga";
      
      const TICK_INTERVAL_MS = 1_000;
      
      const schema = createSchema({
        typeDefs: /* GraphQL */ `
          type Query {
            _empty: Boolean
          }
          type Subscription {
            countdown(from: Int!): Int!
          }
        `,
        resolvers: {
          Subscription: {
            countdown: {
              subscribe: async function* (_, { from }: { from: number }) {
                for (let i = from; i >= 0; i--) {
                  await new Promise((resolve) =>
                    setTimeout(resolve, TICK_INTERVAL_MS),
                  );
                  yield { countdown: i };
                }
              },
            },
          },
        },
      });
      
      const yoga = createYoga({ schema });
      ```
      
      **Why good:** zero setup, works through HTTP proxies and load balancers, standard `text/event-stream` format
      
      ---
      
      ## Pattern 2: Built-in PubSub
      
      Use `createPubSub` for type-safe publish/subscribe between mutations and subscriptions. Ideal for single-instance servers.
      
      ```typescript
      import { createYoga, createSchema, createPubSub } from "graphql-yoga";
      
      // Type-safe topic definitions
      const pubSub = createPubSub<{
        newMessage: [payload: { id: string; text: string; author: string }];
        userJoined: [payload: { userId: string; name: string }];
      }>();
      
      const schema = createSchema({
        typeDefs: /* GraphQL */ `
          type Message {
            id: ID!
            text: String!
            author: String!
          }
      
          type Subscription {
            newMessage: Message!
          }
      
          type Mutation {
            sendMessage(text: String!, author: String!): Message!
          }
      
          type Query {
            _empty: Boolean
          }
        `,
        resolvers: {
          Subscription: {
            newMessage: {
              subscribe: () => pubSub.subscribe("newMessage"),
              resolve: (payload: { id: string; text: string; author: string }) =>
                payload,
            },
          },
          Mutation: {
            sendMessage: (_, { text, author }: { text: string; author: string }) => {
              const message = { id: crypto.randomUUID(), text, author };
              pubSub.publish("newMessage", message);
              return message;
            },
          },
        },
      });
      ```
      
      **Why good:** type-safe topics prevent publishing wrong payload shape, zero external dependencies for single-instance
      
      ---
      
      ## Pattern 3: PubSub with Filtering
      
      Use `pipe`, `filter`, and `map` from `graphql-yoga` to transform and filter subscription events.
      
      ```typescript
      import { createPubSub, createSchema, pipe, filter, map } from "graphql-yoga";
      
      const pubSub = createPubSub<{
        notification: [
          payload: { userId: string; message: string; priority: string },
        ];
      }>();
      
      const schema = createSchema({
        typeDefs: /* GraphQL */ `
          type Notification {
            message: String!
            priority: String!
          }
      
          type Subscription {
            notifications(userId: ID!, minPriority: String): Notification!
          }
      
          type Query {
            _empty: Boolean
          }
        `,
        resolvers: {
          Subscription: {
            notifications: {
              subscribe: (
                _,
                { userId, minPriority }: { userId: string; minPriority?: string },
              ) =>
                pipe(
                  pubSub.subscribe("notification"),
                  // Only deliver notifications for this user
                  filter((event) => event.userId === userId),
                  // Optionally filter by priority
                  filter((event) =>
                    minPriority ? event.priority >= minPriority : true,
                  ),
                  // Transform to subscription payload shape
                  map((event) => ({
                    notifications: {
                      message: event.message,
                      priority: event.priority,
                    },
                  })),
                ),
            },
          },
        },
      });
      ```
      
      **Why good:** `pipe`/`filter`/`map` compose declaratively, server-side filtering prevents sending unnecessary events to clients
      
      ---
      
      ## Pattern 4: WebSocket Subscriptions (graphql-ws)
      
      For bidirectional communication, use `graphql-ws` with the `ws` package. Required when clients need WebSocket transport.
      
      ```typescript
      import { createYoga, createSchema } from "graphql-yoga";
      import { createServer } from "node:http";
      import { useServer } from "graphql-ws/use/ws";
      import { WebSocketServer } from "ws";
      
      const PORT = 4000;
      
      const yoga = createYoga({
        schema,
        graphiql: {
          subscriptionsProtocol: "WS", // Tell GraphiQL to use WebSocket
        },
      });
      
      const httpServer = createServer(yoga);
      
      const wsServer = new WebSocketServer({
        server: httpServer,
        path: yoga.graphqlEndpoint,
      });
      
      useServer(
        {
          execute: (args: any) => args.rootValue.execute(args),
          subscribe: (args: any) => args.rootValue.subscribe(args),
          onSubscribe: async (ctx, _id, params) => {
            const { schema, execute, subscribe, contextFactory, parse, validate } =
              yoga.getEnveloped({
                ...ctx,
                req: ctx.extra.request,
                socket: ctx.extra.socket,
                params,
              });
      
            const args = {
              schema,
              operationName: params.operationName,
              document: parse(params.query),
              variableValues: params.variables,
              contextValue: await contextFactory(),
              rootValue: { execute, subscribe },
            };
      
            const errors = validate(args.schema, args.document);
            if (errors.length) return errors;
            return args;
          },
        },
        wsServer,
      );
      
      httpServer.listen(PORT, () => {
        console.info(`Server running on http://localhost:${PORT}/graphql`);
        console.info(`WebSocket subscriptions on ws://localhost:${PORT}/graphql`);
      });
      ```
      
      **Why good:** `yoga.getEnveloped()` ensures all Envelop plugins (auth, logging) run for WebSocket subscriptions too, GraphiQL configured to use WS
      
      ---
      
      ## Pattern 5: Distributed PubSub with Redis
      
      For multi-instance deployments, use Redis-backed event targets so events propagate across all server instances.
      
      ```typescript
      import { createPubSub } from "graphql-yoga";
      import { Redis } from "ioredis";
      import { createRedisEventTarget } from "@graphql-yoga/redis-event-target";
      
      const publishClient = new Redis(process.env.REDIS_URL);
      const subscribeClient = new Redis(process.env.REDIS_URL);
      
      const eventTarget = createRedisEventTarget({
        publishClient,
        subscribeClient,
      });
      
      const pubSub = createPubSub({ eventTarget });
      
      // Use pubSub exactly like in-memory version
      // pubSub.publish("newMessage", message)
      // pubSub.subscribe("newMessage")
      ```
      
      **Why good:** drop-in replacement for in-memory PubSub, same API, events propagate across instances
      
      **When to use:** Any deployment with 2+ server instances (load-balanced, horizontal scaling). In-memory PubSub only delivers events to the instance that published them.
      
  • reference.md 5.6 KB
    # GraphQL Yoga Quick Reference
    
    > Decision frameworks, plugin reference, production checklist. Referenced from [SKILL.md](../SKILL.md).
    
    ---
    
    ## Key Imports
    
    | Import                   | Package                                     | Purpose                                             |
    | ------------------------ | ------------------------------------------- | --------------------------------------------------- |
    | `createYoga`             | `graphql-yoga`                              | Create Yoga server instance                         |
    | `createSchema`           | `graphql-yoga`                              | Create schema from SDL + resolvers                  |
    | `createPubSub`           | `graphql-yoga`                              | In-memory publish/subscribe for subscriptions       |
    | `pipe`, `filter`, `map`  | `graphql-yoga`                              | Stream utilities for subscription filtering         |
    | `maskError`              | `graphql-yoga`                              | Default error masking function (for custom masking) |
    | `GraphQLError`           | `graphql`                                   | Intentional client-facing errors (bypasses masking) |
    | `useResponseCache`       | `@graphql-yoga/plugin-response-cache`       | HTTP-level response caching                         |
    | `usePersistedOperations` | `@graphql-yoga/plugin-persisted-operations` | Restrict to pre-approved operations                 |
    | `useCSRFPrevention`      | `@graphql-yoga/plugin-csrf-prevention`      | Require custom header for CSRF protection           |
    | `useDeferStream`         | `@graphql-yoga/plugin-defer-stream`         | @defer and @stream directive support                |
    | `createRedisEventTarget` | `@graphql-yoga/redis-event-target`          | Redis-backed PubSub for multi-instance              |
    
    ---
    
    ## createYoga Options
    
    | Option         | Type                                           | Default           | Purpose                           |
    | -------------- | ---------------------------------------------- | ----------------- | --------------------------------- |
    | `schema`       | `GraphQLSchema`                                | required          | The GraphQL schema                |
    | `context`      | `(initialContext) => T`                        | `{}`              | Context factory (async supported) |
    | `plugins`      | `Plugin[]`                                     | `[]`              | Envelop and Yoga plugins          |
    | `cors`         | `CORSOptions \| (req) => CORSOptions \| false` | `{ origin: '*' }` | CORS configuration                |
    | `graphiql`     | `boolean \| GraphiQLOptions`                   | `true`            | GraphiQL IDE                      |
    | `maskedErrors` | `boolean \| { maskError }`                     | `true`            | Error masking                     |
    | `logging`      | `LogLevel \| LoggerObject`                     | `"info"`          | Log level or custom logger        |
    | `multipart`    | `boolean`                                      | `true`            | Enable/disable file uploads       |
    
    ---
    
    ## Plugin Hook Execution Order
    
    ```
    HTTP Request arrives
      |
      v
    onRequest -----> (can short-circuit with endResponse)
      |
      v
    onRequestParse -> (parse GraphQL params from HTTP body)
      |
      v
    onParams -------> (can skip execution with setResult -- cache hit)
      |
      v
    onParse --------> (parse GraphQL document)
      |
      v
    onValidate -----> (validate document against schema)
      |
      v
    onContextBuilding (build resolver context)
      |
      v
    onExecute / onSubscribe
      |
      v
    onExecutionResult
      |
      v
    onResultProcess -> (serialize result to HTTP response)
      |
      v
    onResponse ------> (final headers/logging before send)
    ```
    
    ---
    
    ## Schema Approach Decision
    
    | Approach           | Tool                               | When to Use                                    |
    | ------------------ | ---------------------------------- | ---------------------------------------------- |
    | Schema-first (SDL) | `createSchema` from `graphql-yoga` | Quick prototyping, teams that prefer SDL       |
    | Code-first         | Pothos, Nexus, gqtx                | Full TypeScript inference in schema definition |
    | Vanilla graphql-js | `GraphQLSchema` constructor        | Maximum control, no dependencies               |
    
    All produce a `GraphQLSchema` instance that Yoga accepts.
    
    ---
    
    ## Subscription Transport Decision
    
    | Transport                | Default? | Setup                                 | Best For                                       |
    | ------------------------ | -------- | ------------------------------------- | ---------------------------------------------- |
    | SSE (Server-Sent Events) | Yes      | Zero config                           | Unidirectional updates, works through proxies  |
    | WebSocket (graphql-ws)   | No       | Requires `ws` + `graphql-ws` packages | Bidirectional communication, existing WS infra |
    | SSE Single Connection    | No       | `@graphql-yoga/plugin-graphql-sse`    | Multiple subscriptions over one connection     |
    
    ---
    
    ## Production Checklist
    
    - [ ] `graphiql: false` (or conditional on `NODE_ENV`)
    - [ ] CORS locked to specific origins (not `*`)
    - [ ] CSRF prevention plugin enabled for browser clients
    - [ ] Error masking enabled (default) -- using `GraphQLError` for intentional errors
    - [ ] Logging level set to `"warn"` or custom logger connected
    - [ ] Response caching for read-heavy queries
    - [ ] Security plugins (GraphQL Armor) for public APIs
    - [ ] Persisted operations for private APIs (reject arbitrary queries)
    - [ ] Redis-backed PubSub if using subscriptions with multiple instances
    - [ ] `graphql` peer dependency installed alongside `graphql-yoga`
    
  • SKILL.md 13.9 KB
    ---
    name: api-graphql-yoga
    description: GraphQL Yoga v5 server, Envelop plugins, subscriptions, error masking
    ---
    
    # GraphQL Yoga Patterns
    
    > **Quick Guide:** Use `createYoga` + `createSchema` for a Fetch API-compatible GraphQL server that runs on any JS runtime. Yoga v5 uses Envelop for plugin composition, SSE for subscriptions by default, built-in error masking, and CORS out of the box. Import `GraphQLError` from `graphql` (not `graphql-yoga`) for intentional client-facing errors. Prefer Yoga-specific plugins over Envelop equivalents for HTTP-level optimizations.
    
    ---
    
    <critical_requirements>
    
    ## CRITICAL: Before Using This Skill
    
    > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
    
    **(You MUST import `GraphQLError` from `'graphql'`, NOT from `'graphql-yoga'` -- it is the standard graphql-js export)**
    
    **(You MUST prefer Yoga-specific plugins over Envelop equivalents -- Yoga plugins operate at the HTTP layer and can skip GraphQL execution entirely for cached/persisted results)**
    
    **(You MUST use `createSchema` from `'graphql-yoga'` for schema-first -- passing raw `typeDefs`/`resolvers` objects directly to `createYoga` is not supported in v5)**
    
    **(You MUST use named constants for all numeric values -- timeouts, TTLs, port numbers, limits)**
    
    </critical_requirements>
    
    ---
    
    **Auto-detection:** GraphQL Yoga, graphql-yoga, createYoga, createSchema, createPubSub, Envelop, useResponseCache, useCSRFPrevention, usePersistedOperations, GraphQL subscriptions SSE, error masking, maskedErrors, graphql-ws, Yoga plugin hooks, onRequest, onParams
    
    **When to use:**
    
    - Building a GraphQL server that needs to run on Node.js, Bun, Deno, or Cloudflare Workers
    - APIs requiring subscriptions via SSE (default) or WebSocket
    - Extending GraphQL execution with Envelop plugins (caching, auth, logging)
    - File uploads using the GraphQL Multipart Request spec
    - Production APIs needing error masking, CORS, and CSRF protection
    
    **When NOT to use:**
    
    - REST-only APIs without GraphQL needs
    - Simple CRUD where a framework's built-in route handlers suffice
    - When you need a federated gateway (consider a dedicated gateway solution)
    
    **Key patterns covered:**
    
    - Server setup with `createYoga` and `createSchema` (schema-first)
    - Type-safe context with generics on `createYoga<ServerContext>`
    - Envelop plugin system: lifecycle hooks, custom plugins, Yoga-specific plugins
    - Subscriptions: SSE (default), WebSocket via `graphql-ws`, built-in PubSub
    - Error masking and intentional `GraphQLError` exposure
    - File uploads with WHATWG `File` scalar
    - Production hardening: CORS, CSRF prevention, GraphQL Armor, logging
    - Cross-runtime deployment: Node.js, Bun, Deno, Cloudflare Workers
    
    **Detailed Resources:**
    
    - [examples/core.md](examples/core.md) - Server setup, schema, context, resolvers, cross-runtime deployment
    - [examples/plugins.md](examples/plugins.md) - Envelop plugins, custom plugins, lifecycle hooks
    - [examples/subscriptions.md](examples/subscriptions.md) - SSE, WebSocket, PubSub, filtering
    - [examples/error-handling.md](examples/error-handling.md) - Error masking, GraphQLError, custom masking
    - [examples/production.md](examples/production.md) - CORS, CSRF, response caching, persisted operations, logging
    - [reference.md](reference.md) - Decision frameworks, plugin reference, production checklist
    
    ---
    
    <philosophy>
    
    ## Philosophy
    
    GraphQL Yoga is a **batteries-included, Fetch API-compatible GraphQL server**. Its core is built on the WHATWG Fetch API (`Request`/`Response`), making it runtime-agnostic -- the same server code deploys to Node.js, Bun, Deno, and edge runtimes. The Envelop plugin system provides composable middleware at both the HTTP and GraphQL execution layers.
    
    **Schema approach:** Yoga is schema-library agnostic. Use `createSchema` (schema-first SDL), Pothos (code-first), or vanilla `graphql-js` -- anything that produces a `GraphQLSchema` works.
    
    **Plugin priority:** When both an Envelop plugin and a Yoga-specific plugin exist for the same feature (caching, persisted operations, defer/stream), always choose the Yoga variant. Yoga plugins hook into the HTTP layer and can short-circuit before GraphQL execution begins, skipping parsing and validation entirely for cached or persisted results.
    
    **Error philosophy:** All unexpected errors are masked by default in production. Intentional errors are thrown as `GraphQLError` from the `graphql` package -- these bypass masking and reach clients with their message and extensions intact.
    
    </philosophy>
    
    ---
    
    <patterns>
    
    ## Core Patterns
    
    ### Pattern 1: Server Setup with createYoga
    
    Create a Yoga instance with `createSchema` for SDL-based schemas. The yoga instance IS a Fetch API handler -- pass it directly to any runtime's HTTP server.
    
    ```typescript
    import { createYoga, createSchema } from "graphql-yoga";
    import { createServer } from "node:http";
    
    const PORT = 4000;
    
    const yoga = createYoga({
      schema: createSchema({
        typeDefs: /* GraphQL */ `
          type Query {
            greeting(name: String!): String!
          }
        `,
        resolvers: {
          Query: {
            greeting: (_, { name }) => `Hello, ${name}!`,
          },
        },
      }),
    });
    
    const server = createServer(yoga);
    server.listen(PORT, () => {
      console.info(`Server running on http://localhost:${PORT}/graphql`);
    });
    ```
    
    **Why good:** `createSchema` wraps `makeExecutableSchema`, yoga instance is a standard Fetch handler, works on any runtime
    
    See [examples/core.md](examples/core.md) for complete setup, cross-runtime deployment, and type-safe context.
    
    ---
    
    ### Pattern 2: Type-Safe Context
    
    Pass a generic to `createYoga` for server-specific context typing. The `context` factory receives `YogaInitialContext` (containing `request` and `params`) and returns your custom context.
    
    ```typescript
    import { createYoga, type YogaInitialContext } from "graphql-yoga";
    
    interface ServerContext {
      req: IncomingMessage;
      res: ServerResponse;
    }
    
    const yoga = createYoga<ServerContext>({
      schema,
      context: async ({ request }: YogaInitialContext) => {
        const token = request.headers.get("authorization");
        return { user: token ? await verifyToken(token) : null };
      },
    });
    ```
    
    **Why good:** generic types flow to resolver context parameter, `request` uses standard Fetch API (not framework-specific `req`/`res`)
    
    See [examples/core.md](examples/core.md) for full context patterns.
    
    ---
    
    ### Pattern 3: Envelop Plugin System
    
    Plugins are passed in the `plugins` array. Use Yoga-specific plugins when available -- they operate at the HTTP layer and can skip GraphQL execution entirely.
    
    ```typescript
    import { createYoga } from "graphql-yoga";
    import { useResponseCache } from "@graphql-yoga/plugin-response-cache";
    
    const CACHE_TTL_MS = 2_000;
    
    const yoga = createYoga({
      schema,
      plugins: [
        useResponseCache({
          session: () => null,
          ttl: CACHE_TTL_MS,
        }),
      ],
    });
    ```
    
    **Why good:** Yoga response cache skips parsing/validation for cached results (Envelop equivalent cannot), plugins compose without conflicts
    
    See [examples/plugins.md](examples/plugins.md) for custom plugins, lifecycle hooks, and all Yoga-specific plugins.
    
    ---
    
    ### Pattern 4: Subscriptions with SSE (Default)
    
    Yoga uses Server-Sent Events by default for subscriptions -- no WebSocket setup needed. Use `AsyncGenerator` syntax in subscription resolvers.
    
    ```typescript
    const schema = createSchema({
      typeDefs: /* GraphQL */ `
        type Subscription {
          countdown(from: Int!): Int!
        }
      `,
      resolvers: {
        Subscription: {
          countdown: {
            subscribe: async function* (_, { from }) {
              for (let i = from; i >= 0; i--) {
                await new Promise((resolve) => setTimeout(resolve, 1_000));
                yield { countdown: i };
              }
            },
          },
        },
      },
    });
    ```
    
    **Why good:** no WebSocket infrastructure needed, works through HTTP proxies and load balancers, `graphql-sse` library for clients
    
    See [examples/subscriptions.md](examples/subscriptions.md) for PubSub, WebSocket setup, and filtering.
    
    ---
    
    ### Pattern 5: Error Masking and GraphQLError
    
    Yoga masks all unexpected errors by default. Throw `GraphQLError` (from `graphql`) for intentional client-facing errors -- these bypass masking.
    
    ```typescript
    import { GraphQLError } from "graphql";
    
    const NOT_FOUND_CODE = "USER_NOT_FOUND";
    
    throw new GraphQLError("User not found", {
      extensions: { code: NOT_FOUND_CODE },
    });
    ```
    
    **Why good:** unexpected errors never leak internals (database details, stack traces), intentional errors pass through with message + extensions
    
    See [examples/error-handling.md](examples/error-handling.md) for custom masking, disabling masking, and development mode.
    
    ---
    
    ### Pattern 6: File Uploads
    
    Yoga supports the GraphQL Multipart Request spec. Add a `File` scalar and receive WHATWG `File` objects in resolvers.
    
    ```typescript
    const schema = createSchema({
      typeDefs: /* GraphQL */ `
        scalar File
        type Mutation {
          uploadFile(file: File!): Boolean!
        }
      `,
      resolvers: {
        Mutation: {
          uploadFile: async (_, { file }: { file: File }) => {
            const content = await file.arrayBuffer();
            // Process file content
            return true;
          },
        },
      },
    });
    ```
    
    **Why good:** uses standard WHATWG File API (same as browser), no extra packages needed, disable with `multipart: false`
    
    See [examples/core.md](examples/core.md) for complete file upload patterns.
    
    </patterns>
    
    ---
    
    <decision_framework>
    
    ## Decision Framework
    
    ### Schema Approach
    
    ```
    Need auto-generated types from SDL?
    +-- YES --> createSchema (schema-first with typeDefs + resolvers)
    +-- NO  --> Want full TypeScript inference in schema definition?
        +-- YES --> Code-first library (e.g. Pothos) -- pass resulting GraphQLSchema to Yoga
        +-- NO  --> Vanilla graphql-js GraphQLSchema
    ```
    
    ### Subscription Transport
    
    ```
    Need subscriptions?
    +-- YES --> Do clients need bidirectional communication?
    |   +-- YES --> WebSocket via graphql-ws (add ws + graphql-ws packages)
    |   +-- NO  --> SSE (default, zero config, works through proxies)
    +-- NO  --> No subscription setup needed
    ```
    
    ### Plugin Selection
    
    ```
    Feature available as Yoga-specific plugin?
    +-- YES --> Use Yoga plugin (HTTP-level hooks, can skip execution)
    +-- NO  --> Use Envelop plugin (GraphQL execution-level hooks)
    ```
    
    ### Yoga-Specific Plugins (Prefer Over Envelop)
    
    | Plugin               | Package                                     | Why Yoga-specific                        |
    | -------------------- | ------------------------------------------- | ---------------------------------------- |
    | Response Cache       | `@graphql-yoga/plugin-response-cache`       | Skips execution for cached queries       |
    | Persisted Operations | `@graphql-yoga/plugin-persisted-operations` | Rejects unknown operations at HTTP layer |
    | Defer/Stream         | `@graphql-yoga/plugin-defer-stream`         | Streams via HTTP chunked encoding        |
    | CSRF Prevention      | `@graphql-yoga/plugin-csrf-prevention`      | Requires custom header before parsing    |
    | GraphQL SSE          | `@graphql-yoga/plugin-graphql-sse`          | Single-connection SSE mode               |
    
    </decision_framework>
    
    ---
    
    <red_flags>
    
    ## RED FLAGS
    
    **High Priority:**
    
    - Importing `GraphQLError` from `graphql-yoga` instead of `graphql` -- wrong package, will fail
    - Passing `typeDefs`/`resolvers` object directly to `createYoga` without `createSchema` -- not supported in v5
    - Using an Envelop plugin when a Yoga-specific equivalent exists -- misses HTTP-level optimizations (the Yoga response cache skips parsing entirely; the Envelop equivalent cannot)
    - Throwing plain `Error` in resolvers expecting clients to see the message -- masked to "Unexpected error." in production
    
    **Medium Priority:**
    
    - Not configuring CORS origins for production -- default is `*`, which should be locked down
    - Using in-memory PubSub across multiple server instances -- events won't propagate (use Redis-backed `createRedisEventTarget`)
    - Missing `graphql` peer dependency -- `graphql-yoga` requires `graphql` as a peer, install both
    - Calling `createSchema` with no schema at all -- Yoga requires a schema; it does not infer one
    
    **Gotchas & Edge Cases:**
    
    - `YogaInitialContext.request` is a Fetch API `Request`, not a Node.js `IncomingMessage` -- use `request.headers.get()`, not `req.headers`
    - Plugin execution order changed in v5 -- plugins added via `addPlugin` in `onPluginInit` now execute immediately after the adding plugin, not last
    - `useResponseCache` `session` callback must return a string (user ID) for PRIVATE scope or `null` for public -- returning `undefined` breaks caching
    - SSE subscriptions go through HTTP (text/event-stream) -- some proxies may buffer events; set `X-Accel-Buffering: no` for Nginx
    - `File` scalar in uploads gives you a WHATWG `File` object -- use `.text()`, `.arrayBuffer()`, or `.stream()` methods (not Node.js `Buffer` directly)
    - Yoga's built-in GraphiQL is enabled by default -- disable with `graphiql: false` in production
    - `maskedErrors` set to `false` disables ALL masking including stack traces -- use custom `maskError` function instead for selective exposure
    - CORS `credentials: true` with `origin: '*'` is rejected by browsers per the Fetch spec -- specify exact origins
    
    </red_flags>
    
    ---
    
    <critical_reminders>
    
    ## CRITICAL REMINDERS
    
    > **All code must follow project conventions in CLAUDE.md**
    
    **(You MUST import `GraphQLError` from `'graphql'`, NOT from `'graphql-yoga'` -- it is the standard graphql-js export)**
    
    **(You MUST prefer Yoga-specific plugins over Envelop equivalents -- Yoga plugins operate at the HTTP layer and can skip GraphQL execution entirely for cached/persisted results)**
    
    **(You MUST use `createSchema` from `'graphql-yoga'` for schema-first -- passing raw `typeDefs`/`resolvers` objects directly to `createYoga` is not supported in v5)**
    
    **(You MUST use named constants for all numeric values -- timeouts, TTLs, port numbers, limits)**
    
    **Failure to follow these rules will cause import errors, missed performance optimizations, and information leakage through unmasked errors.**
    
    </critical_reminders>
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related