Toolmingo
Guides8 min read

What Is GraphQL? Complete Guide with Examples

Learn what GraphQL is, how queries and mutations work, how it compares to REST, and how to build and consume a GraphQL API in JavaScript, Python, Go, and PHP.

GraphQL is a query language for APIs — and a runtime for executing those queries — invented by Facebook in 2012 and open-sourced in 2015. Instead of hitting a fixed endpoint that returns a fixed response, you send a typed query describing exactly what you need and get back exactly that — nothing more, nothing less.


REST vs GraphQL at a Glance

Feature REST GraphQL
Endpoint One per resource (/users, /posts) Single endpoint (/graphql)
Response shape Fixed by server Defined by client
Over-fetching Common (extra fields) Impossible (you ask only for what you need)
Under-fetching Common (multiple round trips) Impossible (nest related data in one query)
Versioning URL versioning (/v2/) Schema evolution (add fields, deprecate old)
Type system Optional (OpenAPI helps) Built-in, enforced at runtime
Real-time Polling / WebSocket DIY First-class subscription type
Tooling Swagger/OpenAPI GraphiQL, Apollo Studio, Playground
Learning curve Low Medium

Choose REST when: CRUD APIs, public APIs, caching matters a lot, team is unfamiliar with GraphQL.

Choose GraphQL when: multiple clients with different data needs (web + mobile), complex object graphs, product evolves quickly, developer experience is a priority.


Core Concepts

1. Schema — the contract

Every GraphQL API starts with a schema written in SDL (Schema Definition Language):

type User {
  id: ID!
  name: String!
  email: String!
  posts: [Post!]!
}

type Post {
  id: ID!
  title: String!
  body: String
  author: User!
  createdAt: String!
}

type Query {
  user(id: ID!): User
  users: [User!]!
  post(id: ID!): Post
}

type Mutation {
  createPost(title: String!, body: String): Post!
  updatePost(id: ID!, title: String): Post!
  deletePost(id: ID!): Boolean!
}

type Subscription {
  postCreated: Post!
}

The ! means non-null (guaranteed to be present). Types are the single source of truth — documentation, validation, and autocomplete all come from the schema.

2. Scalar types

Type Description
String UTF-8 text
Int 32-bit integer
Float Double-precision float
Boolean true / false
ID Unique identifier (serialised as String)
Custom scalars Date, DateTime, JSON, URL — defined per API

3. Query — reading data

# Ask for exactly what you need
query GetUser {
  user(id: "42") {
    name
    email
    posts {
      title
      createdAt
    }
  }
}

Response:

{
  "data": {
    "user": {
      "name": "Ana Perović",
      "email": "ana@example.com",
      "posts": [
        { "title": "Hello GraphQL", "createdAt": "2026-07-01" }
      ]
    }
  }
}

Notice: id was not requested so it was not returned.

4. Variables — dynamic queries

Hardcoding "42" in the query is bad practice. Use variables:

query GetUser($userId: ID!) {
  user(id: $userId) {
    name
    email
  }
}
{ "userId": "42" }

Variables prevent injection attacks and enable query caching on the server.

5. Mutation — writing data

mutation CreatePost($title: String!, $body: String) {
  createPost(title: $title, body: $body) {
    id
    title
    createdAt
  }
}

Mutations are structurally identical to queries but signal intent to change data. Best practice: always return the modified object so the client can update its cache.

6. Subscription — real-time data

subscription OnPostCreated {
  postCreated {
    id
    title
    author {
      name
    }
  }
}

Subscriptions use WebSockets (or SSE). The server pushes data to connected clients when an event fires. Not every GraphQL server supports subscriptions — check your library.


Quick Reference

Operation Keyword When to use
Read data query GET equivalent — fetching anything
Write data mutation POST/PUT/DELETE equivalent
Real-time subscription Live feeds, notifications, collaborative apps
Inline fragment ... on TypeName Polymorphic fields (unions, interfaces)
Named fragment fragment F on Type Reusable field selections
Directive @include(if: $bool) Conditional fields in a query
Introspection __schema, __type Schema discovery, tooling

Consuming a GraphQL API

JavaScript (fetch)

async function getUser(userId) {
  const response = await fetch("https://api.example.com/graphql", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Authorization": `Bearer ${token}`,
    },
    body: JSON.stringify({
      query: `
        query GetUser($id: ID!) {
          user(id: $id) {
            name
            email
            posts { title }
          }
        }
      `,
      variables: { id: userId },
    }),
  });

  const { data, errors } = await response.json();
  if (errors) throw new Error(errors[0].message);
  return data.user;
}

GraphQL always uses POST and always returns HTTP 200 — even for errors. Check data.errors, not the status code.

Python (requests)

import requests

def get_user(user_id: str, token: str) -> dict:
    query = """
    query GetUser($id: ID!) {
      user(id: $id) {
        name
        email
        posts { title }
      }
    }
    """
    response = requests.post(
        "https://api.example.com/graphql",
        json={"query": query, "variables": {"id": user_id}},
        headers={"Authorization": f"Bearer {token}"},
        timeout=10,
    )
    response.raise_for_status()
    payload = response.json()
    if "errors" in payload:
        raise ValueError(payload["errors"][0]["message"])
    return payload["data"]["user"]

Go (net/http)

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
)

type GraphQLRequest struct {
    Query     string         `json:"query"`
    Variables map[string]any `json:"variables"`
}

type GraphQLResponse struct {
    Data   map[string]json.RawMessage `json:"data"`
    Errors []struct{ Message string } `json:"errors"`
}

func getUser(id, token string) (json.RawMessage, error) {
    body, _ := json.Marshal(GraphQLRequest{
        Query: `query GetUser($id: ID!) { user(id: $id) { name email } }`,
        Variables: map[string]any{"id": id},
    })

    req, _ := http.NewRequest("POST", "https://api.example.com/graphql", bytes.NewReader(body))
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("Authorization", "Bearer "+token)

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    var gqlResp GraphQLResponse
    json.NewDecoder(resp.Body).Decode(&gqlResp)
    if len(gqlResp.Errors) > 0 {
        return nil, fmt.Errorf(gqlResp.Errors[0].Message)
    }
    return gqlResp.Data["user"], nil
}

PHP (cURL)

function graphqlRequest(string $query, array $variables, string $token): array {
    $ch = curl_init('https://api.example.com/graphql');
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => json_encode(['query' => $query, 'variables' => $variables]),
        CURLOPT_HTTPHEADER     => [
            'Content-Type: application/json',
            "Authorization: Bearer $token",
        ],
        CURLOPT_TIMEOUT        => 10,
    ]);
    $body = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($body === false || $status !== 200) {
        throw new RuntimeException("HTTP error $status");
    }
    $payload = json_decode($body, true);
    if (!empty($payload['errors'])) {
        throw new RuntimeException($payload['errors'][0]['message']);
    }
    return $payload['data'];
}

$data = graphqlRequest(
    'query GetUser($id: ID!) { user(id: $id) { name email } }',
    ['id' => '42'],
    $token
);

Building a GraphQL Server (Node.js example)

The most popular Node.js options:

Library Use case
Apollo Server Production-grade, integrates with Express/Fastify
graphql-yoga Lightweight, modern, spec-compliant
Pothos Code-first schema (TypeScript inference)
Mercurius Fastify plugin, high performance

Minimal Apollo Server example:

import { ApolloServer } from "@apollo/server";
import { startStandaloneServer } from "@apollo/server/standalone";

const typeDefs = `#graphql
  type User {
    id: ID!
    name: String!
    email: String!
  }
  type Query {
    user(id: ID!): User
    users: [User!]!
  }
`;

const users = [
  { id: "1", name: "Ana", email: "ana@example.com" },
  { id: "2", name: "Marko", email: "marko@example.com" },
];

const resolvers = {
  Query: {
    user: (_parent, { id }) => users.find((u) => u.id === id),
    users: () => users,
  },
};

const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, { listen: { port: 4000 } });
console.log(`GraphQL server ready at ${url}`);

Run it, then open http://localhost:4000 to get GraphiQL — an in-browser IDE for exploring your API.


N+1 Problem and DataLoader

The most common GraphQL performance trap: resolving a list of 100 users and then making 100 separate DB queries to fetch each user's posts.

users query → 1 DB query
  ↳ posts for user 1 → 1 DB query
  ↳ posts for user 2 → 1 DB query
  ... × 100 = 101 queries total  ❌

Fix: DataLoader — batches and caches per-request:

import DataLoader from "dataloader";

// Created once per request
const postsByUserLoader = new DataLoader(async (userIds) => {
  const posts = await db.post.findMany({
    where: { authorId: { in: userIds } },
  });
  // Return arrays in the same order as userIds
  return userIds.map((id) => posts.filter((p) => p.authorId === id));
});

const resolvers = {
  User: {
    posts: (user) => postsByUserLoader.load(user.id),
  },
};
// 100 users → 1 batched DB query ✅

6 Common Mistakes

Mistake Fix
Checking HTTP status for errors Always parse response.errors — status is always 200
Returning everything in one massive type Use pagination (Relay cursor or offset), limit fields
No depth/complexity limits Set maxDepth and maxComplexity to prevent DoS
Exposing internal errors Return user-safe messages; log full error server-side
Skipping DataLoader Every list resolver needs batching to avoid N+1
Mutations without return values Always return the affected object for client cache updates

FAQ

Is GraphQL better than REST? Neither is universally better. GraphQL shines when clients have diverse data needs (e.g. web needs 5 fields, mobile needs 3 different ones). REST wins for simple CRUD APIs, public integrations, and when HTTP caching is critical.

Does GraphQL replace REST? Not necessarily. Many teams run both — REST for public/simple APIs, GraphQL for internal product APIs. File uploads, webhooks, and streaming often stay on REST.

How do I handle authentication? Same as REST: send a Bearer token in the Authorization header. Check it in a middleware before the resolver runs. Never handle auth inside individual resolvers.

Can GraphQL be cached? GET-based queries (with query in the URL) can be HTTP-cached. POST requests cannot. Apollo's @cacheControl directive and CDN-level Persisted Queries are the production solutions.

What is schema stitching / federation? When you split a large GraphQL API across multiple services. Apollo Federation lets each service own part of the schema and composes them into one unified API at the gateway.

How do I explore a GraphQL API? Use the built-in GraphiQL or Apollo Sandbox playground — both provide schema explorer, autocomplete, and query execution. The __schema introspection query returns the full type system.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools