DEV Community

Cover image for GraphQL for Beginners
Gokul G.K.
Gokul G.K.

Posted on

GraphQL for Beginners

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. 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.
  2. 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.
  3. 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:

GraphQL Concepts

  1. Queries: For fetching data. They're idempotent (safe to retry) and can include arguments, fragments (reusable query parts), and variables for dynamic inputs.
  2. Mutations: For modifying data. They follow the same structure as queries but signal changes.
  3. 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"
}
Enter fullscreen mode Exit fullscreen mode

GraphQL: Client specifies needed fields

query {
  product(id: 2001) {
    name
    price
    stock
  }
}
# Returns ONLY: name, price, stock
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

GraphQL playground
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.

  1. 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
  2. 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.
  3. Handle Errors Gracefully Using the errors Field + 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.

Best Practices in GraphQL

Top comments (2)

Collapse
 
gokul_gk profile image
Gokul G.K.

GraphQL official documentation

Some comments may only be visible to logged-in visitors. Sign in to view all comments.