Home / Articles

REST vs GraphQL: The Real Trade-Offs Behind Each Architecture

Explains the specific problems REST and GraphQL each solve, their internal mechanics, and the hidden trade-offs to weigh before choosing one for your API.

Publisher: REACTAPP.TOP10 min read2224 words

This article is published in English.

Sources: REST vs GraphQL: Which Problem Does Each One Actually Solve?

When you sit down to architect how a client and server should communicate, one question comes up almost every time: REST or GraphQL?

Debates around this topic tend to frame it as a competition, as if one side must win. Some developers argue that REST has run its course and should be retired. Others insist GraphQL introduces layers of complexity that most projects don't need. Neither framing is particularly useful.

Technologies are not designed to score points in an argument. They exist to address concrete problems developers run into under specific circumstances.

Adopting GraphQL simply because it feels current can leave you untangling caching headaches and expensive server-side queries you never planned for. Conversely, forcing a rigid REST structure onto a data-heavy mobile app can bog down your screens with a flurry of sequential network requests.

To make a sound decision, it helps to set aside the hype and look at the underlying motivations: what each approach was designed to fix, how it operates internally, and what trade-offs come with it.

What Problem Did REST Solve?

Before REST became the norm, web services relied heavily on protocols such as SOAP and XML-RPC. These demanded rigid XML schemas, verbose message envelopes, and elaborate setup. Clients needed to know precise remote procedure names just to retrieve data.

Roy Fielding introduced REST (Representational State Transfer) in 2000.

At its core, REST offered a straightforward but effective concept: lean on the web's own architecture.

Rather than treating a server as an opaque function-execution engine, REST models a system as a collection of resources, each addressable through a clean URL. Actions on these resources are expressed through standard HTTP verbs (GET, POST, PUT, DELETE), while the outcome of each request is reported through familiar HTTP status codes such as 200 OK for success, 404 Not Found when a resource is missing, or 500 Server Error when something breaks on the backend.

Resource-Oriented Architecture (REST)
Client ─── GET /api/users/12 ──────────> Server (Find user #12)
Client <── 200 OK [ { user data } ] ──── Server
Client ─── DELETE /api/posts/99 ───────> Server (Delete post #99)
Client <── 204 No Content ───────────── Server

Why REST Matters

  • Simplicity and Predictability: Once you grasp that /api/products refers to a products collection and /api/products/42 points to one specific item, the interface becomes self-explanatory.
  • Built-in HTTP Caching: Since GET requests are idempotent and mapped to unique URLs, infrastructure like browsers, CDNs (Cloudflare, for instance), and reverse proxies (such as Nginx) can cache responses out of the box, with no extra application logic required.
  • Decoupled Life Cycles: Backend teams can restructure internal database schemas freely, as long as the public HTTP contract stays consistent.

This model served developers well for a long time. But as client applications grew more sophisticated, certain friction points started to surface.

The New Pain Points: Over-Fetching and Under-Fetching

Picture a mobile app rendering a user profile screen. That screen needs exactly three things:

  • The user's name
  • A URL pointing to the user's avatar
  • The titles of the user's three most recent blog posts

With a conventional REST API, satisfying this modest requirement often triggers two distinct issues.

1. Over-Fetching

The client requests GET /api/users/12.

The server replies with the full user record:

{
  "id": 12,
  "name": "Sarah Connor",
  "avatarUrl": "https://cdn.example.com/sarah.jpg",
  "email": "sarah@example.com",
  "address": { "street": "...", "zip": "...", "country": "..." },
  "billingHistory": [ ... ],
  "preferences": { ... },
  "createdAt": "2021-04-12T08:14:00Z"
}

The screen only required name and avatarUrl, yet it ended up pulling in unrelated address details, billing information, and preference settings. Over a slow cellular connection, shipping unnecessary bytes wastes battery and slows down rendering. This scenario is known as over-fetching.

2. Under-Fetching (The N+1 Network Problem)

The client still needs Sarah's posts, but the /api/users/12 response didn't include them.

So it issues a second call: GET /api/users/12/posts.

Suppose that endpoint hands back only a bare list of post identifiers. In that case, the client may then have to make three separate follow-up calls, one fetching /api/posts/101, a second pulling /api/posts/102, and a third retrieving /api/posts/103.

At this point the device has made several sequential network round trips just to display one screen. This pattern is called under-fetching.

What Problem Did GraphQL Actually Solve?

Facebook ran into precisely this bottleneck around 2012, while shifting its flagship app away from mobile web wrappers toward native applications. Mobile connections were unreliable and slow, and Facebook's interface was built from tightly interconnected data — users, friends, comments, likes, reactions.

Designing a dedicated REST endpoint for every screen, something like /api/mobile-profile-screen, quickly turned into an unmanageable pattern.

To solve this, Facebook built GraphQL.

GraphQL isn't a database technology. It's a query language for APIs, paired with a server-side runtime that executes queries against a type system that you define.

Rather than making multiple round trips to different endpoints, the client sends one POST request carrying a structured query that spells out exactly what shape of data it needs. The server then returns a JSON payload matching that shape precisely.

The GraphQL Approach

Here's how that same profile screen would request its data using GraphQL:

GraphQL

query GetUserProfile {
  user(id: "12") {
    name
    avatarUrl
    posts(limit: 3) {
      title
    }
  }
}

And the server hands back exactly what was asked for, nothing more:

JSON

{
  "data": {
    "user": {
      "name": "Sarah Connor",
      "avatarUrl": "https://cdn.example.com/sarah.jpg",
      "posts": [
        { "title": "Building Reliable Systems" },
        { "title": "Understanding Memory Leaks" },
        { "title": "A Guide to Clean Architecture" }
      ]
    }
  }
}

There's no wasted bandwidth from unused fields, and no need for a second or third trip to fetch related data. A single network call delivers precisely the data the view needs to render.

Looking Under the Hood

To really grasp the trade-offs involved, it helps to look at how each approach handles execution on the server side.

REST's Endpoint-Based Model

In a framework such as Express.js, routes map incoming URLs directly to handler functions:

JavaScript

// Express.js REST route
app.get('/api/users/:id', async (req, res) => {
  const user = await db.users.findById(req.params.id);
  if (!user) return res.status(404).json({ error: 'User not found' });
  res.json(user);
});

The backend developer has full control over the query logic, the database calls, and the shape of the response. Performance tuning is simple in this world: you go straight to the relevant endpoint handler and optimize the SQL statement or add caching there.

GraphQL's Schema-and-Resolver Model

GraphQL is built around a Schema alongside individual functions known as Resolvers:

JavaScript

// Type definitions
const typeDefs = `
  type Post {
    id: ID!
    title: String!
  }
  type User {
    id: ID!
    name: String!
    avatarUrl: String!
    posts: [Post]
  }
  type Query {
    user(id: ID!): User
  }
`;
// Resolvers map fields to actual data sources
const resolvers = {
  Query: {
    user: async (_, { id }) => {
      return await db.users.findById(id);
    },
  },
  User: {
    posts: async (parentUser) => {
      // Runs when the client query includes the 'posts' field
      return await db.posts.findByAuthorId(parentUser.id);
    },
  },
};

When a query comes in, the GraphQL engine checks it against the schema and then walks through the resolvers, executing them in a tree-like pattern.

This gives frontend teams a lot of freedom in shaping their requests. But there's a catch worth noticing: the client determines what shape the query takes, yet it's the server that has to absorb the cost of fulfilling it.

What GraphQL Costs You Behind the Scenes

That flexibility doesn't come for free. Switching from REST to GraphQL effectively shifts complexity away from the client and onto the server.

The N+1 Query Problem at the Database Layer

Consider the resolver setup above again. What happens if a client asks for a list of 20 users along with each user's posts?

GraphQL

query {
  users {
    name
    posts {
      title
    }
  }
}
  • The users resolver executes once, pulling all 20 rows in one go (SELECT * FROM users).
  • The posts resolver then fires separately for each individual user returned.
  • The end result: your database ends up handling 1 query for the user list plus 20 additional queries for posts — 21 queries in total.

With REST, you'd typically collapse this into one SQL join inside your endpoint code (SELECT * FROM users JOIN posts...). With GraphQL, you need batching tools like DataLoader to catch these scattered queries and merge them before they hit the database.

The Trouble with HTTP Caching

Consider a REST call like:

GET /api/articles/42

A CDN can happily cache that response for, say, 10 minutes. If 5,000 users hit /api/articles/42 during that window, your origin server only has to process the very first one — the CDN absorbs the rest.

GraphQL breaks this pattern because nearly every operation is sent as a POST to one single endpoint, /graphql.

Since the actual query lives inside the request body rather than the URL, ordinary HTTP caches, proxies, and CDNs have no way of recognizing that two requests are fetching identical data. To work around this, you either need a sophisticated normalized cache on the client (Apollo Client or Relay, for example) or a purpose-built caching gateway.

Query Depth as an Attack Vector

With REST, there's no simple way for an attacker to push a single endpoint into traversing arbitrarily deep relational data.

With GraphQL, though, a malicious actor could submit something like this:

GraphQL

query EvilQuery {
  user {
    posts {
      author {
        posts {
          author {
            posts {
              title
            }
          }
        }
      }
    }
  }
}

Without protections such as query complexity scoring or depth limiting in place, a query like this can bring your database to a crawl or exhaust the server's available memory.

REST and GraphQL Side by Side

FeatureRESTGraphQLData fetchingReturns a fixed shape of data for each URLReturns exactly the fields the client asks forNetwork callsOften needs several round tripsTypically resolved in one requestHTTP cachingBuilt in, straightforward, and efficientHard to achieve; needs extra tooling on the client or gatewayLearning curveShallow, relies on familiar HTTP conventionsSteeper, requires understanding schemas and resolversError reportingConventional HTTP status codes (404, 500, 401)Almost always 200 OK, with problems listed in an errors fieldFile uploadsStraightforward via multipart form dataNeeds custom mutation logic or pre-signed upload URLsIdeal useCRUD-style services, microservices, open web APIsRich UIs and apps spanning several platforms

Making the Call: How to Decide

When REST Is the Better Fit

  • Your resource model is straightforward. For typical CRUD-driven products like admin panels, dashboards, or basic storefronts, REST lets you ship faster.
  • Caching matters a lot to you. For public, read-heavy content such as blogs, news pages, or catalogs, native HTTP caching through CDNs is hard to beat on speed and cost.
  • You're publishing an API for outside consumers. External developers can already hit REST endpoints with curl or fetch without learning a dedicated client library.

When GraphQL Makes More Sense

  • You support many different client types. Think of a product with iOS, Android, smartwatch, and web clients, each needing a different subset of the same underlying data.
  • Your domain is a tightly interlinked graph. This applies when entities like users, groups, comments, permissions, and media constantly reference each other.
  • Frontend and backend work on separate timelines. In larger teams, GraphQL functions as a buffer layer: once resolvers and types are defined, frontend engineers can ship new views without waiting on backend teams to add new endpoints every cycle.

Wrapping Up

Neither of these technologies has been rendered obsolete by the other.

  • REST addressed the need for a consistent, scalable, and cache-friendly way of exposing resources over the web.
  • GraphQL addressed the need for clients to shape their own data requests, cutting down on wasted round trips over slow or unreliable mobile connections.

Choosing between them isn't about following trends, it's about understanding the actual bottlenecks your system faces. If your biggest challenges are mobile performance and an ever-growing list of frontend-specific endpoints, GraphQL can be a strong solution. If what you value most is simplicity, dependable caching, and an easy-to-maintain backend, REST is still a solid, well-proven option.