bedda.tech logobedda.tech
← Back to blog

Why GraphQL is Overrated: The Case for REST in 2025

Matthew J. Whitney
9 min read
software architecturebackendbest practicesperformance optimization

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:

  1. Mark it as deprecated in the schema
  2. Monitor usage across all clients
  3. Coordinate removal with frontend teams
  4. 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.

Have Questions or Need Help?

Our team is ready to assist you with your project needs.

Contact Us