GraphQL
Status: 🟩 COMPLETE Last updated: 2026-06-19 Plain-English tagline: An alternative to REST where the client says exactly what fields and shapes of data it wants in a single query, and the server returns precisely that — no more, no less.
In plain English
REST APIs make you ask a separate question for each “thing” you want. To build a blog post page, you might need:
GET /posts/47 → the post
GET /users/123 → the author
GET /posts/47/comments → the comments
GET /users/{each} → each commenter's profile
That’s 4+ round trips, and each endpoint returns whatever the server decided to include — often more data than you need (“over-fetching”) and sometimes less (“under-fetching,” forcing yet another call).
GraphQL is a different approach, designed at Facebook in 2012 and open-sourced in 2015. Instead of many endpoints, you have ONE endpoint (usually /graphql). Instead of fixed responses, the client sends a query describing exactly the fields and nested shape it wants. The server returns precisely that shape — never more, never less.
query {
post(id: "47") {
title
body
author {
name
}
comments {
body
author {
name
}
}
}
}Response:
{
"data": {
"post": {
"title": "Hello",
"body": "...",
"author": { "name": "George" },
"comments": [
{ "body": "Nice!", "author": { "name": "Alice" } },
{ "body": "+1", "author": { "name": "Bob" } }
]
}
}
}One request. Exactly the data you need. No extras. The server traversed the relationships behind the scenes.
That’s the elevator pitch. It’s powerful, it’s elegant, and it’s surrounded by tooling that makes typed end-to-end queries possible. It also has real operational complexity — discussed below.
Why it matters
Two reasons GraphQL is worth knowing even if you don’t pick it for your own API:
-
Some services only expose GraphQL. Shopify’s Storefront API, GitHub’s v4 API, Hasura, AWS AppSync, Linear’s API — all GraphQL-first. If you integrate with these, you’ll write queries.
-
For specific shapes of app, it’s genuinely the right tool. Heavily relational data (social networks, project management tools), mobile apps minimizing payload size, multi-team frontends consuming a shared backend — these are GraphQL sweet spots.
For the kinds of projects George builds, the modern stack (Next.js + Supabase + REST + server actions) usually doesn’t need GraphQL. But knowing GraphQL means you can spot when it WOULD be the right tool — and integrate with services that publish a GraphQL API.
The core building blocks
GraphQL has a small vocabulary you’ll see repeatedly:
| Term | What it means |
|---|---|
| Schema | The strongly-typed contract: what types exist, what fields they have, what queries/mutations are allowed. Written in SDL (Schema Definition Language). |
| Type | A named object — User, Post, Comment. Has fields, each with its own type. |
| Query | A read-only operation. “Get me data with this shape.” |
| Mutation | A write operation. “Create this. Update this. Delete this.” Same syntax as a query, just declared as a mutation. |
| Subscription | A streaming read. The server pushes updates over a websocket when data changes. |
| Resolver | The server-side function that fetches data for a specific field. Behind every field in the schema is a resolver. |
| Introspection | The schema itself is queryable — __schema, __type. This lets tools auto-generate types and explore the API. |
A concrete example: a small GraphQL schema
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
body: String!
author: User!
comments: [Comment!]!
createdAt: String!
}
type Comment {
id: ID!
body: String!
author: User!
post: Post!
}
type Query {
post(id: ID!): Post
user(id: ID!): User
recentPosts(limit: Int = 20): [Post!]!
}
type Mutation {
createPost(title: String!, body: String!): Post!
deletePost(id: ID!): Boolean!
}Notes:
!after a type means “non-null” — this field is guaranteed to exist.[Post!]!means “a non-null list of non-null Posts” (the list itself can’t be null, and no item can be null).Int = 20is a default value for that parameter.
A client can then write:
query {
recentPosts(limit: 5) {
title
author { name }
}
}The server’s resolver for recentPosts runs first, returns 5 posts. The resolver for each Post.author runs next, returning the User. Each leaf field’s value lands in the response. The client gets exactly what it asked for.
Mutations look the same but write
mutation {
createPost(title: "Hello", body: "World") {
id
createdAt
}
}This creates a post AND returns the new id + creation time in one round trip. The convention: a mutation returns the new state of the resource, so the client can update its local cache without a separate read.
How GraphQL hits the wire
GraphQL is just HTTP POST under the hood. A request looks like:
POST /graphql HTTP/1.1
Host: api.example.com
Authorization: Bearer abc123
Content-Type: application/json
{
"query": "query GetPost($id: ID!) { post(id: $id) { title body } }",
"variables": { "id": "47" },
"operationName": "GetPost"
}The body is JSON: the query string + any variables. Variables are passed separately (not interpolated into the query) to prevent injection attacks.
The response is JSON. If there are errors, they appear in a sibling errors array:
{
"data": { "post": null },
"errors": [
{ "message": "Post not found", "path": ["post"], "extensions": { "code": "NOT_FOUND" } }
]
}A key oddity: GraphQL almost always returns HTTP 200 OK even on errors. The error info lives in the errors array, not the HTTP status. This is part of “you might have data AND errors in the same response” — for instance, when fetching multiple things and only one fails.
The strongly-typed end-to-end story
The killer feature of GraphQL in 2026 is type safety from server to client. The flow:
- Server defines its schema in SDL (or generates it from code).
- Tools (codegen) read the schema + your client queries and generate TypeScript types matching the exact shape of each query’s response.
- Your IDE autocompletes fields. Your typechecker catches typos in queries. Refactoring is safe.
In practice, with graphql-codegen:
import { useQuery } from "@apollo/client";
import { GET_POST_QUERY, GetPostQueryResult } from "./generated";
const { data, loading } = useQuery<GetPostQueryResult>(GET_POST_QUERY, {
variables: { id: "47" },
});
// data.post.title is typed!
// data.post.author.name is typed!
// data.post.notARealField is a compile errorThis level of cross-stack typing is genuinely valuable for big teams or rapidly changing schemas. REST gets close with OpenAPI codegen but with more friction.
Caching is harder
REST has a key cache advantage: the URL identifies a resource. Hit the same URL → get the same cached response. HTTP caches and CDNs work effortlessly.
GraphQL: every query is a POST with a different body. Most HTTP caches won’t cache POSTs. You can’t just put a CDN in front.
Solutions:
- Persisted queries — assign each unique query a hash. Send the hash + variables instead of the full query. Cache by hash + variables. Apollo, Relay, and others support this.
- Client-side normalized caches — Apollo Client, Relay, urql build smart caches in the browser, keyed by the type + id of every object. A
Userfetched in one query is reused for another. - Automatic Persisted Queries (APQ) — the client sends a hash; if the server doesn’t recognize it, it asks for the full query, which it then caches by hash.
- Server-side caching — DataLoader-style batching collapses repeated DB lookups within a request; Redis can cache resolver results.
These work, but they require deliberate engineering. With REST, default HTTP caching just works.
The N+1 problem and DataLoader
A specific GraphQL hazard: a poorly-written resolver can fetch the same data hundreds of times.
query {
recentPosts { # 1 query to fetch posts
title
author { name } # N queries — one per post!
}
}Without help, the resolver for Post.author runs once per post. 100 posts = 101 database queries. This is the classic “N+1 problem.”
DataLoader is the standard solution. It batches lookups within a single request: “we asked for users with ids [1, 2, 3, 1, 2]” → DataLoader dedupes and issues SELECT * FROM users WHERE id IN (1, 2, 3) once. Resolvers ask DataLoader instead of the DB directly.
Every serious GraphQL server uses DataLoader (or similar). If you write a resolver that fetches data per-item naively, you’ve created a performance disaster.
Subscriptions — realtime over GraphQL
A third operation type, subscription, lets clients receive updates pushed by the server:
subscription {
newCommentsOnPost(postId: "47") {
body
author { name }
}
}The client opens a persistent connection (websocket or SSE). When the server has a matching event, it pushes the data.
Useful for chat, presence, live dashboards, collaborative editing. More complex to operate than queries: connection management, scaling, auth across long-lived connections.
For most webapps, dedicated realtime tools (Supabase Realtime, Pusher, Ably) are simpler than GraphQL Subscriptions unless you’re already deep in the GraphQL ecosystem.
Tools and frameworks
The GraphQL ecosystem is mature:
| Tool | Purpose |
|---|---|
| Apollo | Most popular GraphQL toolkit — server, client, codegen, federation |
| Relay | Facebook’s GraphQL client, advanced pagination/caching, opinionated |
| urql | Lighter, more flexible client alternative |
| graphql-codegen | Generate TypeScript types from schema + queries |
| Hasura | Auto-generate a GraphQL API from a Postgres database |
| PostGraphile | Similar — Postgres → GraphQL automatically |
| GraphQL Yoga | Modern, simple GraphQL server |
| Mercurius | Fast GraphQL server for Fastify |
| Pothos | TypeScript-first schema builder, very type-safe |
| Apollo Federation | Compose multiple GraphQL services into one super-graph |
For a hand-built GraphQL API in Node/TypeScript in 2026, Pothos + GraphQL Yoga + DataLoader + graphql-codegen is the modern stack. For “I just want GraphQL over Postgres,” Hasura or PostGraphile are auto-generated.
GraphQL vs REST — the honest comparison
| Aspect | GraphQL | REST |
|---|---|---|
| Shape of response | Defined by client | Defined by server |
| Over-fetching | Eliminated | Common |
| Round trips for related data | One (often) | Many (often) |
| HTTP caching | Hard | Easy |
| Tooling for types | Excellent (codegen, introspection) | Good (OpenAPI codegen) |
| Learning curve | Steeper | Gentler |
| Operational complexity | Higher (N+1, complexity attacks, persisted queries) | Lower |
| File uploads | Awkward (multipart spec) | Native |
| Common use cases | Highly relational data, mobile, multi-team frontends | Resource CRUD, public APIs, simple webapps |
GraphQL is not a “better REST.” It’s a different shape that fits specific use cases. For the kinds of small-to-medium webapps George builds, REST + typed server actions is simpler and equally effective. For Shopify’s storefront, GitHub’s developer ecosystem, or Linear’s API, GraphQL is genuinely the right choice.
Common gotchas
-
Always-200 HTTP responses. GraphQL signals errors in the body, not the status code. Don’t write client code that only checks
response.ok. Always look aterrors. -
N+1 queries are easy to write and hard to spot. Every resolver that touches the database needs to be DataLoader-aware. Profile early.
-
Schema complexity attacks. An attacker can write
{ user { posts { author { posts { author { posts ... } } } } } }to make the server do exponential work. Use query depth limiting and complexity analysis (graphql-cost-analysis, graphql-query-complexity) on production endpoints. -
Introspection on production is a double-edged sword. Public introspection lets attackers map your entire API. Some teams disable it in production; others leave it on to encourage exploration. Pick deliberately.
-
Mutations should return useful data. A mutation that returns
Booleanforces the client to refetch. A mutation that returns the updated resource lets the client update its cache. Always return the resource (or its new state). -
Updating cache after mutations is error-prone. Apollo’s normalized cache helps, but list-shaped data (e.g. adding a new post) often needs explicit cache updates. Read the cache update docs of your chosen client.
-
GraphQL is one endpoint — all auth + rate limiting must happen there. REST naturally spreads requests across many URLs; GraphQL concentrates them. Per-resolver auth checks; per-user query complexity budgets.
-
Subscriptions need a websocket layer. Serverless functions don’t naturally hold connections open. Subscriptions usually require a longer-lived server or a managed service (Pusher, AppSync).
-
File uploads are awkward. GraphQL’s native types are strings, ints, etc. Uploads use multipart-form extensions (
graphql-upload), pre-signed URLs (upload to S3 first, send the URL via GraphQL), or REST endpoints next to the GraphQL one. -
Persisted queries are operational overhead. Caching benefits require the client to know its query hashes ahead of time. Most teams adopt this only after they hit performance walls.
-
Generated TypeScript types can lie. If the server returns null for a field declared non-null, your code crashes at runtime despite types saying it’s safe. Trust the schema; verify at boundaries.
-
GraphQL doesn’t enforce CORS or rate limits. The middleware around GraphQL does. Make sure they’re applied.
-
Stale codegen vs live schema. If the server schema changes and the client codegen hasn’t run, you get build-time type mismatches. Run codegen in CI.
-
The N+1 problem applies to mutations too. A mutation that iterates and calls another mutation per item can detonate.
-
Schema federation has a learning curve. Federating multiple GraphQL services into one super-graph (Apollo Federation, GraphQL Mesh) is powerful but complex. Don’t reach for it until you’ve outgrown a monolithic schema.
-
Error messages can leak internals. A naive resolver that returns
error.messagefrom a database driver can reveal table names, query structure, library versions. Sanitize before returning errors. -
Pagination is a footgun. Naive
{ posts { ... } }returns ALL posts. Forces every caller to over-fetch. Make list fields require explicit pagination (first,afterfor cursor-based) from day one. -
Schema-first vs code-first. Two ways to define schema: write SDL files (schema-first) or generate SDL from code (code-first). Both work; code-first (Pothos, Nexus) wins for type safety; schema-first wins for cross-language teams. Pick early.
-
The community has fragmented. Apollo dominates, but each year a new “Apollo killer” emerges. Stick with the popular tools unless you have a specific need.
-
Performance characteristics depend on resolvers, not GraphQL itself. A slow GraphQL API is a slow database with GraphQL on top. Profile resolvers, optimize queries, batch with DataLoader.
-
Don’t expose your database directly as GraphQL without thought. Hasura and PostGraphile are great, but exposing every table and every relationship gives clients enormous power. Layer auth, complexity limits, and view-level controls.
When to choose GraphQL
- Heavily relational data that frontend teams need to traverse flexibly
- Multiple frontend clients (web, iOS, Android) consuming the same backend with different needs
- Rapid iteration where adding/removing fields shouldn’t require backend API versions
- Mobile apps where round-trip minimization and payload size really matter
- Integrating with a service that publishes GraphQL (Shopify, GitHub, Linear)
When NOT to choose GraphQL
- Simple CRUD with a few obvious endpoints
- Public API where caching is the priority (REST + CDN is much simpler)
- Solo project that doesn’t need its API to be language-agnostic (typed server actions or tRPC are simpler and equally type-safe)
- You’re early-stage and just need to ship
See also
- APIs — the big picture 🟩
- REST APIs 🟩 — the dominant alternative
- What is a backend? đźź©
- Node.js 🟩 🟦 — the runtime most GraphQL servers run on
- Server actions (Next.js) 🟩 — typed first-party RPC alternative
- Supabase 🟩 🟦 — exposes Postgres as REST AND can be wrapped by Hasura/PostGraphile for GraphQL
- Joins and relationships 🟩 — what resolvers traverse
- JWT 🟩 — common auth for GraphQL
- Glossary: GraphQL, Schema, Resolver
Sources
- GraphQL.org — official site — canonical reference
- GraphQL spec
- Apollo GraphQL docs — the dominant client+server toolkit
- How to GraphQL — tutorial — broad introduction
- DataLoader — the canonical batching/caching solution
- Production Ready GraphQL (book) — operating-at-scale concerns
- GitHub GraphQL API v4 — the most-studied public GraphQL API