Concept
The beginner framing: GraphQL's flexibility, letting clients ask for exactly the data they want, in whatever nested shape they want, is exactly what makes it vulnerable to a class of abuse REST's fixed endpoints mostly don't face: a client can construct a single query that's disproportionately, deliberately expensive to execute.
Query depth limiting: rejected at validation, before any resolver runs
query {
comment { replies { replies { replies { replies { replies { id } } } } } }
}import depthLimit from "graphql-depth-limit";
validate(schema, document, [...specifiedRules, depthLimit(3)]);Confirmed by running this exact validation: a deeply nested query (5+ levels, a classic self-referencing-comments DoS pattern) is rejected outright, "exceeds maximum operation depth of 3", while a shallow query passes with zero validation errors. The critical detail, confirmed by how this is wired in: depth limiting is a validation rule, running as part of GraphQL's own validate() step, before execution begins. A malicious deep query never reaches a single resolver, it's rejected at the door, not executed and then discarded, which matters because the entire point is preventing the cost of execution, not cleaning up after it.
Query complexity: depth alone doesn't catch everything
query {
users(first: 10000) { posts(first: 10000) { comments(first: 10000) { id } } }
}Depth limiting alone wouldn't catch this, it's only 3 levels deep, well under a depth limit of, say, 5. But it's requesting potentially hundreds of millions of comment objects (10,000 × 10,000 × 10,000) through pagination arguments, not nesting depth. Query complexity analysis extends the same validation-time-rejection idea to account for this: assigning a "cost" to each field (often factoring in list-size arguments like first), summing the total cost of a query, and rejecting anything over a configured budget, the same validation-before-execution principle as depth limiting, applied to a dimension depth limiting alone can't see.
Introspection in production: confirmed, a genuine tradeoff
const server = new ApolloServer({ typeDefs, resolvers, introspection: false });Confirmed by running this exact configuration and sending an introspection query ({ __schema { types { name } } }) against it: the request is rejected with a clear, specific error, "GraphQL introspection is not allowed by Apollo Server". Introspection, letting clients query the schema's own structure, is genuinely valuable for legitimate tooling (GraphiQL, code generators, IDE autocomplete), but leaving it enabled on a public production endpoint also hands anyone your entire API surface for free: every type, field, argument, and deprecation note, without needing any documentation or prior knowledge. This is a real, deliberate tradeoff most teams resolve by disabling introspection on public production endpoints (or gating it behind authentication) while keeping it enabled in development/staging where the tooling benefit matters more than the exposure risk.