Skip to main content
Back to Journal
GraphQLWeb Development

GraphQL in Practice: What REST Doesn't Tell You

I had been building REST APIs for years and was perfectly happy with them. Then a project came along where the frontend needed data from multiple related resources on almost every page. The REST approach meant either making five separate API calls per page load or building custom aggregation endpoints that coupled the backend to the frontend's exact needs. Neither option felt right.

A friend suggested GraphQL, so I gave it a real try. Six months later, I have thoughts.

The Setup

I used Apollo Server on the backend and Apollo Client on the frontend. The Apollo ecosystem is the most mature GraphQL tooling for JavaScript, and the documentation is solid.

The server setup is straightforward. You define a schema using SDL (Schema Definition Language) and write resolver functions for each field.

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

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

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

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

const resolvers = {
  Query: {
    users: () => db.users.findAll(),
    user: (_, { id }) => db.users.findById(id),
    posts: () => db.posts.findAll(),
  },
  User: {
    posts: (user) => db.posts.findByAuthorId(user.id),
  },
  Post: {
    author: (post) => db.users.findById(post.authorId),
  },
};

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

The schema is your contract. It defines exactly what data is available and how it is structured. The resolvers are the implementation. This separation is one of my favorite things about GraphQL. You can read the schema and understand the entire API surface without looking at any implementation code.

The N+1 Problem

This is the thing that will bite you if you are not prepared for it. Look at the User.posts resolver above. If you query a list of 50 users and ask for their posts, the resolver runs once per user. That is 1 query for the users list plus 50 individual queries for each user's posts. That is 51 database queries for what should be 2.

The standard solution is DataLoader, a utility that batches and caches database lookups within a single request.

const DataLoader = require('dataloader');

const postLoader = new DataLoader(async (userIds) => {
  const posts = await db.posts.findByAuthorIds(userIds);
  return userIds.map(id =>
    posts.filter(post => post.authorId === id)
  );
});

const resolvers = {
  User: {
    posts: (user) => postLoader.load(user.id),
  },
};

DataLoader collects all the load calls that happen in the same tick of the event loop and batches them into a single call to your batch function. Instead of 50 individual queries, you get one query with all 50 user IDs. This is essential for any GraphQL server that touches a database.

What Works Well

The biggest win is flexibility for the frontend. With REST, if a page needs user data plus their posts plus the post comments, you either make three API calls or you build a custom endpoint. With GraphQL, you write one query that asks for exactly what you need.

Schema-first development is also great for team collaboration. The frontend and backend teams agree on the schema early, and then both sides can work in parallel. The schema serves as living documentation that is always accurate because the server enforces it.

Apollo Client's caching is genuinely impressive. It normalizes your data into a client-side cache keyed by type and ID. When you create a new post, you can update the cache directly and the UI reflects the change without refetching from the server.

What Is Surprisingly Hard

Error handling is more nuanced than REST. In REST, a 404 means the resource was not found. A 500 means something broke. In GraphQL, you almost always get a 200 response. Errors come back in an errors array alongside partial data. Distinguishing between "this field is null because the data does not exist" and "this field is null because something went wrong" requires discipline in your error handling strategy.

File uploads are another area where GraphQL is awkward. The spec does not cover file uploads natively. You either use a multipart request extension or handle uploads through a separate REST endpoint, which feels like admitting that GraphQL does not cover everything.

Pagination is more complex than REST. Instead of simple page numbers, the GraphQL community generally uses cursor-based pagination with connections, edges, and nodes. It works well once you understand it, but the initial learning curve is steep.

When REST Is Still Better

For simple CRUD APIs with predictable access patterns, REST is less overhead. You do not need a schema, a resolver layer, DataLoader, and a client-side cache when a few fetch calls would do the job.

For public APIs, REST is also easier for consumers. Anyone can call a REST endpoint with curl. GraphQL requires understanding the query language, which is an extra barrier for third-party developers.

My rule of thumb now: if the frontend has complex, varied data requirements across many views, GraphQL is worth the setup cost. If the data access patterns are simple and predictable, REST gets you there faster with less infrastructure.

graphqlapollorest-apischema-designquery-optimization