Senior 6 min · March 05, 2026

GraphQL vs REST — N+1 Query That Took Down a Product Page

When a GraphQL product page slowed from 200ms to 8 seconds due to N+1 queries, the DataLoader fix proved crucial—and REST avoids the problem entirely.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • REST organizes APIs around resources and HTTP verbs — predictable, cacheable, and universal.
  • GraphQL lets clients declare exact data shapes — solves overfetching and underfetching but adds complexity.
  • Choose REST when you have few clients, simple CRUD, or need aggressive HTTP caching.
  • Choose GraphQL for multiple client types, rapid iteration, or mobile-heavy apps.
  • The hybrid pattern — GraphQL at the edge, REST internally — is what senior teams actually run in production.
Plain-English First

Imagine you're at a restaurant. With REST, there's a fixed menu — you order the 'burger meal' and you get a burger, fries, AND a drink, even if you only wanted the burger. With GraphQL, you tell the waiter exactly what you want: 'just the burger, no fries, add extra cheese.' REST gives you predefined plates; GraphQL lets you build your own plate every time. Neither is wrong — it just depends on whether your customers always want the same thing.

Every modern app lives or dies by its API. Whether you're building a mobile app that has to load fast on a 3G connection in rural Brazil, or a dashboard that needs to stitch together data from five different services, the API architecture you choose on day one will haunt you — or reward you — for years. GraphQL and REST are the two dominant players in that space, and the choice between them is one of the most hotly debated decisions in backend engineering.

REST has been the industry standard for over two decades. It's predictable, HTTP-native, and every developer on earth has used it. But as UIs grew more complex and mobile clients became first-class citizens, REST started showing its cracks. Teams were hitting endpoints and getting mountains of data they didn't need, or worse, firing off six requests just to render a single screen. GraphQL was Facebook's answer to that pain, built internally in 2012 and open-sourced in 2015.

By the end of this article you'll be able to clearly explain the structural difference between REST and GraphQL, identify the specific scenarios where each one wins, spot the classic mistakes teams make when choosing between them, and walk into a system design interview with a confident, nuanced answer instead of a vague 'it depends.'

How REST Actually Works — and Where It Starts to Break

REST (Representational State Transfer) organizes your API around resources. A resource is a noun — a User, an Order, a Product. You expose that resource at a URL, and HTTP verbs tell the server what to do with it: GET to read, POST to create, PUT/PATCH to update, DELETE to remove. This mapping is clean, intuitive, and stateless by design.

The trouble starts when your UI needs don't map neatly onto individual resources. Say you're building a Twitter-style profile page. You need the user's name and avatar, their last three tweets, and their follower count. In REST, that's three separate endpoints: GET /users/:id, GET /users/:id/tweets, GET /users/:id/followers. Three round trips. On mobile, each round trip is expensive.

The other side of that coin is overfetching. GET /users/:id might return 40 fields — date of birth, billing address, preferences, internal flags — when your profile page only needs four of them. You're downloading data you'll throw away, every single time.

These two problems — underfetching (too few fields, too many requests) and overfetching (too many fields, wasted bandwidth) — are not bugs in REST. They're structural consequences of organizing an API around fixed resource shapes instead of around what the client actually needs.

rest_profile_page_requests.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# Scenario: Render a user profile page using a typical REST API.
# We need: user name + avatar, their last 3 tweets, follower count.
# That means 3 separate HTTP requests.

# --- Request 1: Get the user's basic info ---
# Returns ~40 fields. We only need 'name' and 'avatar_url'.
curl https://api.example.com/users/42
# Response (truncated to show the overfetching problem):
# {
#   "id": 42,
#   "name": "Maya Patel",           <-- we need this
#   "avatar_url": "https://...",    <-- we need this
#   "email": "maya@example.com",    <-- not needed on this page
#   "date_of_birth": "1990-03-15",  <-- not needed on this page
#   "billing_address": {...},        <-- definitely not needed
#   "account_flags": [...],          <-- internal — should not even be here!
#   ... 34 more fields
# }

# --- Request 2: Get the user's recent tweets ---
# A separate round trip just to get 3 tweets.
curl https://api.example.com/users/42/tweets?limit=3
# Response:
# [ { "id": 101, "body": "...", "likes": 14 }, ... ]

# --- Request 3: Get follower count ---
# Yet another round trip for a single number.
curl https://api.example.com/users/42/followers/count
# Response:
# { \"follower_count\": 8321 }\n\n# TOTAL: 3 HTTP requests, ~40 wasted fields downloaded and discarded.\n# On a slow mobile connection, these requests are sequential or require\n# client-side orchestration to run in parallel \u2014 adding code complexity.",
        "output": "3 HTTP round trips fired to render one screen.\nResponse body for /users/42 is ~2.1 KB. Actual data used: ~180 bytes.\nOverfetch ratio: ~11x more data downloaded than needed."
      }

How GraphQL Solves Overfetching — and the Trade-offs It Introduces

GraphQL flips the model on its head. Instead of the server deciding what data a response contains, the client declares exactly what it needs in a query. The server exposes a single endpoint (typically POST /graphql) and a typed schema that describes every piece of data available. The client sends a query document describing the shape it wants, and the server returns exactly that shape — nothing more, nothing less.

That same profile page that needed three REST requests? One GraphQL query handles it. The client asks for user name, avatar, their last three tweets, and follower count in a single request. The server resolves all of it and returns one response shaped exactly like the query.

But GraphQL isn't a free lunch. That flexibility comes with real costs. Caching gets harder — REST leverages HTTP caching at the URL level natively, while GraphQL's single endpoint makes URL-based caching useless by default (you need tools like Apollo Client's normalized cache or persisted queries). Query complexity is another concern: a malicious or poorly-written client can craft a deeply nested query that brings your database to its knees. You need query depth limiting and complexity analysis in production. The learning curve for schema design and resolvers is also steeper than standing up REST routes.

profile_page_query.graphqlGRAPHQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# The same profile page data — now in a single GraphQL query.
# The client is 100% in control of the shape of the response.

query GetUserProfile($userId: ID!, $tweetLimit: Int!) {\n  # Fetch the user by ID\n  user(id: $userId) {\n    # Ask for ONLY the fields this page actually renders\n    name\n    avatarUrl\n\n    # Nested query for tweets \u2014 limit is passed as a variable\n    tweets(limit: $tweetLimit) {\n      id\n      body\n      likeCount\n    }

    # A computed field on the User type — resolved server-side
    followerCount
  }
}

# Variables sent alongside the query (not hardcoded in the query string)
# {
#   "userId": "42",
#   "tweetLimit": 3
# }

# --- What the server returns ---
# {\n#   \"data\": {\n#     \"user\": {\n#       \"name\": \"Maya Patel\",\n#       \"avatarUrl\": \"https://cdn.example.com/avatars/42.jpg\",\n#       \"tweets\": [\n#         { \"id\": \"101\", \"body\": \"Just shipped a new feature!\", \"likeCount\": 14 },\n#         { \"id\": \"98\",  \"body\": \"GraphQL resolvers are wild.\",  \"likeCount\": 31 },\n#         { \"id\": \"95\",  \"body\": \"Coffee > sleep.\",              \"likeCount\": 7  }\n#       ],\n#       \"followerCount\": 8321\n#     }\n#   }\n# }\n\n# TOTAL: 1 HTTP request. Response contains EXACTLY the fields requested.\n# Response body: ~310 bytes. Zero wasted data.",
        "output": "1 HTTP request fires.\nResponse body: ~310 bytes \u2014 matches exactly what the UI needs.\nNo overfetching. No underfetching. No client-side orchestration needed."
      }

Real-World Decision Framework — When to Actually Pick Each One

The honest answer isn't 'GraphQL is better' or 'REST is simpler.' The right answer depends on the shape of your problem. Here's how senior engineers actually think about this decision.

Choose REST when your data model is simple and resource-oriented, your clients are few and controlled (e.g., internal services talking to each other), you need aggressive HTTP caching (CDN caching of GET endpoints is trivially easy with REST), or your team is small and you want zero-overhead tooling. Public APIs that third-party developers will consume are also often better served by REST — it's more universally understood and doesn't require GraphQL client libraries.

Choose GraphQL when you have multiple client types with different data needs — a mobile app, a web app, and a third-party integration all hitting the same backend. It's the perfect fit for a BFF (Backend for Frontend) layer. Also choose it when your product is iteration-heavy: adding a new field to a GraphQL schema is non-breaking by default, whereas adding a new REST endpoint requires versioning discussions. Rapid product teams love this.

The hybrid approach is increasingly common: REST for simple CRUD microservices talking to each other, GraphQL at the edge as an API gateway that stitches them together for clients. This is the architecture used by major platforms like GitHub (which offers both APIs) and Shopify.

graphql_server_with_depth_limit.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// A minimal but production-aware GraphQL server using Apollo Server.
// Demonstrates: schema definition, resolvers, and the depth-limiting
// protection you MUST add before going to production.

const { ApolloServer, gql } = require('apollo-server');
const depthLimit = require('graphql-depth-limit'); // npm install graphql-depth-limit

// --- Step 1: Define the Schema ---
// The schema is a contract between server and client.
// Every field, type, and relationship is declared here.
const typeDefs = gql`
  type Tweet {\n    id: ID!\n    body: String!\n    likeCount: Int!\n  }

  type User {
    id: ID!
    name: String!
    avatarUrl: String!
    followerCount: Int!
    # The 'limit' argument lets the client control how many tweets to fetch
    tweets(limit: Int = 10): [Tweet!]!
  }

  type Query {
    user(id: ID!): User
  }
`;

// --- Step 2: Mock data (replace with DB calls in production) ---
const USERS = {
  '42': {
    id: '42',
    name: 'Maya Patel',
    avatarUrl: 'https://cdn.example.com/avatars/42.jpg',
    followerCount: 8321,
  },
};

const TWEETS_BY_USER = {\n  '42': [\n    { id: '101', body: 'Just shipped a new feature!', likeCount: 14 },
    { id: '98',  body: 'GraphQL resolvers are wild.',  likeCount: 31 },
    { id: '95',  body: 'Coffee > sleep.',              likeCount: 7  },
    { id: '91',  body: 'Depth limits save lives.',     likeCount: 22 },
  ],
};

// --- Step 3: Resolvers ---
// Each resolver is a function that returns data for one field in the schema.
// Apollo walks the query tree and calls the right resolver for each field.
const resolvers = {\n  Query: {\n    // Called when client queries: user(id: \"42\") { ... }\n    user: (parent, args) => {\n      const foundUser = USERS[args.id];\n      if (!foundUser) throw new Error(`User ${args.id} not found`);\n      return foundUser;\n    },\n  },\n  User: {\n    // Called for each User object to resolve its 'tweets' field\n    // 'parent' is the User object returned by the Query.user resolver\n    tweets: (parent, args) => {\n      const allTweets = TWEETS_BY_USER[parent.id] || [];\n      // Respect the 'limit' argument the client passed in\n      return allTweets.slice(0, args.limit);\n    },\n  },\n};\n\n// --- Step 4: Server with depth limiting ---\n// Without this, a client could send a query like:\n// { user { followers { following { tweets { author { followers { ... } } } } } } }\n// ...and recursively query until your DB cries.\nconst server = new ApolloServer({\n  typeDefs,\n  resolvers,\n  validationRules: [\n    depthLimit(5), // Reject any query nested deeper than 5 levels\n  ],\n});\n\nserver.listen({ port: 4000 }).then(({ url }) => {\n  console.log(`GraphQL API ready at ${url}`);\n});",
        "output": "GraphQL API ready at http://localhost:4000/\n\n# Send this query via curl or Apollo Sandbox:\n# query { user(id: \"42\") { name avatarUrl followerCount tweets(limit: 3) { body likeCount } } }\n#\n# Response:\n# {\n#   \"data\": {\n#     \"user\": {\n#       \"name\": \"Maya Patel\",\n#       \"avatarUrl\": \"https://cdn.example.com/avatars/42.jpg\",\n#       \"followerCount\": 8321,\n#       \"tweets\": [\n#         { \"body\": \"Just shipped a new feature!\", \"likeCount\": 14 },\n#         { \"body\": \"GraphQL resolvers are wild.\",  \"likeCount\": 31 },\n#         { \"body\": \"Coffee > sleep.\",              \"likeCount\": 7  }\n#       ]\n#     }\n#   }\n# }"
      }

The Hybrid Architecture — GraphQL Gateway + REST Microservices

You don't have to pick one. The most mature production architectures use both: REST between internal microservices and GraphQL at the edge. Here's why that pattern wins.

Internal services talk to each other over REST (or gRPC). These calls are simple, cacheable at the service proxy level, and don't benefit from GraphQL's flexibility because each service has exactly one consumer. Adding GraphQL between internal services adds schema overhead and latency with no payoff.

At the edge, a GraphQL gateway sits between your clients and your internal services. The gateway resolves queries by making REST calls to the backend services. Clients get the benefits of GraphQL — declarative data fetching, reduced overfetching, single round trip — while your internal architecture stays simple and decoupled.

This pattern is what GitHub, Shopify, and Netflix run in production. GitHub's v4 GraphQL API is a gateway that aggregates data from over 200 internal REST services. Clients never know. They just send a query and get back exactly what they need.

graphql_gateway_resolver.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// Example: GraphQL resolver that calls a REST microservice.
// The gateway doesn't talk to databases directly — it aggregates REST APIs.

const { ApolloServer, gql } = require('apollo-server');
const axios = require('axios');

const typeDefs = gql`
  type Product {
    id: ID!
    name: String!
    price: Float!
    # This field comes from a different microservice
    stockLevel: Int
  }

  type Query {
    product(id: ID!): Product
  }
`;

const resolvers = {
  Query: {
    product: async (_, { id }) => {
      // Call the internal Product REST API
      const { data } = await axios.get(`http://product-service:3000/api/products/${id}`);
      return data;
    },
  },
  Product: {\n    stockLevel: async (parent) => {\n      // Separate call to Inventory microservice\n      const { data } = await axios.get(`http://inventory-service:3001/api/stock/${parent.id}`);
      return data.quantity;
    },
  },
};

const server = new ApolloServer({ typeDefs, resolvers });
server.listen(4000).then(() => console.log('Gateway ready'));
Output
Gateway ready at http://localhost:4000
# Client query:
# query { product(id: "42") { name price stockLevel } }
# -> internally calls product-service (REST) and inventory-service (REST)
# -> returns combined response to client
Why This Works in Production:
Each internal microservice team owns a REST API with a stable contract. The GraphQL gateway team is the only team that needs GraphQL expertise. Changes to client data requirements only affect the gateway resolvers, not the internal services. This is the separation of concerns that actually scales.
Production Insight
The hybrid pattern isolates GraphQL complexity to one team.
Internal teams can ignore GraphQL entirely.
Rule: Use an API gateway (Apollo Federation, GraphQL Mesh) to keep layers decoupled.
Key Takeaway
GraphQL at the edge, REST in the core.
This is the production architecture that scales team size.
Don’t force GraphQL on every service.

Production Considerations — Caching, Security, Monitoring

Both REST and GraphQL have production pitfalls that you won't find in tutorials. Here's what actually matters when your API is handling millions of requests.

Caching is the biggest practical difference. REST endpoints are trivially cacheable at the CDN layer because each URL is unique. CDNs can cache GET /users/42 for 300 seconds and serve the cached copy to the next thousand requests. GraphQL's single endpoint means every request is a POST with a different query body — CDN caching is useless unless you use persisted queries (where each query has a unique hash ID). If your team cares about CDN cache hit rates, REST wins hands down.

Security: REST benefits from the full HTTP security toolkit — rate limiting by URL, WAF rules per path, IP allowlisting per resource. GraphQL requires you to implement depth limiting, query cost analysis, and allowlisting of known operations. Without these, a single malicious query can DDoS your entire API.

Monitoring: REST’s HTTP status codes map cleanly to error budgets and alerting — 5xx rate goes up, pager goes off. GraphQL returns 200 even for partial failures, so your monitoring must parse response bodies for errors. Most teams miss this and only realize after an incident that their error rate looked fine while users were seeing partial data.

Neither architecture is more secure or more observable by nature — they just require different tooling. The team that plans for these differences will sleep better at night.

monitoring_middleware.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Middleware to alert on GraphQL errors that return HTTP 200.
// Without this, your monitoring won't detect partial failures.

app.use('/graphql', (req, res, next) => {
  const originalJson = res.json.bind(res);
  res.json = function (body) {
    if (body && body.errors && body.errors.length > 0) {
      // Log all errors
      console.error('[GraphQL Error]', JSON.stringify(body.errors));
      
      // Trigger alert if there are any non-client errors
      const serverErrors = body.errors.filter(e => e.extensions?.code !== 'BAD_USER_INPUT');
      if (serverErrors.length > 0) {
        // Send to PagerDuty / Opsgenie
        metricClient.increment('graphql.server_error', serverErrors.length);
      }
    }
    return originalJson(body);
  };
  next();
});
Critical: GraphQL Error Monitoring
If your monitoring only tracks HTTP 5xx status codes, you will miss GraphQL errors. Many teams have had a 'quiet' partial outage where every query returned HTTP 200 with errors in the body. Always parse the response body for the errors array and alert on it. Treat that as a non-negotiable part of your GraphQL production checklist.
Production Insight
CDN caching is REST’s superpower that GraphQL cannot match.
GraphQL requires query analysis and persisted queries to even approach it.
Rule: If your business depends on CDN cache hit rates, put REST in front.
Key Takeaway
Caching, security, and monitoring are not equal.
REST: URL-level caching, HTTP errors, simple WAF rules.
GraphQL: query analysis, persisted queries, body parsing for errors. Plan accordingly.
● Production incidentPOST-MORTEMseverity: high

The N+1 Query That Took Down a Product Page at 2 PM

Symptom
Product page response time jumped from ~200ms to over 8 seconds around 2 PM on a Tuesday. The REST API was fine. The new GraphQL gateway was the bottleneck.
Assumption
The team assumed that because each resolver was simple — one database call per field — the overall query would be fast. They didn’t account for list resolution.
Root cause
The GraphQL resolver for «reviews» on the «Product» type was calling the database once per product. A query for 50 products fired 1 query for the products and 50 individual queries for reviews. That’s 51 database round trips for a single client request.
Fix
Implemented DataLoader with batching via Dataloader for Node.js. The 50 individual review queries collapsed into 1 SQL IN (...) query. Response time dropped back to under 300ms.
Key lesson
  • Any resolver that fetches a list of related entities (reviews per product, tweets per user) is a candidate for the N+1 problem.
  • Never ship a GraphQL resolver that does a single DB call per parent item without batching.
  • Add query depth limiting (max 5 levels) and complexity analysis to every production GraphQL server.
Production debug guideDiagnose and fix GraphQL performance issues before they cause an outage.5 entries
Symptom · 01
A specific query is slow, but others are fast
Fix
Enable Apollo Studio or GraphQL tracing. Look for resolvers with high «resolveTime«. The slow resolver is your root cause.
Symptom · 02
Response time scales linearly with list size
Fix
Check for the N+1 pattern. Use Dataloader batch functions. Monitor «number of DB queries« per GraphQL request via a logging middleware.
Symptom · 03
All queries are slow after a schema change
Fix
Check if a new resolver introduced a non-optimized join or a missing index. Use «EXPLAIN ANALYZE« on the underlying SQL generated by your ORM.
Symptom · 04
Only some clients experience timeouts
Fix
Those clients are sending deeply nested or expensive queries. Enforce query depth limiting and cost analysis. Return HTTP 429 for abusive clients.
Symptom · 05
REST endpoints are fast, but GraphQL gateway is not
Fix
The gateway may be processing too much data in memory. Check if resolvers are overfetching from underlying REST services. Use «@defer« (if your client supports it) to stream large fields.
★ Quick Debug Cheat Sheet for GraphQL Production IssuesCommands and actions to diagnose the most common GraphQL performance and reliability problems in production.
High latency on a list query
Immediate action
Count DB queries for a single parent item (e.g. 1 product with reviews).
Commands
npx apollo studio agent --graph=MyGraph --variants=production trace <query_id>
tail -f /var/log/app/graphql.log | grep 'resolver' | awk '{print $NF}' | sort | uniq -c | sort -rn
Fix now
Wrap the list resolver in a Dataloader batch function. Example: loaders.reviews.load(productId).
Bad client hitting deep nested query+
Immediate action
Temporarily disable query execution from that client IP.
Commands
iptables -A INPUT -s <client_ip> -p tcp --dport 4000 -j DROP
graphql-depth-limit 5 (add to validationRules in Apollo Server)
Fix now
Enforce depth limit: depthLimit(5). Also add query cost analysis using graphql-validation-complexity.
GraphQL returning HTTP 500 for valid queries+
Immediate action
Check resolver error handling — uncaught exceptions in resolvers produce 500.
Commands
grep '"extensions".*"code".*INTERNAL_SERVER_ERROR' /var/log/app/graphql.json.log
curl -X POST -H 'Content-Type: application/json' -d '{"query": "query { __typename }"}' http://localhost:4000/graphql -v 2>&1 | tail -20
Fix now
Wrap each resolver with a try/catch and return a structured GraphQL error with appropriate extensions.code.
CDN not caching REST responses+
Immediate action
Check Cache-Control headers on REST endpoints.
Commands
curl -I https://api.example.com/v1/products/42 | grep -i cache
curl -X POST -H 'Content-Type: application/json' -d '{"query": "{ product(id: 42) { name } }"}' http://localhost:4000/graphql -v 2>&1 | grep -i cache
Fix now
REST: add Cache-Control: public, max-age=300. GraphQL: implement persisted queries and cache the persisted query responses at the CDN level.
GraphQL vs REST Comparison Table
Feature / AspectRESTGraphQL
Data fetching modelServer defines fixed response shapes per endpointClient declares exactly what fields it needs
Number of endpointsMany (one per resource/action)One (typically POST /graphql)
OverfetchingCommon — endpoint returns all fields regardlessEliminated — client requests only what it needs
Underfetching / N requestsCommon — often needs multiple round tripsSolved — nested queries fetch related data in one request
HTTP cachingNative and trivial (GET requests cache at URL level)Requires extra tooling (persisted queries, Apollo cache)
VersioningRequires versioning (/v1/, /v2/) as schema evolvesNon-breaking by default — add fields, deprecate old ones
Error handlingHTTP status codes (200, 404, 500) are the standardAlways returns HTTP 200; errors live inside the response body
Learning curveLow — every dev knows GET/POST/PUT/DELETEMedium — schema design, resolvers, and DataLoader take time
Tooling ecosystemMature — Swagger/OpenAPI, Postman, curlRich — Apollo, GraphiQL, code generation, introspection
Best forSimple CRUD, public APIs, microservice-to-microserviceComplex clients, mobile apps, rapid iteration, BFF pattern
Real-world adoptersTwitter v1, Stripe, most microservicesGitHub v4, Shopify Storefront, Facebook, Airbnb

Key takeaways

1
REST's core weakness isn't the protocol
it's that fixed resource shapes create a mismatch between what the server returns and what different clients actually need. GraphQL solves this by inverting control to the client.
2
GraphQL's single endpoint breaks HTTP-layer caching. In REST, a CDN can cache GET /products/42 trivially. With GraphQL you need query-level caching strategies like Apollo's persisted queries or a normalized client-side cache.
3
The N+1 problem is GraphQL's most dangerous production pitfall. Every GraphQL server resolving lists of related data must implement DataLoader batching, or a query for 100 users will silently fire 101 database queries.
4
The smartest production architecture often uses both
REST for internal microservice communication (simple, fast, easy to cache) and a GraphQL gateway at the edge for client-facing APIs where different clients need different data shapes.
5
GraphQL error handling requires body parsing for monitoring. REST errors map to HTTP status codes; GraphQL errors hide inside HTTP 200 responses. Your monitoring setup must account for this or you will miss partial failures.

Common mistakes to avoid

5 patterns
×

Using GraphQL for microservice-to-microservice communication

Symptom
Teams add a full GraphQL layer between internal services, adding schema overhead and resolver complexity where a simple REST or gRPC call would be 10x faster and easier.
Fix
GraphQL shines at the client-facing edge (your API gateway or BFF). Between internal services that you control, REST or gRPC is almost always the right call. GraphQL's flexibility is only valuable when the consumer's needs are unpredictable — internal services usually have exactly one consumer with known needs.
×

Ignoring the N+1 query problem in GraphQL resolvers

Symptom
A query for 50 users and their tweets fires 51 database queries, and your API that seemed fast in development becomes dangerously slow under real traffic. You might not even notice until you load test it.
Fix
Install and configure DataLoader from day one. Wrap every resolver that fetches a related entity in a DataLoader batch function. It batches all the individual ID lookups from a single request into one SQL IN (...) query. This is non-negotiable in production GraphQL.
×

Assuming REST endpoints always need versioning

Symptom
Teams pre-emptively build /v1/ into every REST API and then maintain parallel endpoint versions forever, even when changes are purely additive.
Fix
Additive changes (new fields in a response, new optional query params) don't require versioning in REST either. Version only when you need to make a breaking change — removing a field, changing a field's type, or restructuring the response shape. GraphQL makes this easier with the @deprecated directive, but disciplined REST teams can avoid version sprawl too.
×

Not implementing query depth limits or cost analysis on GraphQL

Symptom
A client sends a deeply nested query like { user { friends { posts { comments { author { friends {...} } } } } } } and your database crashes because the query generates exponential resolution.
Fix
Add graphql-depth-limit middleware (set max depth to 5 or 7) and use graphql-validation-complexity or Apollo’s built-in cost analysis. Without these, your GraphQL server is one accidental query away from an outage.
×

Treating GraphQL error handling like REST error handling

Symptom
Monitoring shows 0% error rate because all responses return HTTP 200. Users are seeing partial data with errors in the response body, but no one alerts for it.
Fix
Parse the response body's errors array in your monitoring middleware. Alert on any non-client errors (extensions.code !== 'BAD_USER_INPUT'). Train your on-call team to check the errors array first, not the HTTP status code.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
You're designing the API layer for an e-commerce platform with an iOS ap...
Q02SENIOR
GraphQL always returns HTTP 200 even when an error occurs. How does erro...
Q03SENIOR
A colleague says 'GraphQL is always better than REST because it eliminat...
Q04SENIOR
Describe a production incident where GraphQL's N+1 problem caused a perf...
Q01 of 04SENIOR

You're designing the API layer for an e-commerce platform with an iOS app, an Android app, a web storefront, and a third-party affiliate integration. Each client needs different subsets of product data. Walk me through how you'd decide between REST and GraphQL, and what architecture you'd land on.

ANSWER
I'd start by listing the client types and their data needs. Four clients with different subsets of product data is a strong signal for GraphQL at the edge. I'd put a GraphQL gateway (Apollo Federation) in front, which aggregates data from internal REST microservices (product service, inventory service, pricing service). The internal services stay REST — they're simple, cacheable, and each has one consumer (the gateway). The clients benefit from declarative queries, no overfetching, and reduced round trips. I'd also use persisted queries for the mobile apps to improve caching and security. Finally, I'd enforce query depth limiting (max 5) and cost analysis to protect the backend.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Is GraphQL faster than REST?
02
Can I use GraphQL and REST together in the same application?
03
Why does GraphQL always return HTTP 200 even for errors?
04
Does GraphQL work well with relational databases?
05
Which one should I learn first for my career?
🔥

That's Architecture. Mark it forged?

6 min read · try the examples if you haven't

Previous
REST API Design
3 / 13 · Architecture
Next
Event-Driven Architecture