Skip to main content
Back to Blog
GraphQLREST APIAPI DesignWeb DevelopmentBackendNode.js

GraphQL vs REST API: The Complete Comparison Guide for 2025

Compare GraphQL and REST APIs with real-world examples. Learn when to use each, performance trade-offs, and implementation patterns for modern applications.

6 min read

Choosing between GraphQL and REST is one of the most impactful architectural decisions you'll make for your API. After implementing both patterns across multiple production applications, I'll share the real trade-offs and when each approach shines.

Understanding the Fundamentals

REST: Resource-Based Architecture

REST (Representational State Transfer) organizes your API around resources with standard HTTP methods:

// REST endpoints for a blog application
GET    /api/posts          // List all posts
GET    /api/posts/123      // Get specific post
POST   /api/posts          // Create new post
PUT    /api/posts/123      // Update post
DELETE /api/posts/123      // Delete post

// Nested resources
GET    /api/posts/123/comments
GET    /api/users/456/posts

Each endpoint returns a fixed data structure. Simple and predictable.

GraphQL: Query-Based Architecture

GraphQL uses a single endpoint where clients specify exactly what data they need:

# GraphQL query for the same data
query GetPostWithAuthor {
  post(id: "123") {
    title
    content
    publishedAt
    author {
      name
      avatar
      bio
    }
    comments(first: 10) {
      text
      createdAt
      user {
        name
      }
    }
  }
}

One request, precisely the data you asked for.

The Over-fetching and Under-fetching Problem

REST's Challenge

With REST, you often face these issues:

// Under-fetching: Need multiple requests
const post = await fetch('/api/posts/123').then(r => r.json());
const author = await fetch(`/api/users/${post.authorId}`).then(r => r.json());
const comments = await fetch('/api/posts/123/comments').then(r => r.json());

// Over-fetching: Getting more data than needed
// The /api/posts endpoint returns 20 fields
// but you only need title and thumbnail for a list view

GraphQL's Solution

# Request exactly what you need for a post list
query PostList {
  posts(first: 10) {
    id
    title
    thumbnail
  }
}

# Request everything for a detail view
query PostDetail($id: ID!) {
  post(id: $id) {
    id
    title
    content
    author {
      name
      avatar
    }
    comments {
      text
      user {
        name
      }
    }
  }
}

Implementation Comparison

REST API with Express

// routes/posts.ts
import express from 'express';
import { prisma } from '../lib/prisma';

const router = express.Router();

router.get('/posts', async (req, res) => {
  const { page = 1, limit = 10, category } = req.query;

  const posts = await prisma.post.findMany({
    where: category ? { category } : undefined,
    skip: (Number(page) - 1) * Number(limit),
    take: Number(limit),
    include: {
      author: {
        select: { id: true, name: true, avatar: true }
      }
    }
  });

  res.json({ data: posts, page, limit });
});

router.get('/posts/:id', async (req, res) => {
  const post = await prisma.post.findUnique({
    where: { id: req.params.id },
    include: {
      author: true,
      comments: {
        include: { user: true },
        orderBy: { createdAt: 'desc' }
      }
    }
  });

  if (!post) {
    return res.status(404).json({ error: 'Post not found' });
  }

  res.json(post);
});

export default router;

GraphQL API with Apollo Server

// schema.ts
import { gql } from 'apollo-server-express';

export const typeDefs = gql`
  type Post {
    id: ID!
    title: String!
    content: String!
    publishedAt: DateTime
    author: User!
    comments(first: Int, after: String): CommentConnection!
  }

  type User {
    id: ID!
    name: String!
    avatar: String
    posts: [Post!]!
  }

  type Query {
    post(id: ID!): Post
    posts(first: Int, after: String, category: String): PostConnection!
    user(id: ID!): User
  }

  type Mutation {
    createPost(input: CreatePostInput!): Post!
    updatePost(id: ID!, input: UpdatePostInput!): Post!
    deletePost(id: ID!): Boolean!
  }
`;

// resolvers.ts
export const resolvers = {
  Query: {
    post: async (_, { id }, { prisma }) => {
      return prisma.post.findUnique({ where: { id } });
    },
    posts: async (_, { first = 10, after, category }, { prisma }) => {
      const posts = await prisma.post.findMany({
        where: category ? { category } : undefined,
        take: first + 1,
        cursor: after ? { id: after } : undefined,
        orderBy: { createdAt: 'desc' }
      });

      return {
        edges: posts.slice(0, first).map(post => ({
          node: post,
          cursor: post.id
        })),
        pageInfo: {
          hasNextPage: posts.length > first,
          endCursor: posts[first - 1]?.id
        }
      };
    }
  },
  Post: {
    author: async (post, _, { prisma }) => {
      return prisma.user.findUnique({ where: { id: post.authorId } });
    },
    comments: async (post, { first = 10 }, { prisma }) => {
      return prisma.comment.findMany({
        where: { postId: post.id },
        take: first
      });
    }
  }
};

Performance Considerations

REST Advantages

  1. HTTP Caching: Native browser and CDN caching works out of the box
  2. Simplicity: Easier to implement, debug, and monitor
  3. Predictable Load: Each endpoint has known resource requirements
// REST caching headers
router.get('/posts/:id', async (req, res) => {
  const post = await getPost(req.params.id);

  res.set({
    'Cache-Control': 'public, max-age=3600',
    'ETag': generateETag(post)
  });

  res.json(post);
});

GraphQL Advantages

  1. Reduced Round Trips: Single request for complex data needs
  2. Mobile Optimization: Request only needed fields to save bandwidth
  3. Type Safety: Strong typing catches errors at build time
// GraphQL with DataLoader to prevent N+1 queries
import DataLoader from 'dataloader';

const createLoaders = (prisma) => ({
  userLoader: new DataLoader(async (userIds) => {
    const users = await prisma.user.findMany({
      where: { id: { in: userIds } }
    });
    return userIds.map(id => users.find(u => u.id === id));
  })
});

When to Choose REST

REST is the better choice when:

  • Public APIs: Third-party developers expect REST conventions
  • Simple CRUD: Straightforward resource operations
  • Heavy Caching Needs: Static content, CDN-heavy architectures
  • Team Experience: Team is more familiar with REST patterns
  • Microservices: Service-to-service communication often simpler with REST
// REST excels at simple, cacheable endpoints
app.get('/api/products/:id', cacheMiddleware('1h'), async (req, res) => {
  const product = await getProduct(req.params.id);
  res.json(product);
});

When to Choose GraphQL

GraphQL excels when:

  • Complex Data Requirements: Nested, related data structures
  • Multiple Clients: Mobile, web, and desktop with different data needs
  • Rapid Frontend Development: Frontend team can iterate without backend changes
  • Real-time Features: Subscriptions built into the spec
# GraphQL subscriptions for real-time updates
subscription OnNewComment($postId: ID!) {
  commentAdded(postId: $postId) {
    id
    text
    user {
      name
      avatar
    }
  }
}

Hybrid Approach

Many successful applications use both:

// Public REST API for external consumers
app.use('/api/v1', restRouter);

// GraphQL for internal frontend applications
app.use('/graphql', graphqlMiddleware);

// REST for webhooks and simple integrations
app.post('/webhooks/stripe', stripeWebhookHandler);

Migration Strategy

If migrating from REST to GraphQL (or vice versa):

// Wrap existing REST endpoints in GraphQL resolvers
const resolvers = {
  Query: {
    post: async (_, { id }) => {
      const response = await fetch(`${REST_API}/posts/${id}`);
      return response.json();
    }
  }
};

Conclusion

There's no universal winner. REST remains excellent for simple, cacheable APIs with broad compatibility. GraphQL shines for complex applications with varying client needs and nested data structures.

My recommendation: Start with REST for new projects unless you have specific GraphQL requirements. The lower complexity pays off, and you can always add a GraphQL layer later if needed.

For more API development insights, check out my Node.js best practices guide and TypeScript patterns.