These packages represent the primary ways to build GraphQL servers in the Node.js ecosystem, ranging from standalone servers to framework-specific integrations. @apollo/server is the official, standalone server from the Apollo team, designed to be framework-agnostic. @nestjs/graphql is a deep integration for the NestJS framework, leveraging decorators and dependency injection. express-graphql was the original reference implementation for Express but is now deprecated. graphql-yoga is a modern, batteries-included server built on GraphQL Envelope, focusing on ease of setup and web standards compliance.
Building a GraphQL server in Node.js used to mean picking a single reference implementation. Today, you have distinct choices depending on whether you need a standalone engine, a framework-integrated solution, or a modern, batteries-included toolkit. Let's break down how @apollo/server, @nestjs/graphql, express-graphql, and graphql-yoga handle the core challenges of production GraphQL development.
Before comparing features, we must address the lifecycle status of these packages. This is the most critical architectural decision point.
express-graphql is officially deprecated. The maintainers have archived the repository and explicitly stated it should not be used for new projects. It lacks support for modern GraphQL specifications and security updates.
// express-graphql: DEPRECATED - Do not use in new projects
// npm install express-graphql (NOT RECOMMENDED)
import { graphqlHTTP } from 'express-graphql';
// This pattern is legacy and unsupported
app.use('/graphql', graphqlHTTP({
schema: mySchema,
graphiql: true,
}));
Recommendation: If you are starting fresh, skip express-graphql. If you are maintaining an old codebase, plan a migration to @apollo/server or graphql-yoga immediately.
The other three packages (@apollo/server, @nestjs/graphql, graphql-yoga) are actively maintained and suitable for production.
How you define your schema and resolvers differs significantly based on whether you choose a standalone server or a framework integration.
@apollo/server follows a standalone, programmatic approach. You define your schema and resolvers separately and pass them to the server instance. It gives you full control over the lifecycle.
// @apollo/server: Standalone setup
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
const server = new ApolloServer({
typeDefs: `type Query { hello: String }`,
resolvers: { Query: { hello: () => 'world' } },
});
const { url } = await startStandaloneServer(server, { listen: { port: 4000 } });
console.log(`π Server ready at ${url}`);
@nestjs/graphql uses a code-first or schema-first approach tightly coupled with NestJS decorators. It automatically generates the schema from your TypeScript classes, leveraging dependency injection for resolvers.
// @nestjs/graphql: Decorator-based setup
import { Resolver, Query } from '@nestjs/graphql';
@Resolver()
export class HelloResolver {
@Query(() => String)
hello() {
return 'world';
}
}
// In your module:
// @Module({ providers: [HelloResolver] })
// export class HelloModule {}
graphql-yoga focuses on simplicity and web standards. It often infers the schema from your resolvers or accepts a simple configuration object, aiming for minimal boilerplate.
// graphql-yoga: Minimalist setup
import { createYoga } from 'graphql-yoga';
import { createServer } from 'http';
const yoga = createYoga({
schema: {
typeDefs: `type Query { hello: String }`,
resolvers: { Query: { hello: () => 'world' } },
},
});
const server = createServer(yoga);
server.listen(4000);
Real-world applications need logging, authentication, and caching. The mechanism for adding these cross-cutting concerns varies by package.
@apollo/server uses a robust plugin system. Plugins can hook into various stages of the request lifecycle, such as requestDidStart or willSendResponse.
// @apollo/server: Plugin system
const loggingPlugin = {
async requestDidStart() {
console.log('Request started');
return {
async willSendResponse() {
console.log('Response sent');
},
};
},
};
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [loggingPlugin],
});
@nestjs/graphql leverages the existing NestJS ecosystem. You use Guards, Interceptors, and Filters just like you would for REST endpoints. This provides a unified experience across HTTP and GraphQL.
// @nestjs/graphql: Using NestJS Interceptors
import { UseInterceptors } from '@nestjs/common';
import { LoggingInterceptor } from './logging.interceptor';
@Resolver()
export class HelloResolver {
@Query(() => String)
@UseInterceptors(LoggingInterceptor)
hello() {
return 'world';
}
}
graphql-yoga utilizes "Envelopes" (plugins based on the GraphQL Envelope project). These are lightweight, composable plugins that wrap the execution context.
// graphql-yoga: Envelope plugins
import { useLogger } from '@envelop/core';
import { createYoga } from 'graphql-yoga';
const yoga = createYoga({
schema,
plugins: [
useLogger(), // Built-in logging plugin
],
});
Handling WebSocket connections for subscriptions is often a pain point. Here is how each package addresses it.
@apollo/server supports subscriptions but requires you to manage the WebSocket server implementation (e.g., using ws or socket.io) and wire it up manually. This offers flexibility but adds setup complexity.
// @apollo/server: Manual WebSocket setup
import { makeExecutableSchema } from '@graphql-tools/schema';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
const schema = makeExecutableSchema({ typeDefs, resolvers });
const wsServer = new WebSocketServer({ server, path: '/graphql' });
useServer({ schema }, wsServer);
@nestjs/graphql abstracts the WebSocket complexity. You define subscriptions using decorators, and NestJS handles the underlying transport configuration via its built-in WebSocket adapter.
// @nestjs/graphql: Decorator-based subscriptions
import { Subscription } from '@nestjs/graphql';
import { filter } from 'rxjs';
@Resolver()
export class NotificationResolver {
@Subscription(() => String)
newNotification() {
// Returns an Observable
return this.pubSub.asyncIterator('notificationAdded');
}
}
graphql-yoga has subscriptions built-in and enabled by default. It handles the protocol negotiation and WebSocket management automatically without extra configuration.
// graphql-yoga: Built-in subscriptions
const yoga = createYoga({
schema: {
typeDefs: `type Subscription { count: Int! }`,
resolvers: {
Subscription: {
count: {
subscribe: async function* () {
let count = 0;
while (true) {
yield { count: count++ };
await new Promise((r) => setTimeout(r, 1000));
}
},
},
},
},
},
// No extra WebSocket server setup needed
});
How these servers interact with standard web requests (Fetch API) affects their portability across environments like Cloudflare Workers, Vercel Edge, or standard Node.js.
@apollo/server recently updated to support the Fetch API standard, making it more portable. However, its primary design history is rooted in Node.js streams and Express-style request/response objects.
// @apollo/server: Fetch API support
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
// Works with standard Fetch Request/Response objects
const server = new ApolloServer({ typeDefs, resolvers });
// Can be deployed to edge runtimes with appropriate adapters
@nestjs/graphql is heavily tied to the Node.js runtime and the Express/Fastify underlying HTTP servers. It is not designed for edge runtimes or serverless environments that do not support full Node.js APIs.
// @nestjs/graphql: Node.js specific
// Requires a full Node.js environment for NestJS application bootstrapping
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();
graphql-yoga is built from the ground up on the Fetch API. It treats every request as a standard Request object and returns a Response object. This makes it incredibly easy to deploy anywhere that supports Fetch, including edge networks.
// graphql-yoga: Native Fetch API
const yoga = createYoga({ schema });
// Handler works in Node, Cloudflare Workers, Vercel, etc.
export const handler = (req: Request) => yoga.handle(req);
Despite their differences, these active libraries share core GraphQL capabilities.
All active packages support the standard GraphQL Schema Definition Language (SDL).
// Common SDL used across all packages
const typeDefs = `
type Query {
user(id: ID!): User
}
type User {
id: ID!
name: String!
}
`;
All packages enable introspection queries by default in development, allowing tools like GraphiQL to explore the API.
// Query works identically in Apollo, NestJS, and Yoga
query IntrospectionQuery {
__schema {
types {
name
}
}
}
Each allows you to pass a context object to resolvers, enabling access to user data or database connections.
// Apollo
context: async ({ req }) => ({ user: await getUser(req.headers) });
// NestJS
@Context() context: any;
// Yoga
context: ({ request }) => ({ user: getUser(request) });
| Feature | @apollo/server | @nestjs/graphql | graphql-yoga | express-graphql |
|---|---|---|---|---|
| Status | β Active | β Active | β Active | β Deprecated |
| Primary Use | Standalone / Flexible | NestJS Apps | Modern / Edge Ready | Legacy Express |
| Setup Style | Programmatic | Decorators / DI | Config / Minimal | Middleware |
| Subscriptions | Manual Wiring | Built-in (Decorators) | Built-in (Auto) | Limited / Manual |
| Portability | High (Node + Edge) | Low (Node Only) | Very High (Fetch) | Low (Express) |
| Ecosystem | Large Plugin Market | NestJS Modules | Envelope Plugins | Abandoned |
@apollo/server is the safe, enterprise-grade choice. It is like a reliable Swiss Army knife β it might require a few extra steps to set up specific tools (like subscriptions), but it has a plugin for everything and is backed by a massive community. Choose this if you need federation, complex plugin chains, or maximum flexibility.
@nestjs/graphql is the structural choice for teams already invested in NestJS. It feels like building with LEGO blocks β everything snaps together perfectly with decorators and dependency injection. If your team loves TypeScript and strict architecture, this reduces boilerplate and enforces consistency.
graphql-yoga is the modern, streamlined choice. It is like a Tesla β it just works out of the box with the latest standards (Fetch API, subscriptions, file uploads). Choose this for new projects, especially if you plan to deploy to edge networks or want to avoid configuration hell.
Final Thought: Avoid express-graphql entirely. For new projects, the choice is between the extensibility of Apollo, the structure of NestJS, or the modern simplicity of Yoga. Your existing tech stack and deployment targets should drive this decision.
Choose @apollo/server if you need the industry-standard server with robust plugin support, detailed tracing, and federation capabilities. It is ideal for teams that want a stable, well-documented core that works with Express, Fastify, or AWS Lambda without being locked into a specific application framework.
Choose @nestjs/graphql if your backend is already built with NestJS and you want to leverage its dependency injection, decorators, and modular architecture. It is the best fit for enterprise applications where type safety, strict structure, and integration with other NestJS features (like guards and interceptors) are priorities.
Do NOT choose express-graphql for new projects. It has been officially deprecated by its maintainers and receives no further updates or security patches. Existing projects using it should plan a migration to @apollo/server or graphql-yoga to ensure long-term stability and security.
Choose graphql-yoga if you want a modern, zero-config experience that adheres strictly to web standards. It is perfect for developers who want built-in features like file uploads, subscriptions, and persisted queries without hunting for third-party plugins, or for those migrating away from deprecated solutions quickly.
@apollo/serverThis
@apollo/serverpackage is new since Apollo Server 4. Previous major versions of Apollo Server used a set of package names starting withapollo-server, such asapollo-server,apollo-server-express,apollo-server-core, etc.
Announcement: Join 1000+ engineers at GraphQL Summit 2025 by Apollo for talks, workshops, and office hours. Oct 6-8, 2025 in San Francisco. Get your pass here ->
Apollo Server is an open-source, spec-compliant GraphQL server that's compatible with any GraphQL client, including Apollo Client. It's the best way to build a production-ready, self-documenting GraphQL API that can use data from any source.
You can use Apollo Server as:
Apollo Server provides a simple API for integrating with any Node.js web framework or serverless environment. The @apollo/server package itself ships with a minimally-configurable, standalone web server which handles CORS and body parsing out of the box. Integrations with other environments are community-maintained.
Apollo Server provides:
Full documentation for Apollo Server is available on our documentation site. This README shows the basics of getting a server running (both standalone and with Express), but most features are only documented on our docs site.
You can also check out the getting started guide in the Apollo Server docs for more details, including examples in both TypeScript and JavaScript.
Apollo Server's standalone server lets you get a GraphQL server up and running quickly without needing to set up an HTTP server yourself. It allows all the same configuration of GraphQL logic as the Express integration, but does not provide the ability to make fine-grained tweaks to the HTTP-specific behavior of your server.
First, install Apollo Server and the JavaScript implementation of the core GraphQL algorithms:
npm install @apollo/server graphql
Then, write the following to server.mjs. (By using the .mjs extension, Node lets you use the await keyword at the top level.)
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
// The GraphQL schema
const typeDefs = `#graphql
type Query {
hello: String
}
`;
// A map of functions which return data for the schema.
const resolvers = {
Query: {
hello: () => 'world',
},
};
const server = new ApolloServer({
typeDefs,
resolvers,
});
const { url } = await startStandaloneServer(server);
console.log(`π Server ready at ${url}`);
Now run your server with:
node server.mjs
Open the URL it prints in a web browser. It will show Apollo Sandbox, a web-based tool for running GraphQL operations. Try running the operation query { hello }!
Apollo Server's Express middleware lets you run your GraphQL server as part of an app built with Express, the most popular web framework for Node.
First, install Apollo Server, its Express middleware, the JavaScript implementation of the core GraphQL algorithms, Express, and the standard Express middleware package for CORS headers:
npm install @apollo/server @as-integrations/express5 graphql express cors
If using Typescript you may also need to install additional type declaration packages as development dependencies to avoid common errors when importing the above packages (i.e. Could not find a declaration file for module 'cors'):
npm install --save-dev @types/cors @types/express
Then, write the following to server.mjs. (By using the .mjs extension, Node lets you use the await keyword at the top level.)
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@as-integrations/express5';
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer'
import express from 'express';
import http from 'http';
import cors from 'cors';
// The GraphQL schema
const typeDefs = `#graphql
type Query {
hello: String
}
`;
// A map of functions which return data for the schema.
const resolvers = {
Query: {
hello: () => 'world',
},
};
const app = express();
const httpServer = http.createServer(app);
// Set up Apollo Server
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],
});
await server.start();
app.use(
cors(),
express.json(),
expressMiddleware(server),
);
await new Promise((resolve) => httpServer.listen({ port: 4000 }, resolve));
console.log(`π Server ready at http://localhost:4000`);
Now run your server with:
node server.mjs
Open the URL it prints in a web browser. It will show Apollo Sandbox, a web-based tool for running GraphQL operations. Try running the operation query { hello }!