Why GraphQL is Overrated: The Case for REST in 2025
I'm going to say something that might ruffle some feathers: GraphQL is overrated, and in 2025, REST APIs are still the better choice for most projects.
After spending the last few years architecting systems that handle millions of users and building platforms that process tens of millions in revenue, I've seen the GraphQL hype cycle play out in real production environments. The reality? It's messier than the conference talks suggest.
Don't get me wrong – GraphQL isn't bad technology. But it's become the "blockchain of APIs" – everyone thinks they need it, few understand the tradeoffs, and most would be better off with something simpler.
The GraphQL Hype Train: How We Got Here
Remember 2018? Facebook open-sourced GraphQL, and suddenly every tech blog was declaring REST dead. The promise was seductive:
- Single endpoint for all data needs
- Client-driven queries (no more over/under-fetching!)
- Strong typing and introspection
- Real-time subscriptions built-in
The developer community ate it up. GitHub migrated their API. Shopify rebuilt theirs. Netflix started experimenting. If it was good enough for these giants, surely it was the future, right?
But here's what those early adopters had that your startup doesn't: massive engineering teams, dedicated API infrastructure engineers, and the resources to solve complex performance problems.
Performance Reality Check: N+1 Queries and Caching Nightmares
Let's talk about the elephant in the room: the N+1 query problem. It's GraphQL's dirty little secret that nobody mentions in the tutorials.
Here's a seemingly innocent GraphQL query:
query {
posts {
title
author {
name
email
}
}
}
Looks clean, right? But under the hood, this innocent query can trigger a database nightmare:
-- First query to get posts
SELECT id, title, author_id FROM posts;
-- Then N queries for each author (if you have 100 posts, that's 100 more queries)
SELECT name, email FROM users WHERE id = 1;
SELECT name, email FROM users WHERE id = 2;
SELECT name, email FROM users WHERE id = 3;
-- ... 97 more queries
I've seen this pattern bring down production databases. One poorly constructed GraphQL query from a mobile client caused 30-second response times and database CPU spikes that took down an entire e-commerce platform.
The "solution"? DataLoader. But now you're managing batching logic, cache invalidation, and complex loading patterns. Your simple API just became a distributed systems problem.
Compare this to a well-designed REST endpoint:
// GET /api/posts?include=author
app.get('/api/posts', async (req, res) => {
const posts = await db.query(`
SELECT p.id, p.title, u.name as author_name, u.email as author_email
FROM posts p
JOIN users u ON p.author_id = u.id
`);
res.json(posts);
});
One query. Predictable performance. Easy to cache. Easy to debug.
Complexity Tax: When Simple REST Beats Sophisticated GraphQL
GraphQL introduces what I call the "complexity tax" – the overhead cost of sophisticated tooling that often outweighs the benefits.
Schema Management Hell
Your GraphQL schema becomes a contract that's harder to evolve than REST endpoints. Want to deprecate a field? You need to:
- Mark it as deprecated in the schema
- Monitor usage across all clients
- Coordinate removal with frontend teams
- Handle versioning without breaking existing queries
With REST, you version your endpoints and move on:
// Old version still works
app.get('/api/v1/users', handleV1Users);
// New version with changes
app.get('/api/v2/users', handleV2Users);
Resolver Complexity
GraphQL resolvers seem simple until you need to handle real-world scenarios:
const resolvers = {
Query: {
user: async (parent, { id }, context) => {
// Permission checking
if (!context.user.canViewUser(id)) {
throw new ForbiddenError('Access denied');
}
// Rate limiting per resolver
await rateLimiter.check(context.user.id, 'user_query');
// Caching strategy per field
const cached = await redis.get(`user:${id}`);
if (cached) return JSON.parse(cached);
// Actual data fetching
const user = await User.findById(id);
// Cache with TTL
await redis.setex(`user:${id}`, 300, JSON.stringify(user));
return user;
}
},
User: {
posts: async (user, args, context) => {
// Oh no, another resolver with its own complexity...
}
}
};
That's a lot of boilerplate for what should be simple data fetching.
Security Headaches: Why GraphQL Opens More Attack Vectors
GraphQL's flexibility is also its security weakness. The same features that make it powerful make it dangerous.
Query Depth Attacks
A malicious client can craft deeply nested queries that consume massive server resources:
query MaliciousQuery {
user(id: "1") {
posts {
comments {
author {
posts {
comments {
author {
posts {
# This can go 20+ levels deep
}
}
}
}
}
}
}
}
}
You need query depth limiting, complexity analysis, and timeout mechanisms. More complexity tax.
Introspection Vulnerabilities
GraphQL's introspection feature is great for development but dangerous in production. It exposes your entire API structure to potential attackers:
query IntrospectionQuery {
__schema {
types {
name
fields {
name
type {
name
}
}
}
}
}
Sure, you can disable introspection in production, but now your GraphQL playground doesn't work, making debugging harder.
Rate Limiting Nightmares
Rate limiting REST endpoints is straightforward – you limit requests per endpoint per user. With GraphQL, one endpoint can handle queries of vastly different complexity. You need query cost analysis, which adds even more overhead.
Developer Experience: The Hidden Costs of GraphQL Adoption
The GraphQL ecosystem loves to talk about developer experience, but let's examine the hidden costs:
Tooling Overhead
A typical GraphQL setup requires:
- Schema definition files
- Code generation tools (graphql-codegen)
- Type checking integration
- Custom scalar definitions
- Resolver mapping
- DataLoader setup
- Query complexity analysis
- Custom directives
- Schema stitching (for microservices)
Compare this to a REST API with OpenAPI:
# openapi.yaml
paths:
/users/{id}:
get:
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: User details
content:
application/json:
schema:
$ref: '#/components/schemas/User'
Generate your client, and you're done.
Debugging Complexity
When a REST endpoint fails, you know exactly which endpoint and what went wrong. With GraphQL, you get partial results, and debugging requires understanding the entire resolver chain:
{
"data": {
"user": {
"name": "John Doe",
"posts": null
}
},
"errors": [
{
"message": "Cannot read property 'author_id' of undefined",
"locations": [{"line": 4, "column": 5}],
"path": ["user", "posts"]
}
]
}
Which resolver failed? Was it a database issue? Permission problem? Good luck figuring that out from your logs.
When GraphQL Actually Makes Sense (Spoiler: It's Rare)
I'm not completely anti-GraphQL. There are legitimate use cases, but they're rarer than the hype suggests:
1. Multiple Frontend Teams with Diverse Needs
If you have 5+ frontend teams building different applications with vastly different data requirements, GraphQL's flexibility might justify the complexity.
2. Mature Organizations with Dedicated API Teams
Companies like GitHub and Shopify succeeded with GraphQL because they have entire teams dedicated to API infrastructure. If you have the resources to build sophisticated caching, monitoring, and optimization tooling, GraphQL can work.
3. Rapid Prototyping with Hasura/AWS AppSync
Managed GraphQL services can make sense for rapid prototyping, but even then, you'll likely need to migrate to something more maintainable for production.
The REST Renaissance: Modern Patterns That Work Better
While everyone was chasing GraphQL, REST evolved. Modern REST APIs in 2025 solve most of GraphQL's original problems without the complexity overhead.
JSON:API for Consistent Structure
The JSON:API specification provides consistent response formats and relationship handling:
{
"data": {
"type": "posts",
"id": "1",
"attributes": {
"title": "Hello World"
},
"relationships": {
"author": {
"data": {"type": "users", "id": "1"}
}
}
},
"included": [
{
"type": "users",
"id": "1",
"attributes": {
"name": "John Doe"
}
}
]
}
Smart Field Selection with REST
You can add field selection to REST endpoints:
// GET /api/posts?fields=title,author.name
app.get('/api/posts', (req, res) => {
const fields = req.query.fields?.split(',') || [];
const posts = await Post.findAll({
attributes: fields.includes('title') ? ['title'] : undefined,
include: fields.some(f => f.startsWith('author'))
? [{ model: User, attributes: ['name'] }]
: []
});
res.json(posts);
});
OpenAPI 3.1 with Rich Documentation
Modern OpenAPI specs provide excellent documentation and type safety:
components:
schemas:
Post:
type: object
required: [id, title]
properties:
id:
type: string
format: uuid
title:
type: string
minLength: 1
maxLength: 200
author:
$ref: '#/components/schemas/User'
HTTP/2 and Multiplexing
HTTP/2 eliminates the "multiple requests" problem that GraphQL was supposed to solve. You can make multiple REST calls in parallel without the connection overhead.
Making the Right Choice for Your Team in 2025
Here's my decision framework for choosing between GraphQL and REST in 2025:
Choose REST if:
- Your team has less than 10 backend engineers
- You're building an MVP or early-stage product
- Performance and simplicity matter more than flexibility
- You need predictable caching and CDN behavior
- Your frontend needs are relatively straightforward
- You want to ship fast and iterate quickly
Consider GraphQL only if:
- You have dedicated API infrastructure engineers
- Multiple diverse clients need different data shapes
- You're willing to invest heavily in tooling and monitoring
- Your organization can handle the operational complexity
- You have specific requirements that REST can't meet
The Hybrid Approach
Many successful companies use both. REST for core CRUD operations and public APIs, GraphQL for specific internal use cases that benefit from flexibility.
// REST for public API
app.get('/api/v1/products/:id', handleProductDetails);
// GraphQL for internal dashboard that needs flexible queries
app.use('/internal/graphql', graphqlHTTP({
schema: internalSchema,
graphiql: true
}));
The Bottom Line
GraphQL isn't inherently bad, but it's been oversold. The problems it solves are real, but the solutions often create more problems than they solve.
In 2025, REST APIs with modern patterns – smart caching, field selection, JSON:API structure, and OpenAPI documentation – provide a better balance of simplicity, performance, and maintainability for most projects.
Your time is better spent building features your users care about rather than debugging N+1 queries and managing resolver complexity.
Before you reach for GraphQL, ask yourself: "Do I really need this complexity, or am I just following the hype?"
Most of the time, the honest answer is the latter. And that's okay – sometimes the boring choice is the right choice.
Ready to build APIs that actually work for your business needs? At BeddaTech, we help teams make the right architectural decisions without getting caught up in the hype. Let's talk about your API strategy.