In this guide
If you have built any web app, you have built a REST API — probably without thinking twice. Endpoints like GET /api/users/42 and POST /api/products are the default shape of the modern web. Then someone mentions GraphQL and suddenly you wonder if your perfectly good REST API is obsolete.
It is not. But GraphQL solves a real pain that REST has, and knowing which one to reach for is a decision every backend developer makes regularly. This guide explains both honestly, with the trade-offs that actually matter for student projects and interviews.
What REST really is
REST (Representational State Transfer) is an architectural style, not a protocol. In practice, REST APIs share these traits:
- Resource-based URLs — things, not actions:
/users/42,/orders/17/items - HTTP methods carry meaning — GET reads, POST creates, PUT/PATCH updates, DELETE removes
- Stateless — each request contains everything the server needs
- Standard HTTP caching — GET responses can be cached by browsers, CDNs, and proxies
A typical REST interaction for a social app profile page looks like this:
GET /api/users/42
GET /api/users/42/posts
GET /api/users/42/friends
Three round trips to assemble one screen. That is REST's famous weakness — and the reason GraphQL exists.
What GraphQL really is
GraphQL is a query language for APIs, developed at Facebook and released in 2015. Instead of many endpoints, you get one endpoint (usually /graphql) and the client asks for exactly the data it wants:
query {
user(id: "42") {
name
avatar
posts(limit: 5) {
title
likes
}
friends {
name
}
}
}
One request, exactly the fields you need — no more, no less. The server has a schema that describes every type and field available, and tools like GraphiQL give you autocomplete and documentation for free from that schema.
Note: GraphQL is not a database technology and not a replacement for your database. It is a layer between your client and your data sources — your resolvers still talk to Postgres, MongoDB, or other APIs underneath.
The real differences, side by side
| REST | GraphQL | |
|---|---|---|
| Endpoints | Many, one per resource | One (/graphql) |
| Data fetching | Fixed shape per endpoint | Client chooses fields |
| Over-fetching | Common (you get fields you ignore) | Rare (you ask for exactly what you need) |
| Under-fetching | Common (N requests for N resources) | Rare (nested data in one query) |
| Caching | Easy — standard HTTP caching, CDNs | Harder — all requests are POST to one URL |
| Tooling/docs | Swagger/OpenAPI (hand-maintained) | Schema is self-documenting, introspection built in |
| Learning curve | Gentle | Steeper (schema, resolvers, query language) |
| File uploads | Trivial (multipart forms) | Awkward (needs extensions or separate REST endpoint) |
Over-fetching and under-fetching, concretely
These are the two words interviewers expect you to know:
- Over-fetching:
GET /api/users/42returns 40 fields when your mobile screen shows 4. Wasted bandwidth — painful on slow networks. - Under-fetching: you need the user, their posts, and their friends, so you make 3+ requests. More latency, more code.
GraphQL eliminates both by letting the client specify the exact shape. REST handles both with careful endpoint design (query params like ?fields=name,avatar or embedding related data).
Caching: REST's quiet superpower
This is the trade-off people underestimate. Because REST uses GET requests with unique URLs, the entire HTTP caching infrastructure works out of the box: browser caches, reverse proxies, CDNs. A GET /api/products response can be cached at the edge and served to thousands of users without touching your server.
GraphQL typically sends queries as POST bodies to a single URL, so none of that applies automatically. You can cache GraphQL (persisted queries, application-level caching, DataLoader for batching), but you build it yourself. For read-heavy public content — blogs, catalogues, documentation — REST's free caching is a genuine advantage.
When to choose GraphQL
- Your clients have different data needs — a mobile app wants 5 fields, a dashboard wants 50. GraphQL serves both from one API.
- Nested, relational data — social graphs, project management tools, anything where one screen aggregates many related entities.
- Rapid frontend iteration — frontend teams can add fields to queries without waiting for backend endpoint changes (as long as the schema supports them).
- You want self-documenting APIs — the schema plus GraphiQL is genuinely better documentation than most hand-written Swagger.
When to choose REST
- Simple CRUD apps — most student projects, admin panels, and straightforward apps. REST is simpler to build, test, and debug.
- Heavy caching needs — public content, high read traffic. HTTP caching is battle-tested and free.
- File uploads/downloads — multipart forms in REST are simple; GraphQL file upload is a known pain point.
- Your team is small or learning — REST has a gentler learning curve and every HTTP tool (curl, Postman, browser devtools) understands it natively.
- Microservices with simple contracts — service-to-service calls are often simpler as REST.
Note: "Which is faster?" is the wrong question. Performance depends on your resolvers, database queries, and caching — not the API style. A badly written GraphQL resolver that fires 100 database queries (the N+1 problem) is slower than any REST endpoint.
The N+1 problem (and DataLoader)
The classic GraphQL pitfall: your query asks for 10 users and each user's posts. A naive resolver runs 1 query for users + 10 queries for posts = 11 database queries. Ask for 100 users and it is 101 queries. This is the N+1 problem.
The standard fix is DataLoader (or your framework's equivalent): it batches those 10 separate "posts for user X" calls into a single "posts for users [1..10]" query. If you use GraphQL, learn DataLoader early — it is the difference between a demo and a working API.
A practical decision checklist
Ask these in order:
- Is my data simple CRUD with predictable shapes? → REST. Done.
- Do different clients need very different slices of the same data? → GraphQL.
- Is aggressive HTTP caching central to my design? → REST.
- Am I aggregating many related entities per screen? → GraphQL.
- Is this a learning project with a deadline? → REST. You will ship faster.
- Am I preparing for interviews at product companies? → Learn GraphQL basics anyway; "explain the trade-offs" is a common interview question and this guide is your answer.
You can also mix: many production systems expose GraphQL to clients while services talk REST internally. The styles are not enemies.
Designing a GraphQL schema
If you choose GraphQL, the schema is your API contract — design it deliberately, not as a mirror of your database tables.
type User {
id: ID!
name: String!
email: String
posts: [Post!]!
}
type Query {
user(id: ID!): User
users(limit: Int = 20): [User!]!
}
type Mutation {
createPost(title: String!, body: String!): Post
}
The conventions that matter:
- Query for reads, Mutation for writes. This separation is a GraphQL rule, not a suggestion — clients and tools rely on it.
!means non-null. Fields are nullable by default. Mark a field non-null only when it genuinely cannot be absent; overusing!turns partial failures into whole-query failures.- Design for client needs, not table shapes. If the frontend always shows a user with their recent posts, expose that nesting — do not force the client to assemble it from normalized tables.
- Deprecate, do not break. Mark old fields
@deprecated(reason: "use fullName")and keep them working. GraphQL's no-versioning promise only holds if you honor it. - Paginate lists from day one.
users(limit: 20)with cursor pagination beats returning unbounded arrays that someone eventually queries at 100,000 rows.
Common mistakes
- Choosing GraphQL because it sounds modern. If your app is CRUD with one web client, REST ships faster and debugs easier. Hype is not a requirement.
- Exposing your entire database through GraphQL. Just because the schema can expose everything does not mean it should. Design the schema for client needs, apply the same authorization thinking you would for REST endpoints.
- Ignoring the N+1 problem. Test with realistic data volumes, not 3 rows.
- No rate limiting or query cost analysis. A deeply nested GraphQL query can be expensive; limit query depth and complexity, especially on public APIs. See the rate limiting guide for the general technique.
- Versioning confusion. REST versions with URLs (
/v1/,/v2/); GraphQL evolves by adding fields and deprecating old ones — never breaking existing queries. Both need a deliberate strategy.
Where to go from here
- Compare full-stack framework choices in the React vs Django vs Laravel comparison — your API style decision interacts with your stack decision.
- Choosing between SQL and NoSQL for the data underneath? See SQL vs NoSQL: which database for your project.
- When your API is ready, the MERN deployment guide walks through putting a Node API on a VPS.
- Protect any public API with the strategies in Rate Limiting APIs: Strategies Explained.
- More backend topics in the Web Development branch hub.