GraphQL
debt(d7/e5/b7/t7)
Closest to 'only careful code review or runtime testing' (d7). The detection_hints list graphql-playground and datadog — neither automatically flags N+1 resolver patterns, missing depth limits, or introspection exposure at write time. The code_pattern confirms automated detection is 'no', meaning these issues surface only under load testing or manual review, not at lint/compile time.
Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix calls for implementing DataLoader for every relation AND adding query depth/complexity limits. DataLoader adoption typically requires wrapping every resolver that loads related data — touching multiple resolver files — and adding middleware or schema directives for complexity limits. This is more than a one-line swap but less than a full architectural rework.
Closest to 'strong gravitational pull' (e7). GraphQL is an API-layer architectural choice that applies across all web/api contexts. Every resolver, every new data relationship, every security configuration (introspection, rate limiting, depth limits) is shaped by this decision. Future maintainers must understand GraphQL semantics, DataLoader patterns, and query complexity management — a persistent tax on every new feature touching the API.
Closest to 'serious trap — contradicts how a similar concept works elsewhere' (t7). The misconception is explicit: developers assume GraphQL always outperforms REST by eliminating over-fetching, but without DataLoader batching the N+1 problem makes it significantly slower. This directly contradicts the marketing narrative and the intuition developers bring from REST, making it a serious cognitive trap that competent developers reliably fall into.
Also Known As
TL;DR
Explanation
GraphQL (Facebook, 2015) allows clients to define the shape and depth of data they require in a single request, rather than fetching from multiple REST endpoints and discarding unnecessary fields. A strongly-typed schema defines all available types and operations (queries, mutations, subscriptions). PHP implementations include webonyx/graphql-php and API Platform with GraphQL support. Security considerations include: depth limiting and complexity analysis to prevent DoS, disabling introspection in production, field-level authorisation, and N+1 protection with DataLoader-style batching.
Diagram
flowchart LR
CLIENT2[Client specifies<br/>exact fields needed] --> GQL_REQ[Single endpoint<br/>POST /graphql]
GQL_REQ --> RESOLVER[Resolvers fetch data<br/>per field]
RESOLVER --> DB_GQL[(Database APIs)]
DB_GQL --> RESP_GQL[Response with<br/>only requested fields]
subgraph Problems
N1_GQL[N+1 queries<br/>without DataLoader]
COMPLEX[Complex queries<br/>exhaust server]
end
subgraph Solutions
DATALOADER[DataLoader batching]
DEPTH_LIMIT[Query depth limiting]
COMPLEXITY[Complexity analysis]
end
style CLIENT2 fill:#1f6feb,color:#fff
style RESP_GQL fill:#238636,color:#fff
style N1_GQL fill:#f85149,color:#fff
style DATALOADER fill:#238636,color:#fff
Common Misconception
Why It Matters
Common Mistakes
- Not implementing query depth or complexity limits — attackers can craft deeply nested queries that exhaust server resources.
- N+1 query problem in resolvers — each list item triggers a separate database query; use DataLoader batching.
- Exposing the entire data model via introspection in production — disable introspection for public APIs.
- Not rate-limiting per query complexity — a complex query costs more than a simple one but may count as one request.
Code Examples
// Resolver with N+1 problem:
'posts' => function($root, $args, $ctx) {
$posts = Post::all(); // 1 query
return $posts; // Each post triggers author query — N+1
},
'author' => function($post) {
return User::find($post->user_id); // 1 query per post!
}
// PHP GraphQL with webonyx/graphql-php
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
\$queryType = new ObjectType([
'name' => 'Query',
'fields' => [
'user' => [
'type' => \$userType,
'args' => ['id' => Type::nonNull(Type::int())],
'resolve' => fn(\$root, \$args) => User::find(\$args['id']),
],
],
]);
// Client query — fetch only needed fields (no over-fetching)
// query { user(id: 1) { name email orders { total } } }
// Mutations change data
// mutation { placeOrder(cartId: 42) { id total status } }