GraphQL vs REST: When to Use Which

Every backend developer eventually faces this question: should I build a REST API or use GraphQL? The answer isn't about which technology is 'better'—it's about matching architectural patterns to...

Key Insights

  • REST excels at simple CRUD operations with excellent HTTP caching, while GraphQL shines when clients need flexible data fetching across complex relationships
  • The “right” choice depends more on your client diversity and data graph complexity than abstract architectural preferences—mobile apps with varied screens benefit enormously from GraphQL’s precision
  • You don’t need to choose exclusively: many production systems successfully run REST for public APIs and simple operations while using GraphQL for complex internal client needs

The API Architecture Decision

Every backend developer eventually faces this question: should I build a REST API or use GraphQL? The answer isn’t about which technology is “better”—it’s about matching architectural patterns to your specific constraints. REST has powered the web for two decades with good reason, while GraphQL solves real problems that emerge at scale. Let’s cut through the hype and examine when each approach makes practical sense.

REST Fundamentals & Strengths

REST (Representational State Transfer) organizes APIs around resources identified by URLs. You manipulate these resources using standard HTTP verbs: GET to retrieve, POST to create, PUT/PATCH to update, DELETE to remove. This simplicity is REST’s superpower.

Here’s a straightforward Express.js REST API for a blog:

const express = require('express');
const app = express();
app.use(express.json());

// In-memory store (use a real database in production)
const posts = new Map();
const comments = new Map();

// Posts endpoints
app.get('/api/posts', (req, res) => {
  res.json(Array.from(posts.values()));
});

app.get('/api/posts/:id', (req, res) => {
  const post = posts.get(req.params.id);
  if (!post) return res.status(404).json({ error: 'Post not found' });
  res.json(post);
});

app.post('/api/posts', (req, res) => {
  const post = {
    id: Date.now().toString(),
    title: req.body.title,
    content: req.body.content,
    authorId: req.body.authorId,
    createdAt: new Date().toISOString()
  };
  posts.set(post.id, post);
  res.status(201).json(post);
});

app.put('/api/posts/:id', (req, res) => {
  const post = posts.get(req.params.id);
  if (!post) return res.status(404).json({ error: 'Post not found' });
  
  Object.assign(post, req.body);
  res.json(post);
});

app.delete('/api/posts/:id', (req, res) => {
  if (!posts.delete(req.params.id)) {
    return res.status(404).json({ error: 'Post not found' });
  }
  res.status(204).send();
});

// Comments for a specific post
app.get('/api/posts/:postId/comments', (req, res) => {
  const postComments = Array.from(comments.values())
    .filter(c => c.postId === req.params.postId);
  res.json(postComments);
});

app.listen(3000);

REST’s strengths become apparent in production:

HTTP caching works out of the box. CDNs and browsers understand GET requests, ETags, and Cache-Control headers. You get performance for free.

Tooling is universal. Every developer knows how to use curl, Postman, or browser DevTools to debug REST endpoints. No special clients required.

Stateless operations scale horizontally. Load balancers distribute requests trivially because each request contains everything needed to process it.

Simple mental model. Resources map to database tables. URLs are predictable. Junior developers become productive quickly.

GraphQL Fundamentals & Strengths

GraphQL flips the script: instead of multiple endpoints, you expose a single endpoint with a strongly-typed schema. Clients query exactly what they need using a declarative syntax.

Here’s the same blog functionality in GraphQL using Apollo Server:

const { ApolloServer, gql } = require('apollo-server');

// Schema definition
const typeDefs = gql`
  type Post {
    id: ID!
    title: String!
    content: String!
    author: User!
    comments: [Comment!]!
    createdAt: String!
  }
  
  type User {
    id: ID!
    name: String!
    email: String!
    posts: [Post!]!
  }
  
  type Comment {
    id: ID!
    content: String!
    author: User!
    post: Post!
    createdAt: String!
  }
  
  type Query {
    post(id: ID!): Post
    posts: [Post!]!
    user(id: ID!): User
  }
  
  type Mutation {
    createPost(title: String!, content: String!, authorId: ID!): Post!
    updatePost(id: ID!, title: String, content: String): Post!
    deletePost(id: ID!): Boolean!
  }
`;

// Resolvers implement the schema
const resolvers = {
  Query: {
    post: (_, { id }) => posts.get(id),
    posts: () => Array.from(posts.values()),
    user: (_, { id }) => users.get(id),
  },
  
  Post: {
    author: (post) => users.get(post.authorId),
    comments: (post) => 
      Array.from(comments.values()).filter(c => c.postId === post.id),
  },
  
  User: {
    posts: (user) => 
      Array.from(posts.values()).filter(p => p.authorId === user.id),
  },
  
  Comment: {
    author: (comment) => users.get(comment.authorId),
    post: (comment) => posts.get(comment.postId),
  },
  
  Mutation: {
    createPost: (_, { title, content, authorId }) => {
      const post = {
        id: Date.now().toString(),
        title,
        content,
        authorId,
        createdAt: new Date().toISOString()
      };
      posts.set(post.id, post);
      return post;
    },
    
    updatePost: (_, { id, ...updates }) => {
      const post = posts.get(id);
      if (!post) throw new Error('Post not found');
      Object.assign(post, updates);
      return post;
    },
    
    deletePost: (_, { id }) => posts.delete(id),
  },
};

const server = new ApolloServer({ typeDefs, resolvers });
server.listen().then(({ url }) => console.log(`Server ready at ${url}`));

Now clients can request precisely what they need:

query GetPostWithDetails {
  post(id: "123") {
    title
    content
    author {
      name
      email
    }
    comments {
      content
      author {
        name
      }
    }
  }
}

This single query replaces what would require multiple REST calls. The GraphQL runtime handles the coordination.

Side-by-Side Comparison

Consider fetching a user’s profile with their three most recent posts and comment counts.

REST approach:

// Client needs 3+ requests
const user = await fetch('/api/users/456').then(r => r.json());
const posts = await fetch('/api/users/456/posts?limit=3').then(r => r.json());

// For each post, get comment count (N+1 problem)
const postsWithCounts = await Promise.all(
  posts.map(async post => ({
    ...post,
    commentCount: await fetch(`/api/posts/${post.id}/comments`)
      .then(r => r.json())
      .then(comments => comments.length)
  }))
);

const profile = { user, posts: postsWithCounts };

GraphQL approach:

query UserProfile {
  user(id: "456") {
    name
    email
    posts(limit: 3) {
      title
      createdAt
      comments {
        id
      }
    }
  }
}

The GraphQL query is clearer and executes in one round trip. Your resolvers handle the data fetching logic, with tools like DataLoader preventing N+1 queries on the backend.

Decision Framework: When to Use Which

Choose REST when:

  • You’re building a public API. Third-party developers expect REST. Documentation is straightforward. Rate limiting and caching are well-understood.

  • Your data model is simple and stable. CRUD operations on independent resources don’t need GraphQL’s flexibility.

  • Caching is critical. E-commerce product catalogs, content sites, and read-heavy applications benefit enormously from HTTP caching layers.

  • Your team is small or junior. REST’s learning curve is gentler. Debugging is simpler. Fewer abstractions mean faster onboarding.

Choose GraphQL when:

  • You support multiple client types. Mobile apps need minimal payloads. Web dashboards need comprehensive data. GraphQL lets each client request what it needs without backend changes.

  • Your data is highly relational. Social networks, project management tools, and analytics dashboards involve deep object graphs. GraphQL eliminates waterfall requests.

  • You’re building an internal API. You control both client and server. The type safety and tooling (autocomplete, validation) accelerate development.

  • API evolution is constant. Adding fields to a GraphQL schema doesn’t break existing queries. Deprecating fields is explicit and gradual.

Red flags for GraphQL:

  • File uploads (doable but awkward)
  • Real-time subscriptions at massive scale (WebSockets add complexity)
  • Teams unfamiliar with schema design and resolver optimization
  • Heavy reliance on HTTP caching infrastructure

Real-World Use Cases

E-commerce platform: Use REST for product catalog (cache heavily), GraphQL for checkout flow (complex, personalized data).

Social media app: GraphQL is ideal. Feeds, profiles, and notifications involve deeply nested data. Different screens need different subsets.

SaaS admin dashboard: GraphQL shines. Power users need flexible filtering, sorting, and data combinations. The schema documents available data automatically.

Webhook/integration API: REST is the standard. External systems expect predictable endpoints and clear HTTP semantics.

Mobile-first application: GraphQL reduces payload sizes significantly. A mobile profile screen might need 5 fields while the web version needs 20—one schema serves both.

Conclusion & Migration Considerations

The GraphQL vs REST decision isn’t binary. Shopify runs both: REST for their public API (stability, familiarity) and GraphQL for their admin interface (flexibility, efficiency). GitHub offers both for similar reasons.

If you’re migrating from REST to GraphQL, start with a facade: build GraphQL resolvers that call your existing REST endpoints. This lets clients adopt GraphQL while you gradually optimize resolvers to query databases directly.

Going from GraphQL to REST is less common but happens when teams find GraphQL’s complexity outweighs its benefits for their use case. Tools like Hasura and PostGraphile can auto-generate GraphQL from databases, reducing boilerplate if you’re starting fresh.

The right choice aligns with your constraints: team skills, client needs, data complexity, and performance requirements. REST’s simplicity and caching win for many scenarios. GraphQL’s flexibility and type safety win for complex, client-diverse applications. Choose based on your specific context, not industry trends.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.