Why GraphQL Emerged: Solving the Pain Points of REST APIs
APIs are the backbone of how applications communicate and exchange data. REST has been the go-to architectural style for building APIs (at least for me). It's straightforward, relying on standard HTTP methods, with endpoints structured around resources (e.g., /users/123 to fetch a specific user). As applications grew more complex, think mobile apps, single-page web apps, and microservices, REST started showing its limitations.
The Over-Fetching and Under-Fetching Problems in REST
One of the biggest headaches with REST is overfetching and underfetching of data.
Overfetching happens when an API endpoint returns more data than the client actually needs.
Imagine a social media app that displays only a user's name and profile picture could use a REST endpoint like /users/123 to return only the relevant data. Returning the entire user object, including unnecessary details, wastes bandwidth and slows performance, especially on mobile devices.
On the other side, underfetching occurs when a single endpoint doesn't provide enough data, requiring the client to make multiple API calls.
GraphQL flips this script by empowering clients to specify precisely which data they need in a single request. Instead of rigid endpoints, clients send a query that looks like this:
query {
products {
id
name
category
price
}
}
The server responds with that exact structure, reducing network overhead, minimizing round-trip times, and speeding up development by preventing frontend teams from being blocked by backend changes.
| Feature | Description |
|---|---|
| Strongly Typed | Complete type system defined in schema |
| Declarative | Client specifies what data is needed |
| Single Endpoint | All requests go to /graphql
|
| Efficient | No over-fetching or under-fetching |
| Self-Documenting | Schema is the API documentation |
| Developer-Friendly | Built-in playground (GraphiQL) |
GraphQL as a Query Language and Schema-Based Runtime
GraphQL is both a query language for your data and a runtime that executes queries based on a defined schema, which describes available data types, their relationships, and how to query them. It's usually written in Schema Definition Language (SDL), like:
type User {
id: ID!
name: String!
posts: [Post!]!
}
type Post {
title: String!
content: String
}
type Query {
user(id: ID!): User
}
This schema is accessed through a single HTTP endpoint (usually /graphql), unlike REST's multiple URLs. Clients POST their queries to this endpoint, which validates them against the schema for type safety and rejects invalid requests. While transport-agnostic, it typically uses HTTP for easy integration with existing systems.
Core GraphQL Concepts: Building Blocks for Powerful APIs
The Schema: Types, Queries, Mutations, and Strong Typing.
The schema is the foundation of any GraphQL API. It's a blueprint that defines:
- Types: These describe your data models. Scalar types (e.g., String, Int, Boolean) are built-in, while object types (like User or Post) are custom. You can define enums, interfaces, unions, and input types for added complexity.
-
Queries: The entry points for reading data. Think of them as read-only operations. The Query type in the schema lists available queries, like
user(id: ID!), which returns a User object. - Mutations: For writing data, such as creating, updating, or deleting. Similar to queries but side-effecting.
GraphQL enforces strong typing for every field, allowing runtime checks against defined types. This catches errors early, avoiding issues like receiving a string instead of a number. In contrast, REST often uses loosely typed JSON, which can lead to fragile client code.
Operations: Queries, Mutations, and Subscriptions
GraphQL supports three main operation types:
- Queries: For fetching data. They're idempotent (safe to retry) and can include arguments, fragments (reusable query parts), and variables for dynamic inputs.
- Mutations: For modifying data. They follow the same structure as queries but signal changes.
-
Subscriptions: For real-time updates. Unlike queries and mutations (which are request-response), subscriptions use WebSockets to push data from server to client when events occur, like new messages in a chat app. A subscription might be
subscription { newMessage(chatId: ID!): Message }, keeping the client in sync without polling.
Resolvers: The Data-Fetching Glue
Behind every field in a query or mutation is a resolver, a function that knows how to fetch or compute the data for that field. Resolvers are like mini-handlers, executed in a tree-like fashion as the query resolves from root to leaves.
In GraphQL, the root resolver fetches a user, while child resolvers retrieve related posts. This modular design allows data to be pulled from various sources, unlike REST controllers, which handle entire responses at once, leading to monolithic code. Resolvers enhance separation of concerns, enabling independent optimization and security for each field.
Java Code Example.
Let's convert the REST example for project Aquaworld to GraphQL.
REST vs GraphQL
AquaWorld Endpoint Comparison
| Operation | REST | GraphQL |
|---|---|---|
| Get all products | GET /api/v1/products |
query { products { ... } } |
| Get single product | GET /api/v1/products/2001 |
query { product(id: 2001) { ... } } |
| Create order | POST /api/v1/orders |
mutation { createOrder(input: {...}) { ... } } |
| Get order with items | 2 requests | 1 request with nested data |
| Filter by name | GET /api/v1/products/search?name=... |
query { searchProducts(name: "...") { ... } } |
Data Fetching Comparison
REST: Fixed response structure (over-fetching)
GET /api/v1/products/2001
{
"id": 2001,
"name": "Red Guppy Male",
"category": "guppies",
"description": "...",
"price": 5.99,
"stock": 25,
"imageUrl": "...",
"createdAt": "2025-01-15"
}
GraphQL: Client specifies needed fields
query {
product(id: 2001) {
name
price
stock
}
}
# Returns ONLY: name, price, stock
Quick Start Guide
Pull code from the API Architecture Repo
# 1. Navigate to the project.
cd /Users/gokulg.k/Documents/GitHub/API_Architecture/GraphQL/aquaworld-graphql-api
# 2. Build
mvn clean install
# 3. Run
mvn spring-boot:run
Check out the complete documentation for building the project from scratch, the API schema, the Visual Guide, and more.
GraphQL Endpoint
-
Query & Mutation:
http://localhost:8080/aquaworld/graphql -
Interactive Playground:
http://localhost:8080/aquaworld/graphiql
Make Use of the GraphQL playground to test all queries.

Note: All the magic happens inside the resolver with the help of some annotations.
GraphQL Annotations in Spring Boot (spring-graphql)
| Annotation | Description / When to Use |
|---|---|
@Controller |
Marks the class as a GraphQL controller (same annotation as in Spring MVC/REST, but used here for GraphQL resolvers) |
@QueryMapping |
Maps the method to a field in the root Query type — used for top-level read/fetch operations |
@MutationMapping |
Maps the method to a field in the root Mutation type — used for top-level create/update/delete operations |
@SubscriptionMapping |
Maps the method to a field in the root Subscription type — used for real-time updates (WebSocket-based) |
@SchemaMapping |
Most flexible mapping — connects the method to any field in the schema (root-level or nested object fields) |
@BatchMapping |
Batch-loads related data for multiple parent objects at once — prevents N+1 query problem in lists/relationships |
@Argument |
Explicitly binds a GraphQL argument to a method parameter (optional when parameter name matches the argument name) |
GraphQL Design and Best Practices: Building APIs That Age Gracefully
GraphQL’s flexibility is powerful, but without discipline, it can lead to bloated schemas, performance nightmares, and maintenance headaches. The following best practices help create client-centric, stable, and performant GraphQL APIs that evolve smoothly over time.
- Keep the Schema Client-Centric and Stable – Evolve by Addition, Not Replacement
One of GraphQL’s biggest promises is that clients dictate the shape of the data, not the server.- Design from the UI/use-case perspective
- Avoid breaking changes at all costs
- Evolve by addition instead of versioning the whole API
- Use Pagination Patterns (Connections / Cursors) to Avoid Huge Responses
GraphQL makes it dangerously easy to write queries that return thousands of items (e.g., products { id name price }). Without limits, this kills performance and can even DDoS your own server.
Best practice: Always paginate lists using the Relay-style Connections pattern. - Handle Errors Gracefully Using the
errorsField + Custom Extensions Unlike REST (where HTTP status + body tells the story), GraphQL always returns 200 OK if the query is syntactically valid—even if business logic fails. All errors live in the top-level errors array.Best practice: Structured, machine-readable errors.



Top comments (2)
GraphQL official documentation
Some comments may only be visible to logged-in visitors. Sign in to view all comments.