27.3. GraphQL
Table of Contents
Why GraphQL Exists
REST works well for many APIs, but it has some common pain points:
- Overfetching: You get more data than you need.
Example:/users/1returns user, posts, comments, avatar, even if you only need the user’s name. - Underfetching: You do not get enough data and must call multiple endpoints.
Example: You call/users/1then/users/1/poststhen/posts/{id}/commentsto build a single page. - Too many endpoints: You often create many URLs like
/users,/users/{id},/users/{id}/details,/users/{id}/posts, and so on.
GraphQL was created to solve these problems by letting clients ask exactly for the data they need, in a single request, over a single endpoint.
Key idea: In GraphQL, the client defines the shape of the response. The server provides a flexible schema, and the client picks fields from that schema in each query.
GraphQL is a query language for APIs and a runtime that executes those queries against your data, no matter where that data comes from (database, other APIs, files, etc.).
How GraphQL Works at a High Level
A GraphQL backend usually has:
- A single HTTP endpoint
Often/graphql, usually via POST. - A schema
The schema describes types, fields, and operations that clients can perform. - Resolvers
A resolver is a function that knows how to fetch the data for a specific field.
A GraphQL request is sent as a JSON body like:
{
"query": "{ hello }"
}
The server parses the query string, checks it against the schema, calls resolvers, then returns JSON like:
{
"data": {
"hello": "Hello world"
}
}The GraphQL Schema
The schema is the contract between the client and the server.
You normally write it using the GraphQL Schema Definition Language (SDL).
Example schema:
type User {
id: ID!
username: String!
email: String!
age: Int
}
type Query {
user(id: ID!): User
users: [User!]!
}Important parts:
type User { ... }defines an object type with fields.ID,String,Int,Float,Booleanare built-in scalar types.!means the value cannot be null.
Example:ID!is non-nullable.[User!]!is a non-null list of non-null users.
Rule: In GraphQL, the schema defines what clients can ask for. If it is not in the schema, clients cannot query it.
You also define entry points:
type Queryfor read operations.type Mutationfor write operations (create, update, delete).type Subscriptionfor real-time data over WebSockets.
Queries: Getting Data
A GraphQL query looks like the JSON shape you want to receive, but in a GraphQL-specific syntax.
Example schema:
type Post {
id: ID!
title: String!
body: String!
author: User!
}
type User {
id: ID!
username: String!
posts: [Post!]!
}
type Query {
user(id: ID!): User
}Example query:
query {
user(id: "1") {
id
username
posts {
id
title
}
}
}Example response:
{
"data": {
"user": {
"id": "1",
"username": "alice",
"posts": [
{ "id": "10", "title": "First post" },
{ "id": "11", "title": "Second post" }
]
}
}
}Notice:
- The response structure matches the query.
- You can select nested fields across relationships in a single request.
If you only need the username, you ask only for that:
query {
user(id: "1") {
username
}
}Response:
{
"data": {
"user": {
"username": "alice"
}
}
}Mutations: Writing Data
Reads use Query. Writes use Mutation.
Example schema additions:
input CreatePostInput {
title: String!
body: String!
authorId: ID!
}
type Post {
id: ID!
title: String!
body: String!
author: User!
}
type Mutation {
createPost(input: CreatePostInput!): Post!
}Example mutation:
mutation {
createPost(
input: {
title: "My first GraphQL post"
body: "This is cool"
authorId: "1"
}
) {
id
title
author {
username
}
}
}Response:
{
"data": {
"createPost": {
"id": "42",
"title": "My first GraphQL post",
"author": {
"username": "alice"
}
}
}
}Things to notice:
- You can send complex input using
inputtypes. - The mutation result can include nested fields, not just a success flag.
Variables: Reusing Queries and Mutations
Hardcoding IDs and values in queries is not practical. GraphQL supports variables.
Example mutation with variables:
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
}
}You send it with a JSON body like:
{
"query": "mutation CreatePost($input: CreatePostInput!) { createPost(input: $input) { id title } }",
"variables": {
"input": {
"title": "Another post",
"body": "Content",
"authorId": "1"
}
}
}You can also use variables in queries:
query GetUser($id: ID!) {
user(id: $id) {
id
username
}
}Fields, Arguments, and Aliases
Field arguments
Fields can take arguments, similar to function parameters.
Schema:
type Query {
posts(limit: Int, offset: Int): [Post!]!
}Query:
query {
posts(limit: 10, offset: 20) {
id
title
}
}Aliases
Sometimes you want to query the same field multiple times with different arguments. You can use aliases.
Schema:
type Query {
user(id: ID!): User
}Query with aliases:
query {
firstUser: user(id: "1") {
username
}
secondUser: user(id: "2") {
username
}
}Response:
{
"data": {
"firstUser": { "username": "alice" },
"secondUser": { "username": "bob" }
}
}Fragments: Reusing Field Selections
If you repeat the same group of fields many times, use a fragment.
Example:
fragment UserFields on User {
id
username
email
}
query {
users {
...UserFields
}
user(id: "1") {
...UserFields
}
}This helps keep large queries readable and consistent.
Comparing GraphQL and REST
| Aspect | REST | GraphQL |
|---|---|---|
| Endpoint structure | Many endpoints, one per resource or action | Usually a single /graphql endpoint |
| Data shape | Fixed per endpoint | Defined by the client in each query |
| Overfetching / underfetching | Common issue | Greatly reduced, client asks for exactly what it needs |
| HTTP methods | Uses verbs: GET, POST, PUT, PATCH, DELETE | Usually POST (sometimes GET for simple queries) |
| Versioning | Often /v1, /v2 APIs | Usually evolve schema without versioned URLs |
| Documentation | Swagger / OpenAPI, manual docs | Schema is introspectable and self-documenting |
| Caching | HTTP status codes and headers, URL-based | Trickier, often needs custom caching or client-side tools |
| Error handling | Via HTTP status codes | Mixed data and errors in one JSON response |
Important: GraphQL is not a replacement for HTTP. It sits on top of HTTP and uses it as a transport layer.
Basic Error Handling in GraphQL
GraphQL responses always return a top-level data key, and optionally an errors key.
Example error response:
{
"data": {
"user": null
},
"errors": [
{
"message": "User not found",
"path": ["user"],
"locations": [{ "line": 2, "column": 3 }]
}
]
}data.userisnullbecause the resolver failed.errorscontains details about what went wrong.
You usually do not use HTTP 4xx / 5xx for validation or application errors inside GraphQL. The HTTP status is often 200, and the error lives in the errors array. Some implementations also use 400 or 500 when the query itself is invalid.
How Resolvers Work (Conceptually)
A resolver is a function that GraphQL calls to produce the value of a field.
For example, with this schema:
type User {
id: ID!
username: String!
}
type Query {
user(id: ID!): User
}Behind the scenes, you might have something like this (pseudo-code):
def resolve_user(root, info, id):
return db.get_user_by_id(id)Each field on each type can have its own resolver. If you do not define a resolver for a field, a default resolver usually returns the corresponding property from the parent object.
Resolvers give you flexibility: you can fetch from a database, call another REST API, read a file, or even compute the value.
Example: Simple GraphQL Server Concept
Here is a small conceptual Python example using a GraphQL library style, to make things concrete. This is not tied to a specific framework, but shows the roles:
# Schema (SDL)
schema_str = """
type User {
id: ID!
username: String!
}
type Query {
user(id: ID!): User
}
"""
# Data source for demo
USERS = {
"1": {"id": "1", "username": "alice"},
"2": {"id": "2", "username": "bob"},
}
# Resolver
def resolve_user(root, info, id):
return USERS.get(id)
# Wiring (pseudo-code)
schema = build_schema(schema_str)
schema.set_resolver("Query", "user", resolve_user)
# HTTP handler (very simplified pseudo-code)
def handle_request(http_request):
body = json.loads(http_request.body)
query = body["query"]
variables = body.get("variables")
result = schema.execute(query, variable_values=variables)
return json_response(result.to_dict())Flow:
- Client sends a GraphQL query to
/graphql. - Server parses the query and matches it to the schema.
- For each field, the corresponding resolver is called.
- Results are combined into the final JSON response.
When to Use GraphQL vs REST
GraphQL can be very helpful when:
- You have complex UIs that need data from many related resources.
- Different clients (web, mobile, internal tools) need different subsets of data.
- You want strong typing and introspection for your API.
REST can be simpler and more natural when:
- Your API is very small and straightforward.
- You benefit heavily from built-in HTTP caching and status codes.
- Your clients are few and have stable data needs.
In real projects, it is common to:
- Use REST for some parts of your system.
- Use GraphQL as an aggregation layer on top of existing REST / microservices.
Summary
- GraphQL is a query language and runtime for APIs that lets clients request exactly the data they need.
- The schema defines types, fields, and entry points:
Query,Mutation, and optionallySubscription. - Clients send queries for reads and mutations for writes, often with variables for dynamic values.
- Responses always include
dataand can includeerrors. - GraphQL typically uses one HTTP endpoint, and the shape of the response is controlled by the query, not by the server endpoint.
This gives you a flexible and powerful way to design APIs, especially for complex applications with many data relationships and multiple client types.
Views: 6
KAHIBARO