Skip to main content

Guide

graphql-yoga vs apollo-server vs mercurius 2026

Compare graphql-yoga, apollo-server, and mercurius for building GraphQL APIs in Node.js. Schema stitching, subscriptions, plugins, TypeScript, performance.

·PkgPulse Team·
0

TL;DR

graphql-yoga is the modern standard from The Guild — framework-agnostic, runs anywhere (Node.js, Cloudflare Workers, Deno, Bun), has excellent TypeScript integration with Envelop plugin system, and ships with built-in subscription support via Server-Sent Events. apollo-server is the most-used but increasingly heavy — now part of Apollo's managed platform ecosystem, with @apollo/server v4 being standalone. mercurius is the Fastify-native GraphQL server — if you're on Fastify, it's the fastest option. For new projects: graphql-yoga. For teams already invested in Apollo's tooling: apollo-server. For Fastify: mercurius.

Key Takeaways

  • graphql-yoga: ~700K weekly downloads — The Guild, framework-agnostic, Envelop plugins, SSE subscriptions
  • @apollo/server: ~2M weekly downloads — most popular, Studio integration, federation support
  • mercurius: ~200K weekly downloads — Fastify-native, fastest performance, JIT compiler
  • graphql-yoga v5 works on Cloudflare Workers and edge runtimes — apollo-server v4 is Node.js-only
  • Apollo Federation (schema stitching across services) requires Apollo — yoga supports it via Hive
  • All three use the same GraphQL.js execution engine under the hood

graphql-yoga

graphql-yoga — framework-agnostic modern GraphQL server:

Basic setup

import { createSchema, createYoga } from "graphql-yoga"

// Define schema with SDL:
const schema = createSchema({
  typeDefs: /* GraphQL */ `
    type Query {
      packages(minScore: Int): [Package!]!
      package(name: String!): Package
    }

    type Package {
      name: String!
      weeklyDownloads: Int!
      healthScore: Float!
      tags: [String!]!
    }
  `,
  resolvers: {
    Query: {
      packages: async (_, { minScore = 0 }) =>
        db.packages.findMany({ where: { healthScore: { gte: minScore } } }),

      package: async (_, { name }) =>
        db.packages.findUnique({ where: { name } }),
    },
  },
})

const yoga = createYoga({ schema })

// Standalone Node.js server:
import { createServer } from "http"
const server = createServer(yoga)
server.listen(4000, () => console.log("GraphQL: http://localhost:4000/graphql"))

Works with any framework

// Express:
import express from "express"
import { createYoga, createSchema } from "graphql-yoga"

const yoga = createYoga({ schema })
const app = express()
app.use("/graphql", yoga)

// Next.js App Router:
// app/api/graphql/route.ts
import { createYoga, createSchema } from "graphql-yoga"

const yoga = createYoga({
  schema,
  graphqlEndpoint: "/api/graphql",
})

export { yoga as GET, yoga as POST }

// Cloudflare Workers:
import { createYoga } from "graphql-yoga"
export default {
  fetch: createYoga({ schema }).fetch,
}

// Hono:
import { Hono } from "hono"
const app = new Hono()
const yoga = createYoga({ schema })
app.use("/graphql", (c) => yoga.handle(c.req.raw, c.executionCtx))

Subscriptions (SSE — no WebSocket needed)

import { createSchema, createYoga } from "graphql-yoga"

const schema = createSchema({
  typeDefs: /* GraphQL */ `
    type Query { _dummy: Boolean }

    type Subscription {
      packageHealthUpdated(name: String!): Package!
    }

    type Package {
      name: String!
      healthScore: Float!
      updatedAt: String!
    }
  `,
  resolvers: {
    Subscription: {
      packageHealthUpdated: {
        subscribe: async function* (_, { name }) {
          // Async generator — yields new values as they arrive:
          while (true) {
            const pkg = await fetchHealthFromNpm(name)
            yield { packageHealthUpdated: pkg }
            await new Promise((r) => setTimeout(r, 5000))  // Poll every 5s
          }
        },
      },
    },
  },
})

// Yoga uses SSE for subscriptions by default
// Client subscribes with: useSubscription from @urql/core

Envelop plugins

import { createYoga } from "graphql-yoga"
import { useResponseCache } from "@envelop/response-cache"
import { useRateLimiter } from "@envelop/rate-limiter"
import { useSentryUser } from "@envelop/sentry"
import { useDepthLimit } from "@envelop/depth-limit"

const yoga = createYoga({
  schema,
  plugins: [
    useResponseCache({
      ttl: 60_000,  // 60 second cache
      session: (request) => request.headers.get("authorization"),
    }),
    useDepthLimit({ maxDepth: 7 }),  // Prevent deeply nested queries
    useRateLimiter({
      identifyFn: (context) => context.request.headers.get("x-forwarded-for") ?? "anonymous",
      max: 100,
      window: "1m",
    }),
  ],
})

@apollo/server (v4)

@apollo/server — the most popular GraphQL server:

Standalone server

import { ApolloServer } from "@apollo/server"
import { startStandaloneServer } from "@apollo/server/standalone"

const server = new ApolloServer({
  typeDefs: /* GraphQL */ `
    type Query {
      packages(minScore: Int): [Package!]!
    }

    type Package {
      name: String!
      healthScore: Float!
    }
  `,
  resolvers: {
    Query: {
      packages: (_, { minScore = 0 }) =>
        db.packages.findMany({ where: { healthScore: { gte: minScore } } }),
    },
  },
})

const { url } = await startStandaloneServer(server, {
  listen: { port: 4000 },
  context: async ({ req }) => ({
    token: req.headers.authorization,
    db,
  }),
})

console.log(`GraphQL: ${url}`)

Express integration

import { ApolloServer } from "@apollo/server"
import { expressMiddleware } from "@apollo/server/express4"
import express from "express"
import cors from "cors"

const server = new ApolloServer({ typeDefs, resolvers })
await server.start()

const app = express()
app.use(cors())
app.use(express.json())
app.use("/graphql", expressMiddleware(server, {
  context: async ({ req }) => ({
    user: await getUserFromToken(req.headers.authorization),
    db,
  }),
}))

app.listen(4000)

Apollo Studio + performance plugins

import { ApolloServer } from "@apollo/server"
import {
  ApolloServerPluginUsageReporting,
  ApolloServerPluginCacheControl,
  ApolloServerPluginLandingPageLocalDefault,
} from "@apollo/server/plugin"

const server = new ApolloServer({
  typeDefs,
  resolvers,
  plugins: [
    // Send usage data to Apollo Studio (requires APOLLO_KEY):
    ApolloServerPluginUsageReporting({
      rewriteError(err) {
        // Scrub PII from error messages:
        err.message = err.message.replace(/email: .+/, "email: [redacted]")
        return err
      },
    }),

    // Cache-control headers:
    ApolloServerPluginCacheControl({ defaultMaxAge: 60 }),

    // Apollo Sandbox in development:
    process.env.NODE_ENV === "development"
      ? ApolloServerPluginLandingPageLocalDefault({ embed: true })
      : undefined,
  ].filter(Boolean),
})

mercurius (Fastify)

mercurius — GraphQL for Fastify:

Setup

import Fastify from "fastify"
import mercurius from "mercurius"

const app = Fastify()

app.register(mercurius, {
  schema: /* GraphQL */ `
    type Query {
      packages(minScore: Int): [Package!]!
    }
    type Package {
      name: String!
      healthScore: Float!
    }
  `,
  resolvers: {
    Query: {
      packages: async (_, { minScore = 0 }, context) =>
        context.db.packages.findMany({ where: { healthScore: { gte: minScore } } }),
    },
  },
  context: (request) => ({
    db,
    user: request.user,
  }),
  graphiql: true,  // Enable GraphiQL in development
})

await app.listen({ port: 4000 })
console.log("GraphQL: http://localhost:4000/graphiql")

JIT compiler (mercurius advantage)

// mercurius includes graphql-jit — compiles queries to optimized functions:
import Fastify from "fastify"
import mercurius from "mercurius"
import { makeExecutableSchema } from "@graphql-tools/schema"
import { compileQuery } from "graphql-jit"

// mercurius uses graphql-jit automatically
// Benchmark: mercurius ~30% faster than yoga/apollo for cached queries

app.register(mercurius, {
  schema,
  resolvers,
  // Caches compiled query functions:
  cache: {
    policy: {
      Query: {
        packages: { ttl: 60 },  // Cache packages query for 60s
      },
    },
  },
})

Subscriptions with WebSocket

import Fastify from "fastify"
import mercurius from "mercurius"
import { createPubSub } from "mercurius"

const pubsub = createPubSub()

app.register(mercurius, {
  schema,
  resolvers: {
    Subscription: {
      packageUpdated: {
        subscribe: (_, { name }) => pubsub.subscribe(`package:${name}`),
      },
    },
  },
  subscription: true,   // Enable WebSocket subscriptions
})

// Publish update:
pubsub.publish("package:react", { packageUpdated: newPackageData })

Feature Comparison

Featuregraphql-yoga@apollo/servermercurius
Edge runtime❌ Node.js only
Framework-agnostic❌ Fastify only
Subscriptions✅ SSE + WS❌ (separate pkg)✅ WS
Apollo Federation✅ (via Hive)✅ Native✅ (plugin)
Apollo Studio
JIT compilation
Plugin system✅ Envelop✅ Apollo plugins✅ Mercurius plugins
TypeScript
PerformanceFastModerateFastest
Maintenance✅ The Guild✅ Apollo✅ NearForm

When to Use Each

Choose graphql-yoga if:

  • You want maximum flexibility — framework, runtime, deployment target
  • Cloudflare Workers, Deno, Bun, or Next.js edge runtime
  • The Envelop plugin ecosystem (response cache, rate limiting, depth limit)
  • New project without legacy Apollo dependencies

Choose @apollo/server if:

  • Your team uses Apollo Studio for schema exploration and performance monitoring
  • Apollo Federation for a supergraph/microservices architecture
  • Existing Apollo v2/v3 codebase to migrate

Choose mercurius if:

  • You're already on Fastify — natural fit, best performance
  • JIT-compiled query execution matters (high-throughput APIs)
  • WebSocket subscriptions are a requirement

Schema Design: SDL-First vs Code-First and TypeScript Integration

The GraphQL schema authoring approach — whether to write SDL (Schema Definition Language) strings or generate schema from TypeScript code — affects how type safety flows through your application and which GraphQL server works best for your workflow.

SDL-first schema definition (as shown in the examples above for all three servers) requires maintaining schema types in string literals and then separately typing resolver functions. TypeScript doesn't validate that your resolver return types match the SDL types at compile time without additional tooling. @graphql-codegen/cli (GraphQL Code Generator) solves this by reading your SDL and generating TypeScript types for all resolvers, query variables, and response shapes. This is the standard toolchain for SDL-first development: SDL in .graphql files, graphql-codegen watching them, and generated types imported by resolvers.

Code-first schema generation tools like Pothos (for graphql-yoga and apollo-server) and TypeGraphQL (for apollo-server, class-based) derive the SDL from your TypeScript code. In Pothos, you define types with TypeScript builder methods: builder.objectType('Package', { fields: (t) => ({ name: t.string({ resolve: (root) => root.name }) }) }). The SDL is generated automatically, and TypeScript enforces that resolver return types match field definitions at compile time — no code generation step required. This produces tighter type safety for large schemas but requires learning the Pothos or TypeGraphQL API.

Mercurius on Fastify integrates particularly well with @fastify/swagger for REST/GraphQL hybrid APIs — the same Fastify application can serve REST routes with TypeBox schemas (for OpenAPI) and a GraphQL endpoint via Mercurius. This is relevant for teams building APIs that need both REST compatibility (for third-party integrations) and GraphQL (for primary client consumption). Apollo Server and graphql-yoga also support this pattern via Express or Fastify middleware, but Mercurius's native Fastify integration makes it the lowest-friction option.

Migration Guide

From Apollo Server v3 to Apollo Server v4

Apollo Server v4 dropped the apollo-server package in favor of framework-specific adapters, requiring a dependency change:

# Remove v3
npm uninstall apollo-server

# Install v4 + framework adapter
npm install @apollo/server
// Apollo Server v3 (old)
import { ApolloServer } from "apollo-server"
const server = new ApolloServer({ typeDefs, resolvers })
server.listen({ port: 4000 }).then(({ url }) => console.log(url))

// Apollo Server v4 (new) — standalone
import { ApolloServer } from "@apollo/server"
import { startStandaloneServer } from "@apollo/server/standalone"
const server = new ApolloServer({ typeDefs, resolvers })
const { url } = await startStandaloneServer(server, { listen: { port: 4000 } })

The context function signature changed: v3 passed context as a function to the server constructor, while v4 passes it to startStandaloneServer or the framework middleware. Context is no longer merged automatically — you must explicitly merge request properties.

Community Adoption in 2026

@apollo/server leads at approximately 2 million weekly downloads, down from Apollo's peak when it was the only mature option. The transition from apollo-server to @apollo/server in v4 caused some fragmentation in download stats, but Apollo remains the most recognized name in the GraphQL server space. Apollo Studio — the graph management platform — is the primary competitive differentiator for teams building federated supergraphs.

graphql-yoga reaches approximately 800,000 weekly downloads, growing as The Guild (Envelop, GraphQL Code Generator, Pothos) ecosystem gains mindshare. Yoga's edge runtime support makes it the only option here for Cloudflare Workers or Vercel Edge deployments of GraphQL APIs. Its composable Envelop plugin system — providing response caching, rate limiting, depth limiting, and usage reporting through standardized plugins — competes directly with Apollo's built-in monitoring features.

mercurius sits at approximately 400,000 weekly downloads, serving the Fastify community specifically. Its JIT compilation advantage — measurable in benchmarks at approximately 30% throughput improvement for cached queries — is relevant for high-traffic APIs where GraphQL server processing overhead is material. Teams already running Fastify services choose mercurius naturally; teams choosing a GraphQL server in isolation rarely start with mercurius.

Subscriptions, Real-time Patterns, and Transport Options

GraphQL subscriptions enable real-time data delivery and represent one of the more complex aspects of server implementation choice.

Transport layer selection is the first decision for subscriptions. WebSockets are the traditional transport, but newer alternatives exist. graphql-sse (Server-Sent Events for GraphQL) is a simpler alternative for scenarios that don't require bidirectional communication — SSE connections are one-way (server to client) and work over HTTP, avoiding WebSocket infrastructure complexity. GraphQL Yoga natively supports both WebSocket subscriptions (via graphql-ws) and SSE subscriptions with zero additional configuration.

Apollo Server's subscription support changed significantly in Apollo Server 4. The built-in subscription support from Apollo Server 2 was removed; subscriptions now require the @graphql-yoga/apollo-server-plugin or a separate subscriptions-transport-ws setup. For most teams upgrading from Apollo Server 2 to 4 or starting new, this means GraphQL Yoga is now the recommended subscription server from the Apollo ecosystem itself. Apollo Server 4 is optimized for request/response operations (queries and mutations) in serverless and edge environments.

Mercurius subscriptions are implemented via WebSocket using the graphql-ws protocol. Fastify's native WebSocket support (through @fastify/websocket) integrates cleanly with Mercurius's subscription system. For high-concurrency subscription servers, Mercurius on Fastify performs significantly better than the alternatives — Fastify's architecture is optimized for handling many concurrent connections with low memory overhead per connection.

PubSub and subscription filtering patterns are identical across all three servers since they implement the same GraphQL subscription specification. The publish / subscribe pattern (using an in-process EventEmitter for development, Redis Pub/Sub or Kafka for production) is framework-agnostic. The subscription resolver's subscribe function returns an async iterator; the server delivers each emitted value to subscribed clients. For multi-instance deployments (horizontal scaling), the PubSub transport must be external (Redis, Kafka, NATS) since an in-process EventEmitter only delivers to subscribers on the same process.

Methodology

Download data from npm registry (weekly average, February 2026). Feature comparison based on graphql-yoga v5.x, @apollo/server v4.x, and mercurius v14.x.

Compare GraphQL and API packages on PkgPulse →

Compare Apollo Server and GraphQL Yoga package health on PkgPulse.

See also: DataLoader vs p-batch vs graphql-batch and pothos vs TypeGraphQL vs nexus, better-sqlite3 vs libsql vs sql.js.

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.