← Back to Articles

Working with APIs: REST vs GraphQL in Practice

Code

APIs are the backbone of modern web applications. I've worked with both REST and GraphQL APIs extensively, and each has its strengths and weaknesses. Understanding when to use which approach can make a big difference in your application's performance and developer experience.

My journey with APIs

When I first started building web applications, REST APIs were the standard. You had endpoints for different resources, and you made multiple requests to get the data you needed. It worked, but it wasn't always efficient.

Then I discovered GraphQL, and it changed how I thought about APIs. Being able to specify exactly what data I needed in a single request was revolutionary. But I've learned that neither approach is universally better—they each have their place.

REST API basics

REST (Representational State Transfer) is an architectural style for designing APIs. It uses HTTP methods and follows resource-based URLs:

// REST API examples
GET    /api/users          // Get all users
GET    /api/users/123      // Get user with ID 123
POST   /api/users          // Create a new user
PUT    /api/users/123      // Update user 123
DELETE /api/users/123      // Delete user 123

GET    /api/users/123/posts // Get posts for user 123

Over-fetching and under-fetching problems

One of the biggest issues with REST APIs is data fetching efficiency:

  • Over-fetching: Getting more data than you need
  • Under-fetching: Not getting enough data, requiring additional requests

For example, if you want to display a list of users with their basic information, a REST endpoint might return:

{
  "users": [
    {
      "id": 1,
      "name": "John Doe",
      "email": "john@example.com",
      "avatar": "https://...",
      "bio": "Long biography text...",
      "createdAt": "2023-01-01T00:00:00Z",
      "lastLogin": "2023-12-01T00:00:00Z",
      "preferences": { ... },
      "stats": { ... }
    }
  ]
}

But if you're only showing name and avatar in a list, you're over-fetching a lot of data.

GraphQL to the rescue

GraphQL solves these problems by letting the client specify exactly what data it needs:

query GetUsers {
  users {
    id
    name
    avatar
  }
}

The server responds with exactly that data:

{
  "data": {
    "users": [
      {
        "id": 1,
        "name": "John Doe",
        "avatar": "https://..."
      }
    ]
  }
}

Implementing GraphQL

Setting up a GraphQL API with Apollo Server:

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

const typeDefs = gql`
  type User {
    id: ID!
    name: String!
    email: String!
    avatar: String
    posts: [Post!]!
  }

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

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

const resolvers = {
  Query: {
    users: async () => {
      // Fetch users from database
      return await User.findAll();
    },
    user: async (_, { id }) => {
      return await User.findById(id);
    },
  },
  User: {
    posts: async (user) => {
      return await Post.findByAuthor(user.id);
    },
  },
};

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

Schema definition

GraphQL schemas define the structure of your data:

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

type Post {
  id: ID!
  title: String!
  content: String!
  author: User!
  publishedAt: DateTime!
  tags: [String!]!
}

type Query {
  users(limit: Int, offset: Int): [User!]!
  user(id: ID!): User
  postsByTag(tag: String!): [Post!]!
}

type Mutation {
  createUser(input: CreateUserInput!): User!
  createPost(input: CreatePostInput!): Post!
}

Client-side GraphQL with Apollo Client

Using Apollo Client in React:

import { ApolloClient, InMemoryCache, ApolloProvider, useQuery, gql } from '@apollo/client';

const client = new ApolloClient({
  uri: 'http://localhost:4000/graphql',
  cache: new InMemoryCache(),
});

const GET_USERS = gql`
  query GetUsers {
    users {
      id
      name
      avatar
      posts {
        id
        title
      }
    }
  }
`;

function UsersList() {
  const { loading, error, data } = useQuery(GET_USERS);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return (
    <div>
      {data.users.map(user => (
        <div key={user.id}>
          <img src={user.avatar} alt={user.name} />
          <h3>{user.name}</h3>
          <p>{user.posts.length} posts</p>
        </div>
      ))}
    </div>
  );
}

Performance considerations

REST performance:

  • Multiple endpoints can be cached independently
  • Simpler server-side logic
  • Predictable request patterns
  • Easier to optimize with HTTP caching

GraphQL performance challenges:

  • N+1 query problem
  • Complex query parsing
  • Cache invalidation difficulties
  • Potential for expensive queries

Solving GraphQL performance issues:

DataLoader for batching:

import DataLoader from 'dataloader';

const userLoader = new DataLoader(async (userIds) => {
  const users = await User.findByIds(userIds);
  return userIds.map(id => users.find(user => user.id === id));
});

const resolvers = {
  Query: {
    users: async () => await User.findAll(),
  },
  User: {
    posts: async (user, _, { loaders }) => {
      return await loaders.postsByUserId.load(user.id);
    },
  },
};

Query complexity limits:

const { ApolloServerPluginQueryComplexity } = require('apollo-server-core');

const server = new ApolloServer({
  typeDefs,
  resolvers,
  plugins: [
    ApolloServerPluginQueryComplexity({
      maximumComplexity: 1000,
      onComplete: (complexity) => {
        console.log('Query complexity:', complexity);
      },
    }),
  ],
});

REST vs GraphQL: When to use which

Choose REST when:

  • You have simple, predictable data requirements
  • You're building for mobile apps with limited bandwidth
  • You want to leverage HTTP caching extensively
  • Your API serves many different clients with different needs
  • You prefer simplicity and don't want the complexity of schemas

Choose GraphQL when:

  • You have complex data relationships
  • Different clients need different data shapes
  • You're building for web apps where bandwidth isn't a major concern
  • You want to reduce over-fetching and under-fetching
  • You're willing to invest in schema design and tooling

Hybrid approaches

Many teams use both approaches:

  • REST for CRUD operations: Simple create, read, update, delete
  • GraphQL for complex queries: When you need flexible data fetching
  • REST for file uploads: GraphQL isn't great for multipart requests
  • GraphQL for real-time data: With subscriptions

Migration strategies

If you're migrating from REST to GraphQL:

  1. Start with a subset: Don't try to migrate everything at once
  2. Use schema stitching: Combine REST and GraphQL APIs
  3. Maintain REST endpoints: Keep them for existing clients
  4. Gradual migration: Move clients to GraphQL over time

My experience

I've used both REST and GraphQL extensively. REST is simpler to get started with and works well for straightforward APIs. GraphQL shines when you have complex data requirements and want to optimize for performance and developer experience.

That said, GraphQL has a steeper learning curve and requires more careful planning. If you're building a simple API, start with REST. If you're building something more complex, or if you have multiple clients with different data needs, GraphQL is worth considering.

The choice isn't always binary. Many successful applications use both approaches, choosing the right tool for each job. Focus on what works best for your team and your users.

About the author

Rafael De Paz

Full Stack Developer

Passionate full-stack developer specializing in building high-quality web applications and responsive sites. Expert in robust data handling, leveraging modern frameworks, cloud technologies, and AI tools to deliver scalable, high-performance solutions that drive user engagement and business growth. I harness AI technologies to accelerate development, testing, and debugging workflows.

Tags:

Share: