Fix: GraphQL Yoga Not Working — Schema Errors, Resolvers Not Executing, or Subscriptions Failing
Part of: JavaScript & TypeScript Errors
Quick Answer
How to fix GraphQL Yoga issues — schema definition, resolver patterns, context and authentication, file uploads, subscriptions with SSE, error handling, and Next.js integration.
The Problem
The GraphQL server starts but queries return null:
query {
users {
id
name
}
}
# Returns: { "data": { "users": null } }Or the schema fails to build:
Error: Type "User" not found in documentOr subscriptions don’t deliver real-time updates:
Subscription connects but never receives eventsWhy This Happens
GraphQL Yoga is a batteries-included GraphQL server built on top of the Envelop plugin system. Common issues:
- Every field in the schema needs a resolver — if you define a
usersquery in the schema but don’t implement a resolver for it, the field resolves tonull. GraphQL doesn’t throw for missing resolvers — it returns null. - Type definitions and resolvers must align — the schema defines the shape, resolvers provide the data. A field named
userNamein the schema butusernamein the resolver returns null for that field. - Context is rebuilt per request — authentication data, database connections, and other request-specific values go in the context function. If context returns an empty object, resolvers can’t access auth or database.
- Subscriptions use Server-Sent Events (SSE) by default — unlike Apollo which uses WebSockets, Yoga uses SSE for subscriptions. Client libraries must support SSE, not just WebSocket subscriptions.
A second class of failure is the plugin layer. Yoga sits on top of Envelop, which lets you intercept every phase of a GraphQL request (parse, validate, execute, subscribe). If you install a plugin that returns an error from the validate phase, the resolver never runs — and the error message looks like a normal GraphQL error, not a plugin failure. Common offenders are useDepthLimit, useRateLimit, and useDisableIntrospection, which silently reject queries that look legitimate.
A third class is request lifecycle. Yoga’s context function runs once per HTTP request, but a subscription is a long-lived connection. If you initialize a database connection inside context, every subscription holds it open. Memory and connection-pool issues show up only under load and look like “queries get slower over time” rather than a clear error. Move long-lived state to module scope and only put request-specific values (auth user, request ID) in context.
Version History (GraphQL Yoga v2 → v5 and the Envelop / Hive ecosystem)
GraphQL Yoga has gone through a major rewrite and several architectural shifts. The version you target dramatically changes the API.
- graphql-yoga v1 (2018) — the original “easy GraphQL server” from Prisma. It was a thin wrapper around
graphql-expressandapollo-serverwith sensible defaults. v1 was deprecated when The Guild took over the project. - graphql-yoga v2 (early 2022) — full rewrite by The Guild on top of Envelop. New API (
createServerinstead ofnew GraphQLServer), Web-standardRequest/Response, and built-in support for Cloudflare Workers, Deno, and Bun. Almost no backward compatibility with v1. - graphql-yoga v3 (September 2022) —
createServerrenamed tocreateYoga, official Next.js integration, GraphQL-SSE for subscriptions instead of GraphQL-WS by default, and improved error masking viamaskedErrors. Most blog posts from late 2022 and 2023 reference v3. - graphql-yoga v4 (mid-2023) — refined plugin system, native HTTP/2 and HTTP/3 support, performance improvements for large schemas, and stricter TypeScript types for context and plugins. Most v3 code runs unchanged on v4 with a
npm update. - graphql-yoga v5 (March 2024) — significant restructuring: many features previously bundled with Yoga moved into separate Envelop plugins (cost analysis, persisted operations, response cache). The Hive (The Guild’s schema registry and analytics product) integration became first-class. Older code that relied on built-in features may need to install
@envelop/*packages explicitly. - Hive integration (2024→) — Yoga + Hive Gateway is now the recommended way to build a federated GraphQL stack. The previous federation story relied on Apollo Federation; Hive Gateway is a drop-in replacement that works with any subgraph that speaks the federation spec.
Practical implication: if a tutorial says createServer({ schema }), it is for v2 and the import paths are different. If a tutorial uses pubsub.asyncIterator(...) instead of pubSub.subscribe(...), it is pre-v3 syntax. Check npm ls graphql-yoga before copy-pasting examples. On v5 specifically, if a feature seems “missing” (like cost analysis), the cause is almost always that the corresponding Envelop plugin needs to be installed separately.
Fix 1: Basic Server Setup
npm install graphql-yoga graphql// server.ts — standalone server
import { createSchema, createYoga } from 'graphql-yoga';
import { createServer } from 'node:http';
const yoga = createYoga({
schema: createSchema({
typeDefs: /* GraphQL */ `
type Query {
users: [User!]!
user(id: ID!): User
}
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): Boolean!
}
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
createdAt: String!
}
type Post {
id: ID!
title: String!
body: String!
author: User!
}
input CreateUserInput {
name: String!
email: String!
}
input UpdateUserInput {
name: String
email: String
}
`,
resolvers: {
Query: {
users: async (_, __, context) => {
return context.db.query.users.findMany();
},
user: async (_, { id }, context) => {
return context.db.query.users.findFirst({
where: eq(users.id, id),
});
},
},
Mutation: {
createUser: async (_, { input }, context) => {
const [user] = await context.db.insert(users).values(input).returning();
return user;
},
updateUser: async (_, { id, input }, context) => {
const [user] = await context.db.update(users)
.set(input)
.where(eq(users.id, id))
.returning();
return user;
},
deleteUser: async (_, { id }, context) => {
await context.db.delete(users).where(eq(users.id, id));
return true;
},
},
// Field resolver — resolve nested relationships
User: {
posts: async (parent, _, context) => {
return context.db.query.posts.findMany({
where: eq(posts.authorId, parent.id),
});
},
},
Post: {
author: async (parent, _, context) => {
return context.db.query.users.findFirst({
where: eq(users.id, parent.authorId),
});
},
},
},
}),
// Context — available in all resolvers
context: async ({ request }) => {
const token = request.headers.get('authorization')?.replace('Bearer ', '');
const user = token ? await verifyToken(token) : null;
return {
db, // Database instance
user, // Authenticated user (or null)
request,
};
},
// GraphiQL explorer
graphiql: true,
});
const server = createServer(yoga);
server.listen(4000, () => console.log('GraphQL server on http://localhost:4000/graphql'));Fix 2: Next.js App Router Integration
// app/api/graphql/route.ts
import { createSchema, createYoga } from 'graphql-yoga';
import { db } from '@/lib/db';
const yoga = createYoga({
schema: createSchema({
typeDefs: /* GraphQL */ `
type Query {
hello: String!
posts(limit: Int): [Post!]!
}
type Post {
id: ID!
title: String!
body: String!
}
`,
resolvers: {
Query: {
hello: () => 'Hello from GraphQL Yoga!',
posts: async (_, { limit = 10 }) => {
return db.query.posts.findMany({ limit });
},
},
},
}),
graphqlEndpoint: '/api/graphql',
fetchAPI: { Response },
});
const { handleRequest } = yoga;
export {
handleRequest as GET,
handleRequest as POST,
handleRequest as OPTIONS,
};Fix 3: Authentication and Authorization
import { createSchema, createYoga } from 'graphql-yoga';
import { useGenericAuth } from '@envelop/generic-auth';
const yoga = createYoga({
schema: createSchema({
typeDefs: /* GraphQL */ `
type Query {
me: User
adminStats: Stats! # Should only be accessible to admins
}
`,
resolvers: {
Query: {
me: (_, __, context) => context.currentUser,
adminStats: (_, __, context) => {
if (!context.currentUser) throw new GraphQLError('Not authenticated');
if (context.currentUser.role !== 'admin') throw new GraphQLError('Not authorized');
return getAdminStats();
},
},
},
}),
context: async ({ request }) => {
const token = request.headers.get('authorization')?.replace('Bearer ', '');
let currentUser = null;
if (token) {
try {
const payload = await verifyJWT(token);
currentUser = await db.query.users.findFirst({
where: eq(users.id, payload.sub),
});
} catch {
// Invalid token — continue as unauthenticated
}
}
return { currentUser, db };
},
});Fix 4: Subscriptions with SSE
import { createSchema, createYoga, createPubSub } from 'graphql-yoga';
// Create a pub/sub instance
const pubSub = createPubSub<{
'message:created': [{ messageCreated: Message }];
'user:online': [{ userOnline: User }];
}>();
const yoga = createYoga({
schema: createSchema({
typeDefs: /* GraphQL */ `
type Query {
messages(channelId: ID!): [Message!]!
}
type Mutation {
sendMessage(channelId: ID!, text: String!): Message!
}
type Subscription {
messageCreated(channelId: ID!): Message!
}
type Message {
id: ID!
text: String!
channelId: ID!
author: User!
createdAt: String!
}
`,
resolvers: {
Query: {
messages: async (_, { channelId }, ctx) => {
return ctx.db.query.messages.findMany({
where: eq(messages.channelId, channelId),
orderBy: desc(messages.createdAt),
limit: 50,
});
},
},
Mutation: {
sendMessage: async (_, { channelId, text }, ctx) => {
if (!ctx.currentUser) throw new GraphQLError('Not authenticated');
const [message] = await ctx.db.insert(messages).values({
id: crypto.randomUUID(),
text,
channelId,
authorId: ctx.currentUser.id,
}).returning();
// Publish to subscribers
pubSub.publish('message:created', { messageCreated: message });
return message;
},
},
Subscription: {
messageCreated: {
subscribe: (_, { channelId }) =>
pubSub.subscribe('message:created'),
resolve: (payload) => payload.messageCreated,
},
},
},
}),
});
// Client — subscribe using SSE
// GraphQL Yoga uses Server-Sent Events, not WebSockets
// Most GraphQL clients support SSE:
// - graphql-sse
// - urql with @urql/exchange-sse
// - Apollo Client with SSE linkFix 5: File Uploads
import { createSchema, createYoga } from 'graphql-yoga';
const yoga = createYoga({
schema: createSchema({
typeDefs: /* GraphQL */ `
scalar File
type Query {
files: [FileInfo!]!
}
type Mutation {
uploadFile(file: File!): FileInfo!
uploadFiles(files: [File!]!): [FileInfo!]!
}
type FileInfo {
filename: String!
size: Int!
url: String!
}
`,
resolvers: {
Mutation: {
uploadFile: async (_, { file }: { file: File }) => {
const buffer = Buffer.from(await file.arrayBuffer());
const filename = file.name;
const size = buffer.length;
// Save to storage
const url = await uploadToStorage(buffer, filename);
return { filename, size, url };
},
uploadFiles: async (_, { files }: { files: File[] }) => {
return Promise.all(
files.map(async (file) => {
const buffer = Buffer.from(await file.arrayBuffer());
const url = await uploadToStorage(buffer, file.name);
return { filename: file.name, size: buffer.length, url };
})
);
},
},
},
}),
// File upload limits
multipart: {
maxFileSize: 10 * 1024 * 1024, // 10MB
maxFiles: 5,
},
});Fix 6: Error Handling
import { createSchema, createYoga } from 'graphql-yoga';
import { GraphQLError } from 'graphql';
const yoga = createYoga({
schema: createSchema({
resolvers: {
Mutation: {
createPost: async (_, { input }, ctx) => {
// Throw user-facing errors
if (!ctx.currentUser) {
throw new GraphQLError('You must be logged in', {
extensions: { code: 'UNAUTHENTICATED' },
});
}
if (input.title.length < 3) {
throw new GraphQLError('Title must be at least 3 characters', {
extensions: {
code: 'VALIDATION_ERROR',
field: 'title',
},
});
}
try {
return await ctx.db.insert(posts).values(input).returning();
} catch (error) {
// Log internal errors, return generic message
console.error('Database error:', error);
throw new GraphQLError('Failed to create post', {
extensions: { code: 'INTERNAL_ERROR' },
});
}
},
},
},
}),
// Global error masking
maskedErrors: {
isDev: process.env.NODE_ENV === 'development',
// In production, unexpected errors show "Unexpected error"
// In development, full error details are shown
},
});Still Not Working?
Query returns null instead of data — the resolver for that field is missing or returns undefined. Every field in your schema needs a resolver, either explicit or through a parent resolver that returns an object with matching property names. Check for typos between schema field names and resolver keys.
“Type not found in document” — you’re referencing a type in your schema that isn’t defined. Check that all types used in field definitions, input types, and return types are defined in typeDefs. Also check for circular imports if you split your schema across files.
Subscriptions connect but never receive data — Yoga uses Server-Sent Events (SSE). Your client must support SSE subscriptions. Apollo Client needs graphql-sse or a custom SSE link. Also verify that pubSub.publish() is called in the mutation that should trigger the subscription.
Context is empty in resolvers — the context function must return an object. If it returns undefined or throws, resolvers receive an empty context. Add error handling in the context function and ensure async operations are awaited.
An Envelop plugin silently rejects queries — depth limit, rate limit, and persisted-operation plugins return errors before the resolver runs. Check the response errors[].extensions.code for hints like DEPTH_LIMIT_EXCEEDED or OPERATION_NOT_FOUND. If you upgraded from v4 to v5, some built-in features now require explicit plugin installation (@envelop/depth-limit, @envelop/response-cache).
Memory grows under load — long-lived state (database client, Redis client) should be initialized at module scope, not inside context. The context function runs per request and per subscription event. Putting heavy initialization there leaks connections.
Federation gateway returns gateway-level errors only — if you migrated from Apollo Gateway to Hive Gateway, check that all your subgraphs return valid _service { sdl } and that the gateway’s polling interval matches the rate at which you redeploy subgraphs. Caching can serve a stale composed schema for minutes after a subgraph changes.
For related API and schema-building issues, see Fix: Pothos Not Working and Fix: tRPC Not Working. For HTTP-server alternatives when GraphQL feels like overkill, see Fix: Hono Not Working and Fix: Hono RPC Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Pothos Not Working — Types Not Resolving, Plugin Errors, or Prisma Integration Failing
How to fix Pothos GraphQL schema builder issues — type-safe schema definition, object and input types, Prisma plugin, relay connections, auth scope plugin, and schema printing.
Fix: Fastify Not Working — 404, Plugin Encapsulation, and Schema Validation Errors
How to fix Fastify issues — route 404 from plugin encapsulation, reply already sent, FST_ERR_VALIDATION, request.body undefined, @fastify/cors, hooks not running, and TypeScript type inference.
Fix: Better Auth Not Working — Login Failing, Session Null, or OAuth Callback Error
How to fix Better Auth issues — server and client setup, email/password and OAuth providers, session management, middleware protection, database adapters, and plugin configuration.
Fix: BullMQ Not Working — Jobs Not Processing, Workers Not Starting, or Redis Connection Failing
How to fix BullMQ issues — queue and worker setup, Redis connection, job scheduling, retry strategies, concurrency, rate limiting, event listeners, and dashboard monitoring.