A network of interconnected nodes with glowing data paths, representing the flexibility of GraphQL queries
Back to Blog
API Design
2026-02-13
5 min read

GraphQL vs REST: Designing Flexible Node.js APIs

A

Abhay Vachhani

Developer

For decades, REST has been the standard for API design. But as frontend requirements become more complex, REST's rigid endpoints often lead to **over-fetching** (downloading data you don't need) or **under-fetching** (making multiple requests to get related data). Enter GraphQL: a query language for your API that puts the client in control. But beyond the basics, how do you scale it?

1. The Core Difference: Endpoints vs Types

In REST, you design endpoints for resources (e.g., GET /users/1, GET /users/1/posts). In GraphQL, you define a **Schema** of types and relationships. The client sends a single POST request describing exactly the data structure it wants, and the server responds with JSON matching that shape.

2. Scaling with Apollo Federation

Monolithic GraphQL servers eventually become bottlenecks. **Apollo Federation** allows you to split your graph across multiple microservices (subgraphs) while exposing a single, unified gateway to the client.

For example, a **User Subgraph** defines the User type, while a **Reviews Subgraph** can extend that type to add a reviews field. The Gateway merges them automatically at runtime.

# User Microservice
type User @key(fields: "id") {
  id: ID!
  username: String!
}

# Reviews Microservice
extend type User @key(fields: "id") {
  id: ID! @external
  reviews: [Review]
}

3. The Caching Conundrum

REST uses HTTP caching (ETag, Cache-Control) effectively because each resource has a unique URL. GraphQL uses a single URL (POST /graphql), rendering standard HTTP caching useless.

The Solution: Use **Persisted Queries**. The client hashes the query string (e.g., sha256("query { user { name } }") -> "abc123...") and sends the hash via GET /graphql?id=abc123.... This allows CDNs (Cloudflare, Akamai) to cache the JSON response based on the hash, bringing GraphQL performance on par with REST.

4. Security: Limiting Complexity

A malicious user can easily crash your server with a deeply nested query: query { user { friends { friends { friends ... } } } }. This is a DoS attack waiting to happen.

Defense: Implement **Query Complexity Analysis**. Assign "points" to each field (e.g., scalar = 1, object = 5, list = 10). Reject any query that exceeds a total cost threshold (e.g., 1000 points) before execution begins.

5. The N+1 Problem and DataLoaders

The flexibility of GraphQL comes with a cost: performance pitfalls. The classic issue is the **N+1 Problem**. If you query a list of 50 users and ask for their latest post, a naive GraphQL server might execute 1 SQL query for the users, and then 50 separate SQL queries for the posts. This kills database performance.

The Fix: DataLoaders. A DataLoader is a batching and caching utility. It waits for the event loop tick to collect all IDs requested by resolvers, and then fires a single batch query (e.g., SELECT * FROM posts WHERE user_id IN (...)) to fetch them all at once.

Conclusion

GraphQL shifts the complexity from the frontend (orchestrating multiple requests) to the backend (optimizing resolvers). For data-rich, hierarchical applications, this trade-off is often worth it. By mastering Federation for scale, DataLoaders for performance, and Complexity Analysis for security, you can build APIs that are both developer-friendly and enterprise-ready.

FAQs

Can I use GraphQL with existing REST APIs?

Yes! Your resolvers can simply fetch data from your existing REST endpoints, acting as a gateway/wrapper for your legacy services.

How do I handle authentication in GraphQL?

Auth should be handled at the context level. pass the user/token into the `context` argument of your resolvers, and check permissions before returning data.