GraphQL’s superpower is that it lets clients ask for exactly the data they need, preventing the over-fetching and under-fetching common with REST.

Let’s see this in action. Imagine we have a blog with posts and authors.

A REST API might give us a GET /posts endpoint that returns something like this for each post:

{
  "id": "post-123",
  "title": "My First Post",
  "content": "This is the body of the post...",
  "authorId": "author-abc"
}

If we then want to display a list of post titles and their author’s names, we’d first call GET /posts. Then, for each post, we’d have to make another request to GET /authors/{authorId} to get the author’s name. This is under-fetching – we didn’t get the author’s name in the first request, and we’re making multiple round trips.

Alternatively, GET /posts might be designed to return all related data, including the author’s full details:

{
  "id": "post-123",
  "title": "My First Post",
  "content": "This is the body of the post...",
  "author": {
    "id": "author-abc",
    "name": "Alice",
    "email": "alice@example.com",
    "bio": "A writer and a coder."
  }
}

If our UI only needs the post title and author’s name, we’re over-fetching – we’re downloading content, email, and bio when we don’t need them. This wastes bandwidth and can slow down our application.

GraphQL solves this with a single endpoint, typically /graphql. A client sends a query specifying precisely what it wants. For the same scenario (post title and author name), the GraphQL query would look like this:

query GetPostTitlesAndAuthorNames {
  posts {
    title
    author {
      name
    }
  }
}

The server receives this query and returns only the requested data:

{
  "data": {
    "posts": [
      {
        "title": "My First Post",
        "author": {
          "name": "Alice"
        }
      },
      {
        "title": "Another Great Article",
        "author": {
          "name": "Bob"
        }
      }
    ]
  }
}

Notice how we get all the data in a single request, and we only get what we asked for.

The core concept behind GraphQL is a schema that defines the types of data available and the relationships between them. This schema is strongly typed, acting as a contract between the client and server. When a client sends a query, the GraphQL server parses it, validates it against the schema, and then resolves the requested fields by calling underlying data sources (databases, other APIs, etc.).

Think of the schema as a graph. You start at a root type (like Query) and traverse through fields to reach other types. For example, from Query, you can ask for posts. Each post object has a title and an author. The author object itself has fields like name, email, and bio. The client’s query is essentially a path through this graph.

The flexibility comes from the fact that clients can request any field on any type, as long as it’s defined in the schema. They can also ask for nested data, like posts { title author { name } }, without needing separate endpoints. Mutations are used for changing data, and subscriptions for real-time updates, providing a complete API solution.

A common misconception is that GraphQL is inherently slower than REST. In reality, while a single GraphQL query can be more complex than a simple REST GET request, it often leads to fewer overall network requests and less data transferred, resulting in a faster user experience, especially on mobile or in high-latency environments. The server’s efficiency in resolving the query is key, and well-designed GraphQL servers can be very performant.

The next hurdle for many is understanding how to handle complex relationships and batching requests efficiently on the server side to avoid the "N+1 problem" in GraphQL resolvers.

Want structured learning?

Take the full API Architecture course →