Skip to content

Fix: GraphQL Yoga Not Working — Schema Errors, Resolvers Not Executing, or Subscriptions Failing

FixDevs · (Updated: )

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 document

Or subscriptions don’t deliver real-time updates:

Subscription connects but never receives events

Why 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 users query in the schema but don’t implement a resolver for it, the field resolves to null. 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 userName in the schema but username in 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-express and apollo-server with 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 (createServer instead of new GraphQLServer), Web-standard Request/Response, and built-in support for Cloudflare Workers, Deno, and Bun. Almost no backward compatibility with v1.
  • graphql-yoga v3 (September 2022)createServer renamed to createYoga, official Next.js integration, GraphQL-SSE for subscriptions instead of GraphQL-WS by default, and improved error masking via maskedErrors. 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 link

Fix 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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles