graphql-yoga vs apollo-server vs mercurius: GraphQL Servers in Node.js (2026)
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
| Feature | graphql-yoga | @apollo/server | mercurius |
|---|---|---|---|
| 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 | ✅ | ✅ | ✅ |
| Performance | Fast | Moderate | Fastest |
| 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.