apollo-server, express-graphql, and graphql-tools are foundational packages in the Node.js GraphQL ecosystem, each serving distinct but sometimes overlapping roles. apollo-server is a standalone GraphQL server implementation that includes built-in support for Express and other web frameworks, offering features like schema stitching, subscriptions, and developer tooling. express-graphql is a minimal middleware that adds GraphQL execution to an existing Express application, relying on the core graphql package for query resolution. graphql-tools provides utilities for constructing and manipulating GraphQL schemas programmatically, notably through the makeExecutableSchema function, and is often used alongside server implementations to define resolvers and type definitions cleanly.
When setting up a GraphQL API in Node.js, developers often encounter three key packages: apollo-server, express-graphql, and graphql-tools. While they all relate to GraphQL, they serve different purposes—one is a full server, one is a lightweight middleware, and one is a schema utility library. Understanding their roles prevents architectural missteps, especially since one of them is no longer maintained.
First, a critical update: express-graphql is officially deprecated. The npm page states: "This package is no longer maintained. We recommend using Apollo Server instead." This means you should not use express-graphql in new projects. We include it here only for context and migration guidance.
In contrast, both apollo-server and graphql-tools are actively maintained and widely used in production systems.
apollo-server: Full-Featured GraphQL Serverapollo-server is a complete GraphQL server implementation. It handles HTTP requests, parses queries, executes resolvers, formats errors, and serves GraphQL Playground (in development). It can run standalone or integrate with Express, Koa, Hapi, and more.
// apollo-server: Standalone server
import { ApolloServer } from 'apollo-server';
import { typeDefs, resolvers } from './schema.js';
const server = new ApolloServer({ typeDefs, resolvers });
server.listen().then(({ url }) => console.log(`Server ready at ${url}`));
express-graphql: Minimal Express Middleware (Deprecated)express-graphql is just a middleware that plugs GraphQL execution into an Express app. It delegates almost everything to the graphql package and offers minimal configuration.
// express-graphql: Deprecated usage
import express from 'express';
import { graphqlHTTP } from 'express-graphql';
import { schema } from './schema.js';
const app = express();
app.use('/graphql', graphqlHTTP({ schema, graphiql: true }));
app.listen(4000);
🔴 Do not use this in new code. Migrate to Apollo Server if you're maintaining an old codebase.
graphql-tools: Schema Construction Utilitiesgraphql-tools doesn’t run a server—it helps you build schemas. Its flagship function, makeExecutableSchema, combines type definitions (SDL) and resolver objects into a single executable schema object that any GraphQL server can use.
// graphql-tools: Schema creation
import { makeExecutableSchema } from 'graphql-tools';
const typeDefs = `
type Query {
hello: String
}
`;
const resolvers = {
Query: { hello: () => 'Hello world!' }
};
const schema = makeExecutableSchema({ typeDefs, resolvers });
// Now pass `schema` to Apollo Server, Yoga, or any other server
graphql-tools with apollo-serverMost professional Apollo Server setups use graphql-tools under the hood (even if indirectly). Here’s how they work together:
// Combined: graphql-tools + apollo-server
import { ApolloServer } from 'apollo-server';
import { makeExecutableSchema } from 'graphql-tools';
const schema = makeExecutableSchema({
typeDefs: `type Query { user(id: ID!): User } type User { id: ID! name: String }`,
resolvers: {
Query: { user: (_, { id }) => ({ id, name: 'Alice' }) }
}
});
const server = new ApolloServer({ schema });
server.listen();
Note: Modern Apollo Server versions accept typeDefs and resolvers directly and internally use graphql-tools, so explicit makeExecutableSchema isn’t always needed—but it’s still useful for advanced cases like schema merging.
express-graphql Today?Beyond deprecation, express-graphql lacks:
Apollo Server provides all these out of the box.
graphql-toolsWhere graphql-tools shines is in complex schema management:
Before Apollo Federation, graphql-tools enabled combining multiple GraphQL services into one schema:
import { stitchSchemas } from 'graphql-tools';
const gatewaySchema = stitchSchemas({
subschemas: [
{ schema: userServiceSchema },
{ schema: orderServiceSchema }
]
});
Note: For new microservices, use Apollo Federation instead—but
graphql-toolsstill powers many stitching-based systems.
Generate realistic mock data from your schema:
import { addMocksToSchema } from 'graphql-tools';
const mockedSchema = addMocksToSchema({ schema });
// Now all resolvers return auto-generated mock data
This is invaluable for frontend teams working before backend APIs are ready.
Provides structured error formatting, masking stack traces in production, and extension points:
const server = new ApolloServer({
formatError: (err) => ({
message: err.message,
code: err.extensions?.code || 'INTERNAL_ERROR'
})
});
Offers basic error handling but no customization hooks beyond a global formatError option.
Not applicable—it doesn’t handle runtime execution, so no error formatting.
| Scenario | Recommended Package(s) |
|---|---|
| New GraphQL API from scratch | apollo-server (with or without explicit graphql-tools) |
| Need modular schema design or mocking | graphql-tools + any server (e.g., Apollo Server) |
| Maintaining old Express app with GraphQL | Migrate from express-graphql to apollo-server-express |
| Building a GraphQL gateway | Use @apollo/server with Federation, not graphql-tools stitching |
apollo-server is your go-to for production GraphQL servers—feature-rich, secure, and well-supported.express-graphql is deprecated. Avoid it in new projects; plan to migrate existing uses.graphql-tools is not a server—it’s a toolkit for schema authoring, testing, and composition. It complements server libraries rather than replacing them.In modern architectures, you’ll often see apollo-server and graphql-tools used together: the former runs the API, the latter builds the schema. Understanding this division of labor leads to cleaner, more maintainable GraphQL implementations.
Choose express-graphql only if you’re maintaining a legacy Express app that already uses this middleware and you need minimal GraphQL integration without additional dependencies. Note that it is officially deprecated and should not be used in new projects — consider migrating to Apollo Server or another modern alternative.
Choose apollo-server if you need a production-ready, batteries-included GraphQL server with integrated tooling like Apollo Studio, persisted queries, and built-in support for subscriptions and file uploads. It’s ideal for teams building full-featured GraphQL APIs who want conventions, security defaults, and extensibility without assembling low-level pieces manually.
Choose graphql-tools when you need flexible, programmatic schema construction—especially for modular schema design, schema merging, or mock generation—without tying yourself to a specific server implementation. It pairs well with any GraphQL server (including Apollo Server) and is essential for advanced schema workflows like federation or testing.
Create a GraphQL HTTP server with any HTTP web framework that supports connect styled middleware, including Connect itself, Express and Restify.
npm install --save express-graphql
This module includes a TypeScript declaration file to enable auto complete in compatible editors and type information for TypeScript projects.
Just mount express-graphql as a route handler:
const express = require('express');
const { graphqlHTTP } = require('express-graphql');
const app = express();
app.use(
'/graphql',
graphqlHTTP({
schema: MyGraphQLSchema,
graphiql: true,
}),
);
app.listen(4000);
Use .get or .post (or both) rather than .use to configure your route handler. If you want to show GraphiQL in the browser, set graphiql: true on your .get handler.
const restify = require('restify');
const { graphqlHTTP } = require('express-graphql');
const app = restify.createServer();
app.post(
'/graphql',
graphqlHTTP({
schema: MyGraphQLSchema,
graphiql: false,
}),
);
app.get(
'/graphql',
graphqlHTTP({
schema: MyGraphQLSchema,
graphiql: true,
}),
);
app.listen(4000);
The graphqlHTTP function accepts the following options:
schema: A GraphQLSchema instance from GraphQL.js.
A schema must be provided.
graphiql: If true, presents GraphiQL when the GraphQL endpoint is
loaded in a browser. We recommend that you set graphiql to true when your
app is in development, because it's quite useful. You may or may not want it
in production.
Alternatively, instead of true you can pass in an options object:
defaultQuery: An optional GraphQL string to use when no query
is provided and no stored query exists from a previous session.
If undefined is provided, GraphiQL will use its own default query.
headerEditorEnabled: An optional boolean which enables the header editor when true.
Defaults to false.
rootValue: A value to pass as the rootValue to the graphql()
function from GraphQL.js/src/execute.js.
context: A value to pass as the context to the graphql()
function from GraphQL.js/src/execute.js. If context is not provided, the
request object is passed as the context.
pretty: If true, any JSON response will be pretty-printed.
extensions: An optional function for adding additional metadata to the
GraphQL response as a key-value object. The result will be added to the
"extensions" field in the resulting JSON. This is often a useful place to
add development time metadata such as the runtime of a query or the amount
of resources consumed. This may be an async function. The function is
given one object as an argument: { document, variables, operationName, result, context }.
validationRules: Optional additional validation rules queries must
satisfy in addition to those defined by the GraphQL spec.
customValidateFn: An optional function which will be used to validate
instead of default validate from graphql-js.
customExecuteFn: An optional function which will be used to execute
instead of default execute from graphql-js.
customFormatErrorFn: An optional function which will be used to format any
errors produced by fulfilling a GraphQL operation. If no function is
provided, GraphQL's default spec-compliant formatError function will be used.
customParseFn: An optional function which will be used to create a document
instead of the default parse from graphql-js.
formatError: is deprecated and replaced by customFormatErrorFn. It will be
removed in version 1.0.0.
In addition to an object defining each option, options can also be provided as
a function (or async function) which returns this options object. This function
is provided the arguments (request, response, graphQLParams) and is called
after the request has been parsed.
The graphQLParams is provided as the object { query, variables, operationName, raw }.
app.use(
'/graphql',
graphqlHTTP(async (request, response, graphQLParams) => ({
schema: MyGraphQLSchema,
rootValue: await someFunctionToGetRootValue(request),
graphiql: true,
})),
);
Once installed at a path, express-graphql will accept requests with
the parameters:
query: A string GraphQL document to be executed.
variables: The runtime values to use for any GraphQL query variables
as a JSON object.
operationName: If the provided query contains multiple named
operations, this specifies which operation should be executed. If not
provided, a 400 error will be returned if the query contains multiple
named operations.
raw: If the graphiql option is enabled and the raw parameter is
provided raw JSON will always be returned instead of GraphiQL even when
loaded from a browser.
GraphQL will first look for each parameter in the query string of a URL:
/graphql?query=query+getUser($id:ID){user(id:$id){name}}&variables={"id":"4"}
If not found in the query-string, it will look in the POST request body.
If a previous middleware has already parsed the POST body, the request.body
value will be used. Use multer or a similar middleware to add support
for multipart/form-data content, which may be useful for GraphQL mutations
involving uploading files. See an example using multer.
If the POST body has not yet been parsed, express-graphql will interpret it depending on the provided Content-Type header.
application/json: the POST body will be parsed as a JSON
object of parameters.
application/x-www-form-urlencoded: this POST body will be
parsed as a url-encoded string of key-value pairs.
application/graphql: The POST body will be parsed as GraphQL
query string, which provides the query parameter.
By default, the express request is passed as the GraphQL context.
Since most express middleware operates by adding extra data to the
request object, this means you can use most express middleware just by inserting it before graphqlHTTP is mounted. This covers scenarios such as authenticating the user, handling file uploads, or mounting GraphQL on a dynamic endpoint.
This example uses express-session to provide GraphQL with the currently logged-in session.
const session = require('express-session');
const { graphqlHTTP } = require('express-graphql');
const app = express();
app.use(session({ secret: 'keyboard cat', cookie: { maxAge: 60000 } }));
app.use(
'/graphql',
graphqlHTTP({
schema: MySessionAwareGraphQLSchema,
graphiql: true,
}),
);
Then in your type definitions, you can access the request via the third "context" argument in your resolve function:
new GraphQLObjectType({
name: 'MyType',
fields: {
myField: {
type: GraphQLString,
resolve(parentValue, args, request) {
// use `request.session` here
},
},
},
});
The GraphQL response allows for adding additional information in a response to
a GraphQL query via a field in the response called "extensions". This is added
by providing an extensions function when using graphqlHTTP. The function
must return a JSON-serializable Object.
When called, this is provided an argument which you can use to get information about the GraphQL request:
{ document, variables, operationName, result, context }
This example illustrates adding the amount of time consumed by running the provided query, which could perhaps be used by your development tools.
const { graphqlHTTP } = require('express-graphql');
const app = express();
app.use(session({ secret: 'keyboard cat', cookie: { maxAge: 60000 } }));
const extensions = ({
document,
variables,
operationName,
result,
context,
}) => {
return {
runTime: Date.now() - context.startTime,
};
};
app.use(
'/graphql',
graphqlHTTP((request) => {
return {
schema: MyGraphQLSchema,
context: { startTime: Date.now() },
graphiql: true,
extensions,
};
}),
);
When querying this endpoint, it would include this information in the result, for example:
{
"data": { ... }
"extensions": {
"runTime": 135
}
}
GraphQL's validation phase checks the query to ensure that it can be successfully executed against the schema. The validationRules option allows for additional rules to be run during this phase. Rules are applied to each node in an AST representing the query using the Visitor pattern.
A validation rule is a function which returns a visitor for one or more node Types. Below is an example of a validation preventing the specific field name metadata from being queried. For more examples see the specifiedRules in the graphql-js package.
import { GraphQLError } from 'graphql';
export function DisallowMetadataQueries(context) {
return {
Field(node) {
const fieldName = node.name.value;
if (fieldName === 'metadata') {
context.reportError(
new GraphQLError(
`Validation: Requesting the field ${fieldName} is not allowed`,
),
);
}
},
};
}
Disabling introspection does not reflect best practices and does not necessarily make your
application any more secure. Nevertheless, disabling introspection is possible by utilizing the
NoSchemaIntrospectionCustomRule provided by the graphql-js
package.
import { specifiedRules, NoSchemaIntrospectionCustomRule } from 'graphql';
app.use(
'/graphql',
graphqlHTTP((request) => {
return {
schema: MyGraphQLSchema,
validationRules: [...specifiedRules, NoSchemaIntrospectionCustomRule],
};
}),
);
getGraphQLParams(request: Request): Promise<GraphQLParams>
Given an HTTP Request, this returns a Promise for the parameters relevant to running a GraphQL request. This function is used internally to handle the incoming request, you may use it directly for building other similar services.
const { getGraphQLParams } = require('express-graphql');
getGraphQLParams(request).then((params) => {
// do something...
});
During development, it's useful to get more information from errors, such as
stack traces. Providing a function to customFormatErrorFn enables this:
customFormatErrorFn: (error) => ({
message: error.message,
locations: error.locations,
stack: error.stack ? error.stack.split('\n') : [],
path: error.path,
});