website/pages/docs/going-to-production.mdx
import { Callout } from 'nextra/components';
This guide covers key practices to prepare a server built with GraphQL.js for production use. Concerns include concerns include build optimization, caching, error handling, schema management, and operational monitoring.
GraphQL.js includes development-time checks that are useful during local testing. For v16 and earlier, these checks are enabled by default and should be disabled in production to reduce overhead. See Development Mode for further details about these checks and how to disable them. Starting in v17, development mode is disabled by default, and must be explicitly enabled for development environments.
<Callout type="info" emoji="ℹ️"> In v16 and earlier, development mode is enabled by default and must be explicitly disabled in production. Starting in v17, development mode is disabled by default and may require explicit enabling. See [Development Mode](./development-mode) for details and instructions. </Callout>GraphQL gives clients a lot of flexibility, which can be a strength or a liability depending on how it's used. In production, it's important to control how much of your schema is exposed and how much work a single query is allowed to do.
Common strategies for securing a schema include:
These techniques can help protect your server from accidental misuse or intentional abuse.
The most reliable way to protect your GraphQL endpoint from malicious requests is to only allow operations that you trust — those written by your own engineers — to be executed.
This technique is not suitable for public APIs that are intended to accept ad-hoc queries from third parties, but if your GraphQL API is only meant to power your own websites and apps then it is a simple yet incredibly effective technique to protect your API endpoint.
Implementing the trusted documents pattern is straightforward:
"query":"{...}") and
instead provide the document hash ("documentId": "sha256:...").This pattern not only improves security significantly by preventing malicious queries, it has a number of additional benefits:
Be careful not to confuse trusted documents (the key component of which are trust) with automatic persisted queries (APQ) which are a network optimization potentially open for anyone to use.
Additional resources:
fetch()(Unnecessary if you only allow trusted documents.)
Introspection lets clients query the structure of your schema, including types and fields. While helpful during development, it may be an unnecessary in production and disabling it may reduce your API's attack surface.
You can disable introspection in production, or only for unauthenticated users:
import {
validate,
specifiedRules,
NoSchemaIntrospectionCustomRule,
} from 'graphql';
const validationRules = isPublicRequest
? [...specifiedRules, NoSchemaIntrospectionCustomRule]
: specifiedRules;
Note that many developer tools rely on introspection to function properly. Use introspection control as needed for your tools and implementation.
(Can be a development-only concern if you only allow trusted documents.)
GraphQL allows deeply nested queries, which can be expensive to resolve. You can prevent this with query depth limits or cost analysis.
The following example shows how to limit query depth:
import depthLimit from 'graphql-depth-limit';
const validationRules = [depthLimit(10), ...specifiedRules];
Instead of depth, you can assign each field a cost and reject queries that exceed a total budget.
Tools like graphql-cost-analysis can help.
GraphQL doesn't include built-in authentication. Instead, you can attach user data to the request using middleware, and pass this through to the business logic where authorization should take place:
// From your business logic
const postRepository = {
getBody({ user, post }) {
if (user?.id && user.id === post.authorId) {
return post.body;
}
return null;
},
};
// Resolver for the `Post.body` field:
function Post_body(source, args, context, info) {
// return the post body only if the user is the post's author
return postRepository.getBody({ user: context.user, post: source });
}
For more details, see the Authentication and Middleware guide.
To prevent abuse, you can limit how often clients access specific operations or fields. The
graphql-rate-limit package lets
you define rate limits directly in your schema using custom directives.
For more control, you can also implement your own rate-limiting logic using the request context, such as limiting by user, client ID, or operation name.
In production, performance often depends on how efficiently your resolvers fetch and process data. GraphQL allows flexible queries, which means a single poorly optimized query can result in excessive database calls or slow response times.
The most common performance issue in GraphQL is the N+1 query problem, where nested resolvers
make repeated calls for related data. DataLoader helps avoid this by batching and caching
field-level fetches within a single request.
For more information on this issue and how to resolve it, see Solving the N+1 Problem with DataLoader.
You can apply caching at several levels, depending on your server architecture:
For larger applications, consider request-scoped caching or external systems like Redis to avoid memory growth and stale data.
Observability is key to diagnosing issues and ensuring your GraphQL server is running smoothly in production. This includes structured logs, runtime metrics, and distributed traces to follow requests through your system.
Use a structured logger to capture events in a machine-readable format. This makes logs easier to filter and analyze in production systems. Popular options include:
You might log things like:
Avoid logging sensitive data like passwords or access tokens.
Operational metrics help track the health and behavior of your server over time.
You can use tools like Prometheus or OpenTelemetry to capture query counts, resolver durations, and error rates.
There's no built-in GraphQL.js metrics hook, but you can wrap resolvers or use the execute
function directly to insert instrumentation.
Distributed tracing shows how a request flows through services and where time is spent. This is especially helpful for debugging performance issues.
GraphQL.js allows you to hook into the execution pipeline using:
execute: Trace the overall operationparse and validate: Trace early stepsformatResponse: Attach metadataStarting in v17, GraphQL.js also publishes lifecycle events on Node.js
node:diagnostics_channel tracing channels for parse, validation, execution,
subscription setup, variable coercion, root selection set execution, and field
resolution. These channels are mainly for application performance monitoring
(APM) tools and other tracing integrations, which can subscribe to the
graphql:* channels without changing application request code.
Tracing tools that work with GraphQL include:
How you handle errors in production affects both security and client usability. Avoid exposing internal details in errors, and return errors in a format clients can interpret consistently.
For more information on how GraphQL.js formats and processes errors, see Understanding GraphQL.js Errors.
By default, GraphQL.js includes full error messages and stack traces. In production, you may want to return a generic error to avoid leaking implementation details.
You can use a custom error formatter to control this:
import { GraphQLError } from 'graphql';
function formatError(error) {
if (process.env.NODE_ENV === 'production') {
return new GraphQLError('Internal server error');
}
return error;
}
This function can be passed to your server, depending on the integration.
GraphQL allows errors to include an extensions object, which you can use to add
metadata such as error codes. This helps clients distinguish between different types of
errors:
throw new GraphQLError('Forbidden', {
extensions: { code: 'FORBIDDEN' },
});
You can also create and throw custom error classes to represent specific cases, such as authentication or validation failures.
Schemas evolve over time, but removing or changing fields can break client applications. In production environments, it's important to make schema changes carefully and with clear migration paths.
Use the @deprecated directive to mark fields or enum values that are planned for removal.
Always provide a reason so clients know what to use instead:
type User {
oldField: String @deprecated(reason: "Use `newField` instead.")
}
Only remove deprecated fields once you're confident no clients depend on them.
You can compare your current schema against the previous version to detect breaking changes. Tools that support this include:
Integrate these checks into your CI/CD pipeline to catch issues before they reach production.
You should tailor your GraphQL server's behavior based on the runtime environment.
Example:
const isDev = process.env.NODE_ENV !== 'production';
app.use(
'/graphql',
graphqlHTTP({
schema,
graphiql: isDev,
customFormatErrorFn: formatError,
}),
);
Use this checklist to verify that your GraphQL.js server is ready for production. Before deploying, confirm the following checks are complete:
process.env.NODE_ENV to 'production'DataLoader is used to batch data fetchingextensions.code for consistent client handlingformatError function is used to control error output@deprecated and a clear reason