KAHIBARO
Discord Login Register

27.3. GraphQL

Why GraphQL Exists

REST works well for many APIs, but it has some common pain points:

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:

  1. A single HTTP endpoint
    Often /graphql, usually via POST.
  2. A schema
    The schema describes types, fields, and operations that clients can perform.
  3. 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:

json
{
  "query": "{ hello }"
}

The server parses the query string, checks it against the schema, calls resolvers, then returns JSON like:

json
{
  "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:

graphql
type User {
  id: ID!
  username: String!
  email: String!
  age: Int
}
type Query {
  user(id: ID!): User
  users: [User!]!
}

Important parts:

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:

Queries: Getting Data

A GraphQL query looks like the JSON shape you want to receive, but in a GraphQL-specific syntax.

Example schema:

graphql
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:

graphql
query {
  user(id: "1") {
    id
    username
    posts {
      id
      title
    }
  }
}

Example response:

json
{
  "data": {
    "user": {
      "id": "1",
      "username": "alice",
      "posts": [
        { "id": "10", "title": "First post" },
        { "id": "11", "title": "Second post" }
      ]
    }
  }
}

Notice:

If you only need the username, you ask only for that:

graphql
query {
  user(id: "1") {
    username
  }
}

Response:

json
{
  "data": {
    "user": {
      "username": "alice"
    }
  }
}

Mutations: Writing Data

Reads use Query. Writes use Mutation.

Example schema additions:

graphql
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:

graphql
mutation {
  createPost(
    input: {
      title: "My first GraphQL post"
      body: "This is cool"
      authorId: "1"
    }
  ) {
    id
    title
    author {
      username
    }
  }
}

Response:

json
{
  "data": {
    "createPost": {
      "id": "42",
      "title": "My first GraphQL post",
      "author": {
        "username": "alice"
      }
    }
  }
}

Things to notice:

Variables: Reusing Queries and Mutations

Hardcoding IDs and values in queries is not practical. GraphQL supports variables.

Example mutation with variables:

graphql
mutation CreatePost($input: CreatePostInput!) {
  createPost(input: $input) {
    id
    title
  }
}

You send it with a JSON body like:

json
{
  "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:

graphql
query GetUser($id: ID!) {
  user(id: $id) {
    id
    username
  }
}

Fields, Arguments, and Aliases

Field arguments

Fields can take arguments, similar to function parameters.

Schema:

graphql
type Query {
  posts(limit: Int, offset: Int): [Post!]!
}

Query:

graphql
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:

graphql
type Query {
  user(id: ID!): User
}

Query with aliases:

graphql
query {
  firstUser: user(id: "1") {
    username
  }
  secondUser: user(id: "2") {
    username
  }
}

Response:

json
{
  "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:

graphql
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

AspectRESTGraphQL
Endpoint structureMany endpoints, one per resource or actionUsually a single /graphql endpoint
Data shapeFixed per endpointDefined by the client in each query
Overfetching / underfetchingCommon issueGreatly reduced, client asks for exactly what it needs
HTTP methodsUses verbs: GET, POST, PUT, PATCH, DELETEUsually POST (sometimes GET for simple queries)
VersioningOften /v1, /v2 APIsUsually evolve schema without versioned URLs
DocumentationSwagger / OpenAPI, manual docsSchema is introspectable and self-documenting
CachingHTTP status codes and headers, URL-basedTrickier, often needs custom caching or client-side tools
Error handlingVia HTTP status codesMixed 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:

json
{
  "data": {
    "user": null
  },
  "errors": [
    {
      "message": "User not found",
      "path": ["user"],
      "locations": [{ "line": 2, "column": 3 }]
    }
  ]
}

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:

graphql
type User {
  id: ID!
  username: String!
}
type Query {
  user(id: ID!): User
}

Behind the scenes, you might have something like this (pseudo-code):

python
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:

python
# 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:

  1. Client sends a GraphQL query to /graphql.
  2. Server parses the query and matches it to the schema.
  3. For each field, the corresponding resolver is called.
  4. Results are combined into the final JSON response.

When to Use GraphQL vs REST

GraphQL can be very helpful when:

REST can be simpler and more natural when:

In real projects, it is common to:

Summary

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

Comments

Please login to add a comment.

Don't have an account? Register now!