GraphQL Cheat Sheet
This GraphQL cheat sheet covers 29 sections: queries, variables, fragments and directives, mutations and subscriptions, schema design and nullability, errors and HTTP status codes, cursor pagination, resolvers and DataLoader, caching, security limits, federation and tooling. Analyze and format queries, lint a schema and turn a query into curl without leaving the page.
{ "data": null }
Nothing matches that search. Try a shorter keyword such as fragment, mutation or cursor. Several words are combined, so every one of them has to match. Keys: / focuses the search, Esc clears it, t switches the theme.
Toolbox & First Query
format and analyze queries, lint schemas, turn queries into HTTP requests, and make a first call
Builders: Type and Resolvers, Relay Connection, Client Code, JSON to SDL
A First Query
Ask for exactly the fields you need
{
film(id: "ZmlsbXM6MQ==") {
title
director
releaseDate
}
}
The same request with curl
curl -s https://swapi-graphql.netlify.app/graphql -H "Content-Type: application/json" -d '{"query": "{ film(id: \"ZmlsbXM6MQ==\") { title } }"}'
How GraphQL Works
the schema, operations and the one endpoint
The Core Ideas
Schema, operations, resolvers
| Piece | Role |
|---|---|
| schema | the typed contract: every type, field and argument the API offers |
| query | read data; fields at the same level may run in parallel |
| mutation | change data; top-level fields run one after another |
| subscription | receive a stream of results when events happen |
| resolver | the server function that produces a field's value |
| introspection | the schema can be queried itself, which powers tooling |
A Minimal Schema
Types and the root Query
type Query {
film(id: ID!): Film
}
type Film {
id: ID!
title: String!
director: String
characters: [Person!]!
}
Queries
fields, arguments, aliases, nested selections and __typename
Fields and Arguments
Nested selections follow relationships
query {
film(filmID: 1) {
title
planetConnection(first: 2) {
planets { name }
}
characterConnection(first: 3) {
characters { name }
}
}
}
Aliases
Ask for the same field twice
query {
first: film(filmID: 1) { title }
latest: film(filmID: 6) { title }
}
Operation Names and __typename
Name every operation
query EmpireCast {
film(filmID: 2) {
__typename
title
characterConnection(first: 2) {
characters { __typename name }
}
}
}
Several Root Fields
One request, independent reads
{
product(by: { sku: "LAMP-01" }) { name price }
orders(first: 1, status: PAID) { totalCount }
}
Variables
typed variables, defaults and input objects
Variables
Never build queries by string concatenation
query Film($id: ID!, $cast: Int = 2) {
film(id: $id) {
title
characterConnection(first: $cast) {
characters { name }
}
}
}
# variables: {"id": "ZmlsbXM6Mw=="}
The HTTP body with variables
{
"query": "query Film($id: ID!) { film(id: $id) { title } }",
"variables": { "id": "ZmlsbXM6Mw==" },
"operationName": "Film"
}
Request Errors
A missing required variable
query Order($id: ID!) {
node(id: $id) { id }
}
A typo in a field name
{
product(by: { sku: "LAMP-01" }) { nmae }
}
Lists, Enums and Input Objects
A list variable
query Products($skus: [String!]!) {
products(skus: $skus) { sku price }
}
# variables: {"skus": ["LAMP-01", "BULB-07"]}
Enum values are strings in JSON
query Shipped($status: OrderStatus) {
orders(first: 5, status: $status) {
totalCount
edges { node { id } }
}
}
# variables: {"status": "SHIPPED"}
Fragments
named fragments, inline fragments, interfaces and unions
Named Fragments
Reuse a selection
query TwoFilms {
a: film(filmID: 4) { ...FilmCard }
b: film(filmID: 5) { ...FilmCard }
}
fragment FilmCard on Film {
title
releaseDate
}
Unions and Interfaces
Pick fields by concrete type
query {
search(text: "ai") {
__typename
... on Product { sku price }
... on Customer { name }
}
}
Fetch any object by global ID
query {
node(id: "ord_8812") {
id
... on Order { status total customer { name } }
}
}
Fragments on Interfaces
Spread an interface fragment into a union
query {
search(text: "ai") { ...Ref }
}
fragment Ref on Node {
__typename
id
}
Directives
@include, @skip, @deprecated, @specifiedBy, @oneOf, @defer and @stream
Conditional Fields
@include and @skip
query Film($withCrawl: Boolean!) {
film(filmID: 1) {
title
openingCrawl @include(if: $withCrawl)
}
}
# variables: {"withCrawl": false}
Schema Directives
Built into the specification
type Product {
stock: Int @deprecated(reason: "Use stockLevel.")
stockLevel: Int
}
scalar DateTime @specifiedBy(url: "https://www.rfc-editor.org/rfc/rfc3339")
input ProductBy @oneOf {
id: ID
sku: String
}
@oneOf: exactly one field
{
product(by: { id: "prd_1", sku: "LAMP-01" }) { name }
}
Incremental Delivery
@defer sends slow parts later
import { buildSchema, parse, experimentalExecuteIncrementally } from 'graphql';
const schema = buildSchema(`
directive @defer(if: Boolean! = true, label: String) on FRAGMENT_SPREAD | INLINE_FRAGMENT
type Query { product: Product }
type Product { name: String reviews: [String] }
`);
const rootValue = {
product: {
name: 'Desk lamp',
reviews: async () => { await new Promise((r) => setTimeout(r, 50)); return ['Bright', 'Sturdy']; },
},
};
const document = parse(`{ product { name ... @defer(label: "slow") { reviews } } }`);
const result = await experimentalExecuteIncrementally({ schema, document, rootValue });
console.log(JSON.stringify(result.initialResult));
for await (const next of result.subsequentResults) console.log(JSON.stringify(next));
Custom Directives
A schema directive that changes a resolver
import { createSchema } from 'graphql-yoga';
import { mapSchema, getDirective, MapperKind } from '@graphql-tools/utils';
import { defaultFieldResolver, graphql } from 'graphql';
const typeDefs = `
directive @upper on FIELD_DEFINITION
type Query { greeting: String @upper, plain: String }
`;
const base = createSchema({ typeDefs, resolvers: { Query: { greeting: () => 'hello', plain: () => 'hello' } } });
const schema = mapSchema(base, {
[MapperKind.OBJECT_FIELD]: (field) => {
if (!getDirective(base, field, 'upper')?.[0]) return field;
const resolve = field.resolve ?? defaultFieldResolver;
return { ...field, resolve: async (...args) => String(await resolve(...args)).toUpperCase() };
},
});
console.log(JSON.stringify(await graphql({ schema, source: '{ greeting plain }' })));
Mutations
input types, payloads and serial execution
A Mutation
Input object in, payload out
mutation CreateOrder($input: CreateOrderInput!) {
createOrder(input: $input) {
order { id status total }
userErrors { field message code }
}
}
# variables: {"input": {"customerId": "cus_42", "items": [{"sku": "LAMP-01"}, {"sku": "BULB-07", "qty": 3}]}}
Business errors as data
mutation {
createOrder(input: {
customerId: "cus_42"
items: [{ sku: "CHAIR-3", qty: 1 }, { sku: "DESK-9" }]
}) {
order { id }
userErrors { field message code }
}
}
The schema behind it
type Mutation {
createOrder(input: CreateOrderInput!): CreateOrderPayload!
}
input CreateOrderInput {
customerId: ID!
items: [OrderItemInput!]!
note: String
}
input OrderItemInput {
sku: String!
qty: Int! = 1
}
type CreateOrderPayload {
order: Order
userErrors: [UserError!]!
}
type UserError {
field: [String!]
message: String!
code: String!
}
Execution Order
Top-level mutation fields run in sequence
mutation {
first: addToCart(sku: "LAMP-01") { itemCount }
second: addToCart(sku: "BULB-07") { itemCount }
}
Optimistic Updates
Update the UI before the server answers
const [setStatus] = useMutation(SET_STATUS);
setStatus({
variables: { id: 'ord_8813', status: 'PAID' },
optimisticResponse: {
setOrderStatus: { __typename: 'Order', id: 'ord_8813', status: 'PAID' },
},
});
Subscriptions
real-time results over WebSockets or SSE
A Subscription
Results pushed when events happen
subscription OnOrderStatus {
orderStatusChanged(id: "ord_8813") { id status }
}
# accept: text/event-stream
The resolver is an async generator
const resolvers = {
Subscription: {
orderStatusChanged: {
subscribe: async function* (_, { id }) {
for await (const order of orderEvents(id)) {
yield { orderStatusChanged: order };
}
},
},
},
};
When to use subscriptions
| Need | Choose |
|---|---|
| small, frequent updates the user waits for: chat, live status | subscription |
| data that changes rarely | polling or refetch on focus |
| large result sets that change | subscription that sends IDs, then a query |
| notifications to other systems | webhooks, not GraphQL subscriptions |
Schema & SDL
type definitions, the shop schema used on this page, descriptions and extensions
Schema Definition Language
The shop schema behind the order examples
"""Any object that can be refetched by its global ID."""
interface Node { id: ID! }
enum OrderStatus { PENDING PAID SHIPPED }
type Product implements Node {
id: ID!
sku: String!
name: String!
price: Float!
stock: Int @deprecated(reason: "Use stockLevel.")
stockLevel: Int
}
type Order implements Node {
id: ID!
status: OrderStatus!
total: Float!
createdAt: DateTime!
customer: Customer!
items: [OrderItem!]!
note: String
}
union SearchResult = Product | Customer | Order
type Query {
node(id: ID!): Node
product(by: ProductBy!): Product
products(skus: [String!]!): [Product]!
search(text: String!): [SearchResult!]!
orders(first: Int = 10, after: String, status: OrderStatus): OrderConnection!
me: Customer
price(sku: String!): Float!
}
Schema First or Code First
Two ways to build the same schema
| Approach | You write | Typical libraries |
|---|---|---|
| schema first | SDL files, then resolvers that match them | Apollo Server, GraphQL Yoga, gqlgen, Spring for GraphQL |
| code first | classes or functions; the SDL is generated | Strawberry, Pothos, Nexus, Hot Chocolate, graphql-java with annotations |
Split a schema with extend
type Query {
me: Customer
}
extend type Query {
orders(first: Int = 10, after: String): OrderConnection!
}
extend type Customer {
loyaltyPoints: Int!
}
Types & Scalars
scalars, enums, lists, input types, interfaces, unions and custom scalars
The Type System
Kinds of types
| Kind | Example | Used for |
|---|---|---|
| scalar | Int, Float, String, Boolean, ID | leaf values |
| enum | enum OrderStatus { PENDING PAID } | a fixed set of values |
| object | type Order { ... } | output with fields |
| interface | interface Node { id: ID! } | shared fields that several types implement |
| union | union SearchResult = Product \| Customer | one of several types with no shared fields |
| input | input CreateOrderInput { ... } | arguments; cannot reference output types |
| list | [Order!]! | ordered collections |
Custom Scalars
A DateTime scalar in graphql-js 17
import { GraphQLScalarType, GraphQLSchema, GraphQLObjectType, GraphQLNonNull, Kind, graphql } from 'graphql';
const DateTime = new GraphQLScalarType({
name: 'DateTime',
coerceOutputValue: (d) => new Date(d).toISOString(),
coerceInputValue: (v) => { const d = new Date(v); if (isNaN(d)) throw new TypeError('Invalid DateTime'); return d; },
coerceInputLiteral: (ast) => (ast.kind === Kind.STRING ? new Date(ast.value) : undefined),
});
const schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
fields: {
shipBy: {
type: DateTime,
args: { ordered: { type: new GraphQLNonNull(DateTime) } },
resolve: (_, { ordered }) => new Date(ordered.getTime() + 2 * 864e5),
},
},
}),
});
const source = 'query ($at: DateTime!) { shipBy(ordered: $at) }';
console.log(JSON.stringify(await graphql({ schema, source, variableValues: { at: '2026-10-14T09:00:00Z' } })));
console.log((await graphql({ schema, source, variableValues: { at: 'yesterday' } })).errors[0].message);
Nullability
non-null fields, null propagation and how to choose
Null Propagation
An error in a non-null field nulls its parent
{
product(by: { sku: "LAMP-01" }) { name }
price(sku: "DESK-9")
}
A nullable field keeps the damage local
{
product(by: { sku: "CHAIR-3" }) {
name
stockLevel
}
}
Choosing !
Rules of thumb
| Field | Make it | Why |
|---|---|---|
| id, and fields stored on the same row | non-null | they cannot fail on their own |
| fields resolved from another service | nullable | a failure stays local |
| lists | [Item!]! | an empty list beats null; null items are rarely useful |
| arguments and input fields | non-null when required | the client gets a validation error instead of a runtime one |
| new fields on a public API | nullable first | making a field non-null later is safe; the reverse breaks clients |
Errors
the errors array, partial data, extensions codes and masking
The Error Format
A field error with an extensions code
{
me { name email }
}
Throw a GraphQLError to choose the message
import { GraphQLError } from 'graphql';
const resolvers = {
Query: {
me: (_, __, ctx) => {
if (!ctx.user) {
throw new GraphQLError('You must be signed in.', {
extensions: { code: 'UNAUTHENTICATED' },
});
}
return ctx.user;
},
},
};
Where Each Failure Goes
Errors array or data
| Failure | Put it in | Typical HTTP status today |
|---|---|---|
| syntax or validation error | errors, no data | 400 with graphql-response+json, 200 with application/json |
| missing or invalid variable | errors, no data | 200 on graphql-http and Yoga, 400 on Apollo Server 5 |
| not signed in | errors with code UNAUTHENTICATED | 200, or 401 if the server checks before GraphQL runs |
| bug or upstream timeout in a field | errors with path, partial data | 200 |
| expected business outcome: out of stock, duplicate email | payload field such as userErrors | 200 |
Errors Inside Lists
One bad item, the rest still arrive
{
products(skus: ["LAMP-01", "DESK-9", "BULB-07"]) { sku price }
}
Schema Evolution
safe and breaking changes, deprecation and versionless APIs
Safe and Breaking Changes
What clients notice
| Change | Effect on existing clients |
|---|---|
| add a type, field, enum value or optional argument | safe |
| add a required argument or a non-null input field without a default | breaking |
| remove or rename a field, type or enum value | breaking |
| change a field's type, such as String to Int | breaking |
| make an output field nullable | breaking for typed clients |
| make an output field non-null | safe |
| make an argument or input field non-null | breaking |
| add a value to an enum used in output | safe by the spec, but breaks clients with exhaustive switch statements |
Find breaking changes with graphql-js
import { buildSchema, findBreakingChanges, findDangerousChanges } from 'graphql';
const before = buildSchema(`
type Query { order(id: ID!): Order }
type Order { id: ID! status: Status! total: Float! note: String }
enum Status { PENDING PAID }
`);
const after = buildSchema(`
type Query { order(id: ID!, region: String!): Order }
type Order { id: ID! status: Status! total: Int! }
enum Status { PENDING PAID REFUNDED }
`);
for (const c of findBreakingChanges(before, after)) console.log('breaking ', c.type, c.description);
for (const c of findDangerousChanges(before, after)) console.log('dangerous', c.type, c.description);
Deprecate, Then Remove
The lifecycle of a field
type Product {
stock: Int @deprecated(reason: "Use stockLevel. Removal planned after 2027-01-31.")
stockLevel: Int
}
List the deprecated fields
{
__type(name: "Product") {
fields(includeDeprecated: true) {
name
isDeprecated
deprecationReason
}
}
}
GraphQL over HTTP
POST and GET, content types, status codes and file uploads
Requests
GET for queries
curl -s -G https://swapi-graphql.netlify.app/graphql --data-urlencode 'query={ film(filmID: 1) { title } }'
Mutations over GET are refused
mutation {
createOrder(input: { customerId: "cus_42", items: [{ sku: "LAMP-01" }] }) { order { id } }
}
# method: GET
Status Codes
The Accept header decides 200 or 400
for a in application/json application/graphql-response+json; do
curl -s -o /dev/null -w "$a -> %{http_code}\n" https://swapi-graphql.netlify.app/graphql -H "Content-Type: application/json" -H "Accept: $a" -d '{"query": "{ film(filmID: 1) { titel } }"}'
done
What servers send today and what the 2026 draft recommends
| Situation | graphql-http 1.23 and Yoga 5.24, application/json | same servers, graphql-response+json | working draft, September 2026 |
|---|---|---|---|
| data and no errors | 200 | 200 | 200 |
| data and field errors | 200 | 200 | 294 Partial Success |
| JSON body cannot be parsed | 400 | 400 | 400 |
| GraphQL document cannot be parsed | 200 | 400 | 400 |
| document fails validation | 200 | 400 | 422 |
| variables cannot be coerced | 200 | 200 | 422 |
| mutation sent with GET | 405 | 405 | 405 with an Allow header |
File Uploads
Multipart uploads and the alternative
| Option | How it works | Note |
|---|---|---|
| signed URL | a mutation returns an S3 or GCS upload URL; the client PUTs the file there | recommended: files never touch the GraphQL server |
| multipart request | the GraphQL multipart request spec with an Upload scalar | needs CSRF protection; Apollo Server 4 and later require a preflight header |
Introspection
__schema, __type, schema downloads and turning it off
Querying the Schema
Ask a type for its fields
{
__type(name: "PageInfo") {
kind
fields {
name
type { kind name ofType { name } }
}
}
}
Download the schema as SDL
import { getIntrospectionQuery, buildClientSchema, printSchema } from 'graphql';
const res = await fetch('https://swapi-graphql.netlify.app/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: getIntrospectionQuery() }),
});
const { data } = await res.json();
const sdl = printSchema(buildClientSchema(data));
console.log(sdl.split('\n').length, 'lines');
console.log(sdl.slice(sdl.indexOf('type Film '), sdl.indexOf('type Film ') + 110));
Disabling It
Turn introspection off in production
import { ApolloServer } from '@apollo/server';
const apollo = new ApolloServer({ typeDefs, resolvers, introspection: process.env.NODE_ENV !== 'production' });
import { createYoga } from 'graphql-yoga';
import { useDisableIntrospection } from '@graphql-yoga/plugin-disable-introspection';
const yoga = createYoga({ schema, plugins: [useDisableIntrospection()] });
Root Types
Which operations a server supports
{
__schema {
queryType { name }
mutationType { name }
subscriptionType { name }
}
}
Pagination
Relay cursor connections, offset paging and totalCount
Cursor Connections
First page
query Orders($after: String) {
orders(first: 2, after: $after) {
totalCount
edges { cursor node { id status } }
pageInfo { hasNextPage endCursor }
}
}
Next page
query Orders($after: String) {
orders(first: 2, after: $after) {
edges { node { id status } }
pageInfo { hasNextPage endCursor }
}
}
# variables: {"after": "b3JkZXI6b3JkXzg3OTA="}
The connection types
type OrderConnection {
edges: [OrderEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type OrderEdge {
cursor: String!
node: Order!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
Limits
Cap page size on the server
{
orders(first: 500) { totalCount }
}
Offset Pagination
Offset versus cursor
| Style | Arguments | Fits |
|---|---|---|
| offset | limit: 20, offset: 40, or page: 3 | admin tables with page numbers; small data that rarely changes |
| cursor | first: 20, after: "b3Jk..." | feeds, infinite scroll, large or fast-changing data |
Caching & Persisted Queries
HTTP caching, automatic persisted queries, trusted documents and client caches
Automatic Persisted Queries
Send a hash, fall back to the full query
import { createHash } from 'node:crypto';
import { createYoga, createSchema } from 'graphql-yoga';
import { useAPQ } from '@graphql-yoga/plugin-apq';
const yoga = createYoga({
schema: createSchema({ typeDefs: 'type Query { hello: String! }', resolvers: { Query: { hello: () => 'world' } } }),
plugins: [useAPQ()],
logging: false,
});
const query = '{ hello }';
const sha256Hash = createHash('sha256').update(query).digest('hex');
const extensions = JSON.stringify({ persistedQuery: { version: 1, sha256Hash } });
const get = async () => (await yoga.fetch('http://localhost/graphql?extensions=' + encodeURIComponent(extensions))).text();
console.log('1 GET hash only:', await get());
const post = await yoga.fetch('http://localhost/graphql', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ query, extensions: JSON.parse(extensions) }) });
console.log('2 POST query and hash:', await post.text());
console.log('3 GET hash only:', await get());
Trusted Documents
Allow only the operations your apps ship
| Step | What happens |
|---|---|
| build | the compiler or codegen extracts every operation from the client code with its hash |
| deploy | the hashes and documents are uploaded to the server or the gateway |
| runtime | clients send the hash only; any other document is rejected |
HTTP and Server Caching
Options that work with POST-heavy APIs
| Layer | Approach |
|---|---|
| CDN | GET with persisted queries and Cache-Control from the server |
| server response cache | Yoga's response cache plugin or Apollo Server's responseCachePlugin, keyed per user when needed |
| field hints | @cacheControl(maxAge: 60) on types and fields in Apollo Server |
| resolver data | DataLoader per request, Redis or an in-memory LRU across requests |
| client | normalized caches in Apollo Client, urql Graphcache or Relay |
Resolvers
the four arguments, context, default resolvers and server code
The Resolver Signature
parent, args, context, info
const resolvers = {
Query: {
orders: (parent, args, context, info) => context.db.orders.list(args),
},
Order: {
total: (order) => order.items.reduce((s, i) => s + i.price * i.qty, 0),
customer: (order, _, ctx) => ctx.loaders.customer.load(order.customerId),
},
};
Python with Strawberry
Code-first types and a query
import strawberry
@strawberry.type
class Product:
sku: str
name: str
price: float
PRODUCTS = [Product(sku="LAMP-01", name="Desk lamp", price=39.5), Product(sku="BULB-07", name="LED bulb", price=5.0)]
@strawberry.type
class Query:
@strawberry.field
def products(self, max_price: float | None = None) -> list[Product]:
return [p for p in PRODUCTS if max_price is None or p.price <= max_price]
schema = strawberry.Schema(query=Query)
print(schema.as_str())
print(schema.execute_sync("{ products(maxPrice: 10) { sku name } }").data)
Node with GraphQL Yoga
A complete server
import { createServer } from 'node:http';
import { createYoga, createSchema } from 'graphql-yoga';
const yoga = createYoga({
schema: createSchema({
typeDefs: 'type Query { hello(name: String = "world"): String! }',
resolvers: { Query: { hello: (_, { name }) => `Hello, ${name}` } },
}),
});
createServer(yoga).listen(4000, () => console.log('http://localhost:4000/graphql'));
N+1 & DataLoader
batching, caching per request and query planning
The N+1 Problem
Five orders, five customer queries, then one
import { graphql, buildSchema } from 'graphql';
import DataLoader from 'dataloader';
const schema = buildSchema(`
type Query { orders: [Order!]! }
type Order { id: ID! customer: Customer! }
type Customer { id: ID! name: String! }
`);
const names = { c1: 'Ana', c2: 'Mihai' };
const orders = [['o1', 'c1'], ['o2', 'c2'], ['o3', 'c1'], ['o4', 'c2'], ['o5', 'c1']];
let sql = [];
const customersByIds = async (ids) => {
sql.push(`SELECT * FROM customers WHERE id IN (${ids.map((i) => `'${i}'`).join(', ')})`);
return ids.map((id) => ({ id, name: names[id] }));
};
const root = (load) => ({ orders: () => orders.map(([id, c]) => ({ id, customer: () => load(c) })) });
const source = '{ orders { id customer { name } } }';
await graphql({ schema, source, rootValue: root(async (id) => (await customersByIds([id]))[0]) });
console.log('without DataLoader:', sql.length, 'queries');
sql = [];
const loader = new DataLoader(customersByIds);
await graphql({ schema, source, rootValue: root((id) => loader.load(id)) });
console.log('with DataLoader:', sql.length, 'query');
console.log(sql[0]);
DataLoader Rules
Get the details right
| Rule | Why |
|---|---|
| create loaders per request, in context | the cache must not leak data between users |
| return results in the same order as the keys | DataLoader matches results to keys by position |
| return null or an Error for missing keys | the array length must equal the number of keys |
| use one loader per access pattern | byId and byEmail need separate caches |
Servers by Language
Apollo Server, Yoga, Strawberry, Spring for GraphQL, Hot Chocolate, gqlgen and graphql-java
Pick a Server
The common choice in each language
| Language | Library | Style |
|---|---|---|
| JavaScript, TypeScript | GraphQL Yoga, Apollo Server, Pothos for code-first types | schema first or code first |
| Python | Strawberry, Ariadne, Graphene | code first, schema first, code first |
| Java, Kotlin | Spring for GraphQL on graphql-java, Netflix DGS | schema first with annotated controllers |
| C#, .NET | Hot Chocolate | code first or schema first |
| Go | gqlgen | schema first with generated Go types |
| Rust | async-graphql, Juniper | code first |
| PHP | webonyx/graphql-php, Lighthouse for Laravel | code first, schema first |
| Ruby | graphql-ruby | code first |
Spring for GraphQL
An annotated controller
@Controller
public class OrderController {
private final OrderService orders;
private final CustomerService customers;
public OrderController(OrderService orders, CustomerService customers) {
this.orders = orders;
this.customers = customers;
}
@QueryMapping
public Order order(@Argument String id) {
return orders.find(id);
}
@BatchMapping
public Map<Order, Customer> customer(List<Order> list) {
return customers.forOrders(list);
}
}
Apollo Server 5
Standalone server
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
context: async ({ req }) => ({ token: req.headers.authorization }),
});
console.log(url);
Security
depth and cost limits, aliases, batching, introspection and CSRF
Limit Query Depth and Cost
A depth limit as a validation rule
import { buildSchema, parse, validate, specifiedRules, GraphQLError, Kind } from 'graphql';
const schema = buildSchema(`
type Query { customer(id: ID!): Customer }
type Customer { name: String orders: [Order] }
type Order { id: ID customer: Customer }
`);
const maxDepth = (limit) => (context) => ({
OperationDefinition(op) {
const depthOf = (set, seen) => Math.max(0, ...set.selections.map((s) => {
if (s.kind === Kind.FRAGMENT_SPREAD) {
const frag = seen.has(s.name.value) ? null : context.getFragment(s.name.value);
return frag ? depthOf(frag.selectionSet, new Set([...seen, s.name.value])) : 0;
}
const own = s.kind === Kind.FIELD ? 1 : 0;
return s.selectionSet ? own + depthOf(s.selectionSet, seen) : own;
}));
const depth = depthOf(op.selectionSet, new Set());
if (depth > limit) context.reportError(new GraphQLError(`Query depth ${depth} exceeds the limit of ${limit}.`, { nodes: [op] }));
},
});
const query = parse('{ customer(id: "cus_42") { orders { customer { orders { customer { orders { id } } } } } } }');
console.log(validate(schema, query, [...specifiedRules, maxDepth(5)]).map((e) => e.message));
Use a maintained package in production
| Package | Works with | Covers |
|---|---|---|
| graphql-armor | Apollo Server, GraphQL Yoga and Envelop | depth, aliases, directives, tokens, cost, field suggestions |
| Yoga and Envelop plugins | GraphQL Yoga | rate limits, response cache, persisted operations, disabling introspection |
| Hot Chocolate | .NET | built-in cost analysis based on the IBM cost specification |
| Spring for GraphQL | Java | MaxQueryDepthInstrumentation and MaxQueryComplexityInstrumentation from graphql-java |
Aliases and Batching
One request, many attempts
mutation {
a1: login(email: "ana@example.com", password: "123456") { token }
a2: login(email: "ana@example.com", password: "password") { token }
a3: login(email: "ana@example.com", password: "qwerty") { token }
}
Checklist
Before a GraphQL API goes public
ok depth, alias and cost limits on every operation ok maximum page size enforced in resolvers ok authorization checked in the business layer, per object, not only per endpoint ok introspection and field suggestions off for private APIs ok trusted documents for first-party clients ok CSRF: require Content-Type application/json or a custom header, never accept text/plain or form posts ok errors masked so stack traces and SQL never reach clients ok request size and timeout limits at the HTTP layer
Authentication & Authorization
tokens in context, field-level checks and directives
Authenticate Once, Authorize Everywhere
Put the user in context
const yoga = createYoga({
schema,
context: async ({ request }) => {
const header = request.headers.get('authorization') ?? '';
const user = header.startsWith('Bearer ') ? await verifyToken(header.slice(7)) : null;
return { user };
},
});
Check access where the data is loaded
const resolvers = {
Query: {
node: async (_, { id }, { user, loaders }) => {
const obj = await loaders.byGlobalId.load(id);
return obj && canRead(user, obj) ? obj : null;
},
},
};
Declarative rules
| Tool | Style |
|---|---|
| @auth or @requiresScopes directives | Apollo Router, Hot Chocolate @authorize, Lighthouse @can |
| graphql-shield | a permission tree mapped onto the schema for Node servers |
| @PreAuthorize | Spring Security on Spring for GraphQL controller methods |
Federation
subgraphs, entities, routers and composite schemas
Entities Across Subgraphs
A subgraph extends Product with reviews
import { ApolloServer } from '@apollo/server';
import { ApolloServerPluginInlineTraceDisabled } from '@apollo/server/plugin/disabled';
import { buildSubgraphSchema } from '@apollo/subgraph';
import { parse } from 'graphql';
const typeDefs = parse(`
extend schema @link(url: "https://specs.apollo.dev/federation/v2.12", import: ["@key"])
type Product @key(fields: "sku") {
sku: String!
reviewCount: Int!
}
`);
const counts = { 'LAMP-01': 12, 'BULB-07': 3 };
const resolvers = {
Product: { __resolveReference: (ref) => ({ sku: ref.sku, reviewCount: counts[ref.sku] ?? 0 }) },
};
const server = new ApolloServer({ schema: buildSubgraphSchema([{ typeDefs, resolvers }]), plugins: [ApolloServerPluginInlineTraceDisabled()] });
const res = await server.executeOperation({
query: 'query ($r: [_Any!]!) { _entities(representations: $r) { ... on Product { sku reviewCount } } }',
variables: { r: [{ __typename: 'Product', sku: 'LAMP-01' }, { __typename: 'Product', sku: 'BULB-07' }] },
});
console.log(JSON.stringify(res.body.singleResult.data, null, 2));
Federation or one server
| Option | Fits |
|---|---|
| a single GraphQL server | one team, or a monolith with one schema |
| federation with Apollo Router, Hive Gateway or WunderGraph Cosmo | several teams owning parts of the graph, deployed independently |
| schema stitching with graphql-tools | combining a few existing GraphQL APIs you do not control |
Clients
fetch, Apollo Client 4, urql, Relay and graphql-request
Plain fetch
No library needed
const res = await fetch('https://swapi-graphql.netlify.app/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/graphql-response+json, application/json' },
body: JSON.stringify({
query: 'query Film($id: ID!) { film(id: $id) { title director } }',
variables: { id: 'ZmlsbXM6Mg==' },
}),
});
const { data, errors } = await res.json();
if (errors) console.error(errors);
console.log(res.status, data.film);
Apollo Client 4
React setup and a query
import { ApolloClient, HttpLink, InMemoryCache, gql } from '@apollo/client';
import { ApolloProvider, useQuery } from '@apollo/client/react';
const client = new ApolloClient({
link: new HttpLink({ uri: '/graphql' }),
cache: new InMemoryCache(),
});
const ORDER = gql`
query Order($id: ID!) {
node(id: $id) { id ... on Order { status total } }
}
`;
function OrderStatus({ id }) {
const { data, loading, error } = useQuery(ORDER, { variables: { id } });
if (loading) return 'Loading';
if (error) return error.message;
return data.node.status;
}
Choosing a Client
What each one is good at
| Client | Strength | Cache |
|---|---|---|
| fetch or graphql-request | scripts, servers, tiny bundles | none |
| urql | small, extensible with exchanges | document cache, normalized with Graphcache |
| Apollo Client | most features, devtools, local state | normalized by __typename and id |
| Relay | large React apps, fragment colocation enforced by a compiler | normalized, with garbage collection |
| TanStack Query with fetch | apps that already use it for REST | per query key |
Tooling & Codegen
GraphiQL, code generation, linting and schema checks
Typed Operations
GraphQL Code Generator with the client preset
import type { CodegenConfig } from '@graphql-codegen/cli';
const config: CodegenConfig = {
schema: 'https://swapi-graphql.netlify.app/graphql',
documents: ['src/**/*.{ts,tsx}'],
generates: {
'./src/gql/': { preset: 'client' },
},
};
export default config;
Everyday Tools
What each tool is for
| Tool | Use it to |
|---|---|
| GraphiQL | explore a schema and run queries in the browser; Yoga and Apollo serve it in development |
| graphql-eslint | lint operations and schemas in CI: naming, deprecated fields, unused fragments |
| GraphQL Inspector or Rover schema check | catch breaking schema changes in pull requests |
| GraphQL extension for VS Code | autocomplete and validation from graphql.config files |
| Altair, Insomnia, Postman | send queries with headers, variables and files |
Testing
operations in tests, mocked schemas and what to test where
Test Operations, Not Just Resolvers
Call the server in-process
import assert from 'node:assert/strict';
import { yoga } from './shop.mjs';
async function execute(query, variables, headers = {}) {
const res = await yoga.fetch('http://test/graphql', {
method: 'POST',
headers: { 'content-type': 'application/json', ...headers },
body: JSON.stringify({ query, variables }),
});
return { status: res.status, body: await res.json() };
}
const order = await execute('query ($id: ID!) { node(id: $id) { ... on Order { status total } } }', { id: 'ord_8812' });
assert.equal(order.status, 200);
assert.deepEqual(order.body.data.node, { status: 'PAID', total: 49.5 });
const anonymous = await execute('{ me { name } }');
assert.equal(anonymous.body.errors[0].extensions.code, 'UNAUTHENTICATED');
const signedIn = await execute('{ me { name } }', undefined, { authorization: 'Bearer test' });
assert.equal(signedIn.body.data.me.name, 'Ana Pop');
console.log('3 operations tested, all assertions passed');
The same with Apollo Server
import assert from 'node:assert/strict';
import { ApolloServer } from '@apollo/server';
const server = new ApolloServer({
typeDefs: 'type Query { hello(name: String!): String! }',
resolvers: { Query: { hello: (_, { name }) => `Hello, ${name}` } },
});
const res = await server.executeOperation({ query: 'query ($n: String!) { hello(name: $n) }', variables: { n: 'Ana' } });
assert.equal(res.body.kind, 'single');
assert.equal(res.body.singleResult.data.hello, 'Hello, Ana');
console.log(JSON.stringify(res.body.singleResult));
Mocked Schemas
Fake data that matches the schema
import { makeExecutableSchema } from '@graphql-tools/schema';
import { addMocksToSchema } from '@graphql-tools/mock';
import { graphql } from 'graphql';
const typeDefs = `
type Query { orders: [Order!]! }
type Order { id: ID! status: Status! total: Float! }
enum Status { PENDING PAID }
`;
const schema = addMocksToSchema({
schema: makeExecutableSchema({ typeDefs }),
mocks: { Float: () => 49.5, ID: () => 'ord_1', Status: () => 'PAID' },
});
console.log(JSON.stringify(await graphql({ schema, source: '{ orders { id status total } }' })));
What to Test Where
A practical split
| Layer | Test |
|---|---|
| resolvers and services | business rules and authorization, with the database or a test container |
| operations | real queries from the client through yoga.fetch or executeOperation, including errors and permissions |
| schema | breaking-change checks and graphql-eslint rules in CI |
| client | components against mocked responses with MSW or Apollo's MockedProvider |
| performance | the slowest real operations, with DataLoader batching and cost limits enabled |
GraphQL vs REST
when each one fits, and what changes for the team
Side by Side
The practical differences
| Topic | REST | GraphQL |
|---|---|---|
| endpoints | one per resource | one for everything |
| response shape | fixed by the server | chosen by the client |
| over-fetching and round trips | common | avoided by design |
| HTTP caching | built in with GET and ETags | needs persisted queries or a response cache |
| errors | HTTP status codes | mostly 200 with an errors array |
| versioning | /v2 or headers | evolve the schema, deprecate fields |
| typed contract | optional, with OpenAPI | always, with the schema |
| rate limiting | per request | per query cost |
| file uploads | multipart, simple | outside the spec, signed URLs preferred |
Troubleshooting
the errors people search for, and what they mean
Common Errors
Message and fix
| Error | Cause and fix |
|---|---|
| Cannot query field "x" on type "Y" | typo or field not in the schema; check the suggestion and the server version |
| Field "edges" of type "[OrderEdge!]!" must have a selection of subfields | object fields need braces down to scalars |
| Field "totalCount" must not have a selection since type "Int!" has no subfields | remove the braces after a scalar field |
| Variable "$id" of required type "ID!" was not provided | send it in variables, with the same name and no $; graphql 17 words it Variable "$id" has invalid value |
| Variable "$x" of type "ID" used in position expecting type "ID!" | declare the variable as non-null or give it a default |
| Variable "$n" is never used | remove the declaration or use it; unused variables fail validation |
| Cannot return null for non-nullable field Y.x | the resolver returned null or threw; fix it or make the field nullable |
| Unknown type "X" | the type is missing from the schema, or the operation was written for a different API |
| Must provide query string, Missing query, Unexpected parameter "qeury" | the body is not JSON with a query key, or Content-Type is wrong |
| Syntax Error: Expected Name, found "{" | a parenthesis or brace is missing; the analyze tool above shows where |
Versions
the specification and the libraries as of October 2026
The Specification
Editions and drafts
| Document | Status |
|---|---|
| GraphQL specification, September 2025 edition | current; added @oneOf input objects, schema coordinates and descriptions on operations |
| GraphQL over HTTP | working draft, being prepared for its first release |
| Incremental delivery: @defer and @stream | RFC in progress; experimental in graphql-js 17 |
| Composite Schemas | working draft for federation-style gateways |
| Relay Cursor Connections | stable convention used by most paginated APIs |
Libraries
Current releases checked on October 14, 2026
| Library | Version | Note |
|---|---|---|
| graphql-js | 17.0.2 | v17 released June 15, 2026, with @defer and @stream behind experimental APIs |
| GraphQL Yoga | 5.24.5 | supports graphql 15, 16 and 17 |
| Apollo Server | 5.5.1 | Node.js 20+, graphql 16.11+; v4 ended January 26, 2026 |
| Apollo Client | 4.3.3 | hooks in @apollo/client/react; rxjs peer dependency |
| urql | 5.0.4 | | |
| graphql-ws | 6.4.0 | WebSocket subscriptions |
| GraphQL Code Generator CLI | 7.4.6 | | |
| Strawberry | 0.335.0 | Python 3.11 or later |
| Spring for GraphQL | 2.0.5 | Spring Boot 4; graphql-java 26.1 is the latest graphql-java |
| Hot Chocolate | 16.6.8 | .NET |
| gqlgen | 0.17.95 | Go |
What Is a GraphQL Cheat Sheet
A GraphQL cheat sheet is a compact reference to the query language and the conventions around it: how to write queries, variables and fragments, how to design a schema and its nullability, what errors and status codes a server returns, and how to paginate, cache and secure an API. People also search for GraphQL query examples or a GraphQL syntax reference; they usually want the same thing while writing a query against an unfamiliar API or designing a new schema.
This cheat sheet covers 29 sections and 94 snippets, from a first query to federation. The output under each example is what it returned when run on October 14, 2026: film queries ran against the public Star Wars API at swapi-graphql.netlify.app, and order, mutation, error and subscription examples ran against a small shop schema on GraphQL Yoga 5.24.5 with graphql-js 17.0.2.
The toolbox at the top parses a query or schema in your browser and reports its depth, field count, estimated cost, aliases and any unused or undefined fragments and variables. It also formats and minifies documents, lints a schema against common naming conventions, and turns a query into curl, fetch, a GET URL and a persisted-query hash. The builders write a type with its resolvers for four servers, Relay connection types, client code for five libraries, and a schema inferred from sample JSON. Nothing you paste is sent anywhere.
How It Differs From the Official Docs
The documentation at graphql.org teaches the language one concept at a time, and the specification defines it precisely. This page puts the parts developers look up during real work side by side, with the behavior of today's servers where they differ from the spec, such as the HTTP status codes. For the REST side of the same decisions, see the REST API cheat sheet; for the payload format, the JSON cheat sheet.
What This GraphQL Cheat Sheet Covers
The 29 sections follow the path of a request, and each link jumps to that section above.
- Basics: the toolbox and a first query, how GraphQL works, queries, variables, fragments and directives.
- Operations: mutations and subscriptions.
- Schema: SDL, types and scalars, nullability, errors and schema evolution.
- Over the wire: GraphQL over HTTP, introspection, pagination and caching and persisted queries.
- Server side: resolvers, N+1 and DataLoader, servers by language, security, authorization and federation.
- In practice: clients, tooling, testing, GraphQL compared with REST, troubleshooting and versions.
GraphQL Quick Reference
The syntax people look up most often, one line each.
| Task | Syntax |
|---|---|
| Query with an argument | { film(id: "1") { title } } |
| Named operation with a variable | query Film($id: ID!) { film(id: $id) { title } } |
| Default value | query ($first: Int = 10) |
| Alias | latest: film(id: "6") { title } |
| Named fragment | fragment Card on Film { title }, used as ...Card |
| Inline fragment | ... on Product { price } |
| Conditional field | crawl @include(if: $full) or @skip(if: $short) |
| Mutation | mutation ($input: CreateOrderInput!) { createOrder(input: $input) { order { id } } } |
| Type name in the result | __typename |
| Non-null and lists | [Order!]! |
| Next page | orders(first: 20, after: $endCursor) |
| Schema of a type | { __type(name: "Order") { fields { name } } } |
What GraphQL Is
GraphQL is a query language for APIs and a runtime that executes those queries against a typed schema. Facebook built it in 2012 for its mobile apps, released it as open source in 2015, and moved it to the GraphQL Foundation, under the Linux Foundation, in 2019. The current edition of the specification is dated September 2025.
A GraphQL server publishes a schema that lists every type, field and argument it supports. Clients send a document to a single endpoint, the server validates it against the schema, runs a resolver function for each requested field and returns JSON in the same shape as the query. Because the schema is typed and can be queried itself, tools such as GraphiQL, code generators and editors know exactly what is available.
GraphQL usually sits in front of other systems: databases, REST services and microservices. It is a common choice in back-end development for products with web and mobile apps that each need a different slice of the same data.
Where It Fits
GraphQL fits apps whose screens combine data from many sources, teams that want a typed contract between front end and back end, and products with several clients evolving at different speeds. REST remains simpler for public APIs with plain resources and for anything that relies on HTTP caching; the comparison section above has the details.
How a GraphQL Request Is Executed
Following one request through a server explains where each part of this page applies.
- The client sends a POST with a JSON body holding query, variables and operationName, or a GET with the same values in the URL.
- The server parses the document; a syntax error ends the request with a parse error.
- The document is validated against the schema and any extra rules such as depth and cost limits.
- Variables are coerced to their declared types, and defaults are filled in.
- The executor calls a resolver for each field, top-level mutation fields one at a time, with DataLoader batching database calls.
- Field errors become entries in errors with a path, and nulls propagate up to the nearest nullable field.
- The response is serialized as JSON with data and errors, and an HTTP status that depends on the server and the Accept header.
Common GraphQL Mistakes
The same problems show up in most schema and API reviews.
In Schema Design
- Making every field non-null, so one failing resolver wipes out the whole response.
- Mutations with many scalar arguments instead of a single input object and a payload type.
- Expected failures thrown as errors instead of returned as typed userErrors.
- Lists without pagination or a maximum page size.
- Removing fields without deprecating them first and checking that no client still uses them.
In Servers and Clients
- Resolvers that query the database per item, the N+1 problem, instead of batching with DataLoader.
- No depth, alias or cost limits, so one request can ask for millions of objects.
- Checking that a user is logged in but not that they may read each object, especially through node(id:).
- Building queries with string concatenation instead of variables.
- Clients that read data and ignore errors, hiding partial failures.
How to Build Good GraphQL APIs
Design the Schema for Clients
Start from the screens and use cases that will query the API rather than from the database tables, name fields for what they mean to clients, and review schema changes like code. Run a breaking-change check in continuous integration against the operations your clients actually send, and deprecate before you remove.
Then Protect and Watch It
Add depth and cost limits, trusted documents for your own apps and per-object authorization before launch, and test resolvers and whole operations, as the guide to types of software testing describes. Track errors and latency per operation name after each deployment. The TypeScript cheat sheet and React cheat sheet cover the stack most GraphQL clients are written in.
How This GraphQL Cheat Sheet Is Maintained
This page is written and maintained by Bogdan Sandu for TMS Outsource, a software development company. It was checked on October 14, 2026 against the GraphQL specification, September 2025 edition, the GraphQL over HTTP working draft and the Relay Cursor Connections specification. The output of the examples was produced by running them with graphql-js 17.0.2, GraphQL Yoga 5.24.5, graphql-http 1.23.1, Apollo Server 5.5.1, Strawberry 0.335.0, Node.js 24 and curl. If a snippet is wrong or out of date, the date in the byline shows when it was last reviewed.
GraphQL Questions People Actually Ask
The questions developers search for most about GraphQL, answered in a few sentences each.
What is GraphQL?
GraphQL is a query language for APIs and a runtime that executes those queries against a typed schema. Clients send one request to a single endpoint and describe exactly which fields they need, and the response comes back in the same shape. It was created at Facebook in 2012, open-sourced in 2015 and is now maintained by the GraphQL Foundation.
What is the difference between GraphQL and REST?
REST exposes many endpoints with fixed responses and relies on HTTP methods, status codes and caching. GraphQL exposes one endpoint where the client chooses the fields, which avoids over-fetching and extra round trips but makes HTTP caching, rate limiting and error handling work differently. Many companies run both.
What is a GraphQL query, mutation and subscription?
A query reads data, and its fields may run in parallel. A mutation changes data, and its top-level fields run one after another. A subscription keeps a connection open, over WebSockets or Server-Sent Events, and pushes a new result each time an event happens.
What are fragments in GraphQL?
A fragment is a reusable set of fields on a type, such as fragment FilmCard on Film { title releaseDate }, spread into queries with ...FilmCard. Inline fragments such as ... on Product { price } select fields only when an object has that type, which is how you query interfaces and unions.
Why does GraphQL return 200 for errors?
Many servers answer 200 whenever they can return a GraphQL response, and put problems in the errors array, because a single response can contain both data and errors. With the application/graphql-response+json media type, servers return 400 for requests that cannot be parsed or validated, and the 2026 working draft of GraphQL over HTTP recommends 422 for invalid requests and 294 for partial success.
What is the N+1 problem in GraphQL?
Each field resolver runs on its own, so loading a list of 50 orders and then each order's customer can make 51 database queries. DataLoader solves it by collecting the keys requested in the same tick and loading them with one batched query, with a cache that lives for one request.
How do I secure a GraphQL API?
Limit query depth, aliases and estimated cost, cap page sizes, check authorization for every object in the business layer, turn off introspection and field suggestions for private APIs, mask internal error messages, and use trusted documents for first-party clients. Packages such as graphql-armor add most of these limits to Apollo Server and GraphQL Yoga.
How does pagination work in GraphQL?
Most APIs follow the Relay connection convention: a field takes first and after arguments and returns edges with a cursor and a node, plus pageInfo with hasNextPage and endCursor. The client passes endCursor as after to load the next page. Offset pagination with limit and offset also works but breaks when rows are inserted between pages.
Is this GraphQL cheat sheet up to date?
Yes. It was checked on October 14, 2026 against the September 2025 GraphQL specification, the GraphQL over HTTP working draft of September 2026, graphql-js 17.0.2, GraphQL Yoga 5.24.5, Apollo Server 5.5.1, Apollo Client 4.3.3 and Strawberry 0.335. The example outputs come from real runs.
Can I download this GraphQL cheat sheet as a PDF?
Yes. The page has a print stylesheet, so your browser's print command with Save as PDF produces a clean copy with every output and explanation expanded and the navigation left out. The print core button prints only the core snippets for a short reference.