📘 How-to

GraphQL API security best practices: a practical hardening guide

A step-by-step guide to hardening GraphQL APIs with persisted queries, depth limits, field-level auth, rate limiting, and production-safe error handling.

By the end of this guide you will have a GraphQL API that rejects unknown queries, caps execution cost, and checks permissions inside every resolver. You need a working GraphQL server and the ability to add validation rules, middleware, and server configuration.

  1. Disable introspection in production. Set the Apollo Server or framework option that turns introspection off, and point your frontend team at a schema registry or static SDL file instead. The deciding factor is that the server must not respond to __schema or __type queries in production, because attackers can use them to map admin mutations and hidden fields.
  2. Enforce a query-depth limit as a validation rule. Use a library such as graphql-depth-limit, set a threshold around 5–10 based on your real schema, and apply it before execution. It fails only if the rule runs before resolvers and has no broad exemptions.
  3. Add query-complexity scoring. Use graphql-query-complexity or Apollo Server's cost analysis to assign higher weights to fields that perform database joins or call external APIs, and set a strict maximum cost per query. The cap works only when the scores reflect actual resolver work, not a flat count of fields.
  4. Enforce persisted, allowlisted queries. Extract every query from your client code at build time, hash it, register the hashes with the server, and reject any query the server has not seen before. Apollo Server supports Automatic Persisted Queries for the hash exchange. For first-party clients this should be strict; for public APIs, combine it with the following steps rather than skip them.
  5. Rate-limit per user identity and IP at the operation level, not just per HTTP request. Configure limits for each user and each IP, and treat batched requests as multiple operations. The control is meaningless if one HTTP call can still run dozens of aliased operations for free.
  6. Cap operations per batch and aliases per query. Set hard limits on how many operations a single request can contain and how many times one field can be aliased. This closes the gap where attackers combine alias1: user, alias2: user, and so on inside one request.
  7. Validate every input before it reaches business logic. Do not rely on GraphQL scalar coercion alone; check IDs, enums, arrays, and pagination sizes. Weak validation lets malformed arguments reach resolvers and become injection vectors.
  8. Implement field-level authorization in every resolver. Check the user's rights on each object and field, not just at the gateway; use directive-based authorization if you want the rules visible in the schema. Field-level checks matter because a gateway token only proves identity, not entitlement to a given field.
  9. Limit request body size at the reverse proxy or server. Set a maximum JSON payload size—often a few hundred kilobytes—to stop memory exhaustion from oversized queries or huge variable blobs.
  10. Sanitize errors and log query patterns. Return generic messages in production so stack traces and SQL hints do not leak, while logging depth, cost, operation name, and timing for anomaly detection.

Common mistakes

  • Leaving introspection enabled "for the frontend team" in production.
  • Trusting API-gateway authentication without checking inside resolvers.
  • Setting a depth limit of 20 or higher, or skipping it for trusted clients.
  • Rate-limiting only by IP address, which ignores logged-in user batches and is trivial to route around.
  • Treating GraphQL scalar validation as sufficient input sanitization.
  • Returning verbose resolver errors to clients instead of generic production messages.

People also search for

Discussion 0

Nothing has been said yet. Start it.

Log in to join the discussion

🛡️Safe SearchAlways on
Fast ResultsInstant answers
🔒Private by designYour search, your privacy