Back to Scaling Node.js Applications guides

GraphQL vs REST

Stanley Ulili
Updated on May 12, 2025

When building an API, you’ll likely choose between two popular options: REST and GraphQL.

REST is the traditional choice. It’s been around longer, is easy to understand, and structures data into resources. It works well when your app needs specific, well-defined information.

GraphQL, developed by Facebook in 2015, offers more flexibility. It lets you ask for exactly the data you need, which is helpful when your app deals with complex or changing data needs.

This article breaks down how REST and GraphQL differ and helps you decide which one is better for your project.

What is REST?

REST is the traditional way to build APIs that most developers are familiar with.

Roy Fielding defined it in 2000, basing it on regular HTTP methods (GET, POST, PUT, DELETE) that map to database operations. With REST, you organize your API around resources - things like users, products, or orders that clients can access through specific URLs.

REST uses a straightforward approach - each URL represents a resource, and HTTP methods tell the server what to do with it. Need user data? Make a GET request to /users/123. Want to update that user? Send a PUT request to the same URL. This predictable pattern makes REST easy to understand and use in many different applications.

What is GraphQL?

GraphQL takes a completely different approach to APIs. Facebook created it in 2015 to solve problems they faced with their mobile apps.

Instead of having many different endpoints, GraphQL gives you just one. But here's the key difference - the client tells the server exactly what data it wants. You write a query that looks a bit like JSON, specifying precisely which fields you need.

GraphQL uses a schema that acts like a contract between your frontend and backend. This schema defines all the data types and operations available. The beauty of this system is that your frontend can evolve without constantly changing the backend - clients simply request whatever data they need, whenever they need it.

GraphQL vs REST: a quick comparison

Your choice between GraphQL and REST will impact how fast you can develop features and how well your app performs. Each approach works best in different situations.

Here's a simple breakdown of the main differences:

Feature GraphQL REST
Data fetching One request gets everything you need Multiple endpoints for different data
Response structure You decide what data you get back Server decides what you get back
Versioning Evolves without version numbers Uses explicit versions (v1, v2)
Learning curve Takes more time to learn Easier to grasp initially
Performance optimization Uses batching and query analysis Uses HTTP caching and ETags
Payload size You only get what you ask for Often returns extra data you don't need
Documentation Self-documents through the schema Needs separate documentation tools
Error handling Can return partial data with errors Either succeeds completely or fails
State modification Uses mutations with predictable responses Uses HTTP methods with varied responses
Real-time capabilities Built-in subscription system Needs WebSockets added separately
Tooling ecosystem Growing set of specialized tools Huge selection of mature tools
Backend implementation Schema-first with resolver functions Resource-oriented endpoints
Network bandwidth Uses less bandwidth for complex data Often requires multiple network requests
Caching More complex, needs custom solutions Simple, uses HTTP's built-in caching
Community size Growing quickly but newer Massive and well-established

API design

The way you design your API has a big impact on how developers interact with it. A well-designed API should be simple to understand, easy to use, and straightforward to maintain.

GraphQL and REST take completely different approaches to API design.

With REST, you create separate URLs (endpoints) for each resource in your system. You use standard HTTP methods to perform different operations on these resources. The approach is simple to understand:

rest-diagram.svg

 
# REST API design example
GET /api/users                # Get all users
GET /api/users/42             # Get user with ID 42
GET /api/users/42/posts       # Get posts for user 42
POST /api/users               # Create a new user
PUT /api/users/42             # Update user 42
DELETE /api/users/42          # Delete user 42

REST gives you a clean, organized structure, but you need to create many different endpoints as your API grows. You also need to think carefully about how to name and organize your resources.

GraphQL takes a totally different approach. You create a schema that defines all your data types and how they connect. Instead of many endpoints, you have just one that accepts queries:

graphql-diagram.svg

 
# GraphQL schema definition
type User {
  id: ID!
  username: String!
  email: String!
  posts: [Post!]
}

type Post {
  id: ID!
  title: String!
  content: String!
  author: User!
}

type Query {
  user(id: ID!): User
  users: [User!]!
}

type Mutation {
  createUser(username: String!, email: String!): User!
  updateUser(id: ID!, username: String, email: String): User!
  deleteUser(id: ID!): Boolean!
}

GraphQL's schema shows not just what data you have but how it all connects together. It takes more work upfront to design a good schema, but gives you much more flexibility later. Your frontend developers can request exactly the data they need without you having to create new endpoints.

Query building

GraphQL and REST's biggest difference is how you get data from them.

With REST, you need to know exactly which endpoints to call and often make multiple requests to get related data. Your client code needs to understand the API structure and stitch together data from different sources:

 
// REST query example in JavaScript
async function getUserWithPosts(userId) {
  // First request to get user data
  const userResponse = await fetch(`/api/users/${userId}`);
  const user = await userResponse.json();

  // Second request to get user's posts
  const postsResponse = await fetch(`/api/users/${userId}/posts`);
  const posts = await postsResponse.json();

  // Combine the data
  return {
    ...user,
    posts
  };
}

With REST, you often make multiple trips to the server, and you get back whatever data the server decided to send. This can mean getting too much data you don't need or not enough data, requiring more requests.

GraphQL lets you write a query that specifies exactly what data you want, even if it's nested or comes from what would be multiple REST endpoints:

 
// GraphQL query example in JavaScript
async function getUserWithPosts(userId) {
  const query = `
    query GetUserWithPosts($userId: ID!) {
      user(id: $userId) {
        id
        username
        email
        posts {
          id
          title
          content
        }
      }
    }
  `;

  const response = await fetch('/graphql', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      query,
      variables: { userId }
    })
  });

  const result = await response.json();
  return result.data.user;
}

With GraphQL, you make one request and get exactly what you ask for – no more, no less. You can request a user and their posts in a single query, and even specify which fields you want from each. This cuts down on unnecessary data transfer and speeds up your app.

Data modification

Creating, updating, and deleting data works differently in each approach too.

In REST, you use different HTTP methods to tell the server what you want to do with a resource. The URL points to the resource, and the method (POST, PUT, DELETE) indicates the action:

 
// REST modification examples

// Creating a new user
async function createUser(userData) {
  const response = await fetch('/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(userData)
  });
  return response.json();
}

// Updating a user
async function updateUser(userId, userData) {
  const response = await fetch(`/api/users/${userId}`, {
    method: 'PUT', // Or PATCH for partial updates
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(userData)
  });
  return response.json();
}

// Deleting a user
async function deleteUser(userId) {
  const response = await fetch(`/api/users/${userId}`, {
    method: 'DELETE'
  });
  return response.status === 204; // No content success
}

REST's approach is simple and uses HTTP the way it was designed. But each endpoint can return different data structures, making your client code more complex as your API grows.

GraphQL handles all data changes through "mutations." Like queries, mutations let you specify exactly what data you want back after the change:

 
// GraphQL mutation examples

// Creating a user
async function createUser(userData) {
  const mutation = `
    mutation CreateUser($input: CreateUserInput!) {
      createUser(input: $input) {
        id
        username
        email
      }
    }
  `;

  const response = await fetch('/graphql', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      query: mutation,
      variables: { input: userData }
    })
  });

  const result = await response.json();
  return result.data.createUser;
}

// Similar pattern for update and delete operations

With GraphQL mutations, you get a consistent pattern for all data changes. You can request specific fields to return after the operation completes, which helps keep your client code clean and predictable.

Error handling

The way errors work in these approaches shows their different design philosophies.

REST uses HTTP status codes to tell you about errors. A 200 means success, 404 means not found, 500 means server error, and so on. This is simple but all-or-nothing:

 
// REST error handling
async function fetchUser(userId) {
  try {
    const response = await fetch(`/api/users/${userId}`);

    if (!response.ok) {
      if (response.status === 404) {
        throw new Error('User not found');
      } else if (response.status === 401) {
        throw new Error('Unauthorized access');
      } else {
        throw new Error(`Server error: ${response.status}`);
      }
    }

    return await response.json();
  } catch (error) {
    console.error('Failed to fetch user:', error);
    throw error;
  }
}

With REST, your request either works completely or fails completely. This can be a problem when you're trying to fetch several pieces of related data - if any part fails, you lose everything.

GraphQL handles errors differently. It can return both successful data and errors in the same response, letting you display partial data even when some parts fail:

 
// GraphQL error handling
async function fetchUserWithPosts(userId) {
  const query = `
    query GetUserWithPosts($userId: ID!) {
      user(id: $userId) {
        id
        username
        email
        posts {
          id
          title
          content
        }
      }
    }
  `;

  const response = await fetch('/graphql', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      query,
      variables: { userId }
    })
  });

  const result = await response.json();

  // GraphQL can return both data and errors
  if (result.errors) {
    console.warn('Some errors occurred:', result.errors);
  }

  // May still have partial data even with errors
  return result.data?.user;
}

GraphQL gives you detailed error information along with whatever data it could successfully get. This means your app can still show useful information to users even when part of a request fails. For example, if a user's profile loads but their recent posts fail, you can still show the profile.

Real-time capabilities

Modern apps often need real-time updates, and both approaches handle this differently.

REST has no built-in way to handle real-time data. You need to add WebSockets or Server-Sent Events alongside your REST API:

 
// REST with WebSockets for real-time updates
function subscribeToUserUpdates(userId) {
  const socket = new WebSocket(`wss://api.example.com/users/${userId}/updates`);

  socket.onmessage = (event) => {
    const data = JSON.parse(event.data);
    console.log('User update received:', data);
    // Update UI with new data
  };

  socket.onclose = () => {
    console.log('Connection closed, attempting to reconnect...');
    // Reconnect logic
  };

  return {
    unsubscribe: () => socket.close()
  };
}

This works, but it means running two separate systems - your REST API and your WebSocket server. Your client code needs to manage both connections and merge data from different sources.

GraphQL has subscriptions built right in, alongside queries and mutations. This gives you a consistent way to handle both regular and real-time data:

 
// GraphQL subscription client example
import { createClient } from 'graphql-ws';

const client = createClient({
  url: 'wss://api.example.com/graphql'
});

function subscribeToUserUpdates(userId) {
  const subscription = `
    subscription UserUpdates($userId: ID!) {
      userUpdated(userId: $userId) {
        id
        username
        email
        lastActive
      }
    }
  `;

  const unsubscribe = client.subscribe(
    {
      query: subscription,
      variables: { userId }
    },
    {
      next: (result) => {
        console.log('User update received:', result.data.userUpdated);
        // Update UI with new data
      },
      error: (error) => {
        console.error('Subscription error:', error);
      },
      complete: () => {
        console.log('Subscription completed');
      }
    }
  );

  return { unsubscribe };
}

GraphQL subscriptions use the same schema and syntax as regular queries. This makes real-time features feel like a natural extension of your API rather than a separate system bolted on later. Like queries, you can specify exactly which fields you want to receive when updates happen.

Performance optimization

Improving performance looks very different depending on whether you use REST or GraphQL. Each has its own strategies and trade-offs, so how you speed things up depends on the approach you choose.

REST makes great use of HTTP's built-in caching. This is simple and works well for most cases:

 
// REST with HTTP caching
async function fetchUser(userId) {
  const response = await fetch(`/api/users/${userId}`, {
    headers: {
      // Use conditional request if we have an ETag
      'If-None-Match': localStorage.getItem(`user-${userId}-etag`)
    }
  });

  if (response.status === 304) {
    // Not modified, use cached data
    return JSON.parse(localStorage.getItem(`user-${userId}-data`));
  }

  // Store ETag for future requests
  const etag = response.headers.get('ETag');
  if (etag) {
    localStorage.setItem(`user-${userId}-etag`, etag);
  }

  const data = await response.json();
  localStorage.setItem(`user-${userId}-data`, JSON.stringify(data));
  return data;
}

REST's caching works great for simple data, but gets complicated when you need to combine data from multiple sources. How do you cache a response that depends on several different resources that might change at different times?

GraphQL needs different optimization techniques. One of the most powerful is DataLoader, which helps batch and cache database queries:

 
// Server-side DataLoader example for GraphQL
import DataLoader from 'dataloader';

// Create a loader that batches and caches user requests
const userLoader = new DataLoader(async (userIds) => {
  console.log('Loading users:', userIds);
  // Instead of N separate database queries, load all users in one query
  const users = await db.users.findMany({
    where: {
      id: { in: userIds }
    }
  });

  // Return users in the same order as the keys
  return userIds.map(id => users.find(user => user.id === id) || null);
});

// Resolver function uses the loader
const resolvers = {
  Query: {
    user: async (_, { id }) => {
      return userLoader.load(id);
    }
  },
  User: {
    posts: async (user) => {
      // Each user.posts resolver call gets batched
      return postsByUserLoader.load(user.id);
    }
  }
};

GraphQL's performance tools help solve the common "N+1 query problem" where fetching a list of items can trigger dozens of separate database queries. With DataLoader, GraphQL can batch these into a single efficient query. This takes more work to set up than REST's caching, but gives you more control over exactly how data loading happens.

Documentation and discoverability

Good documentation helps developers use your API correctly. Each approach has a different way of handling this.

REST APIs typically use separate documentation tools like Swagger or OpenAPI:

 
// OpenAPI specification for a REST API (yaml)
openapi: 3.0.0
info:
  title: User API
  version: 1.0.0
paths:
  /users:
    get:
      summary: Get all users
      responses:
        '200':
          description: List of users
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/User'
  /users/{id}:
    get:
      summary: Get a user by ID
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: User details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: string
        username:
          type: string
        email:
          type: string

With REST, you need to keep your documentation updated separately from your code. This can lead to outdated docs if you forget to update them. However, tools like Swagger create nice interactive documentation that makes it easy to test API endpoints.

GraphQL has documentation built in. Clients can actually query the API itself to find out what data and operations are available:

 
// GraphQL introspection query
const introspectionQuery = `
  {
    __schema {
      types {
        name
        description
        fields {
          name
          description
          type {
            name
            kind
          }
        }
      }
    }
  }
`;

// Tools like GraphiQL, Apollo Studio, or GraphQL Playground
// use introspection to provide interactive documentation

GraphQL's schema tells you exactly what you can do with the API. Tools like GraphiQL use this to create documentation that's always accurate because it comes directly from the running code. You can explore what queries are possible, what fields are available, and even test queries right in the browser.

Final thoughts

Which should you choose? It depends on your app.

GraphQL is best for complex data, multiple client types, or real-time features. It avoids over-fetching and helps frontend teams with a strong type system and clear schema.

REST is simpler, widely supported, and easy to cache. It's a solid choice for public APIs and teams that want something familiar.

Both are good options. Pick the one that fits your project, team, and users

Author's avatar
Article by
Stanley Ulili
Stanley Ulili is a technical educator at Better Stack based in Malawi. He specializes in backend development and has freelanced for platforms like DigitalOcean, LogRocket, and AppSignal. Stanley is passionate about making complex topics accessible to developers.
Got an article suggestion? Let us know
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Make your mark

Join the writer's program

Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.

Write for us
Writer of the month
Marin Bezhanov
Marin is a software engineer and architect with a broad range of experience working...
Build on top of Better Stack

Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.

community@betterstack.com

or submit a pull request and help us build better products for everyone.

See the full list of amazing projects on github