api-graphql-yoga
GraphQL Yoga v5 server, Envelop plugins, subscriptions, error masking
Install
npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/api-graphql-yoga/skills/api-graphql-yoga
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
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+createSchemafor 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. ImportGraphQLErrorfromgraphql(notgraphql-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
createYogaandcreateSchema(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
GraphQLErrorexposure - File uploads with WHATWG
Filescalar - Production hardening: CORS, CSRF prevention, GraphQL Armor, logging
- Cross-runtime deployment: Node.js, Bun, Deno, Cloudflare Workers
Detailed Resources:
- examples/core.md - Server setup, schema, context, resolvers, cross-runtime deployment
- examples/plugins.md - Envelop plugins, custom plugins, lifecycle hooks
- examples/subscriptions.md - SSE, WebSocket, PubSub, filtering
- examples/error-handling.md - Error masking, GraphQLError, custom masking
- examples/production.md - CORS, CSRF, response caching, persisted operations, logging
- reference.md - Decision frameworks, plugin reference, production checklist
<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
GraphQLErrorfromgraphql-yogainstead ofgraphql-- wrong package, will fail - Passing
typeDefs/resolversobject directly tocreateYogawithoutcreateSchema-- 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
Errorin 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
graphqlpeer dependency --graphql-yogarequiresgraphqlas a peer, install both - Calling
createSchemawith no schema at all -- Yoga requires a schema; it does not infer one
Gotchas & Edge Cases:
YogaInitialContext.requestis a Fetch APIRequest, not a Node.jsIncomingMessage-- userequest.headers.get(), notreq.headers- Plugin execution order changed in v5 -- plugins added via
addPlugininonPluginInitnow execute immediately after the adding plugin, not last useResponseCachesessioncallback must return a string (user ID) for PRIVATE scope ornullfor public -- returningundefinedbreaks caching- SSE subscriptions go through HTTP (text/event-stream) -- some proxies may buffer events; set
X-Accel-Buffering: nofor Nginx Filescalar in uploads gives you a WHATWGFileobject -- use.text(),.arrayBuffer(), or.stream()methods (not Node.jsBufferdirectly)- Yoga's built-in GraphiQL is enabled by default -- disable with
graphiql: falsein production maskedErrorsset tofalsedisables ALL masking including stack traces -- use custommaskErrorfunction instead for selective exposure- CORS
credentials: truewithorigin: '*'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.
Reviews (0)
No reviews yet.
No comments yet.