Skip to main content

graphql-yoga vs apollo-server vs mercurius: GraphQL Servers in Node.js (2026)

·PkgPulse Team

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

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 →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.