GraphQL Cheatsheet - GraphQL Query Language & API Reference
This reference is for developers designing a GraphQL schema or consuming one from Apollo/URQL/etc. It covers the client-facing vocabulary first — querying exactly the fields you want, mutating data, subscribing to updates, and reusing fragments — then the schema side from the type system and directives to pagination and the introspection __typename. Unlike a generic syntax sheet, items are grouped by the role you are playing in an API conversation. After reading you should be able to shape a precise query with arguments and fragments, declare a type with the right scalar/object fields, and add a connection-style paginated field.
Typical Use Case
For full-stack engineers who must define both the API contract and the front-end data shape, especially those moving from REST. The payoff is that what the client asks for becomes an executable contract; the cost is that field-level authorisation, N+1 queries and query depth all become server problems. Covers queries with aliases, fragments, directives and cursor pagination, mutations and the type system. One request can trigger an expensive query, so depth limits must be explicit.
Query 9
{ user { id name } }{ user(id: 1) { name } }{ users { id name posts { title } } }query GetUser($id: ID!) { user(id: $id) { name } }{ user { name email } posts { title } }{ user(id: 1) { name @include(if: $withName) } }{ user(id: 1) { name @skip(if: $skipName) } }{ me: user(id: 1) { name } you: user(id: 2) { name } }{ users(first: 10) { edges { node { name } } } }Mutation 5
mutation { createUser(name: "Alice") { id } }mutation CreateUser($input: CreateUserInput!) { createUser(input: $input) { id } }mutation { updateUser(id: 1, name: "Bob") { id name } }mutation { deleteUser(id: 1) { id } }mutation { bulkCreate(input: [{ name: "A" }, { name: "B" }]) { id } }Subscription 3
subscription { newMessage { content } }subscription { userUpdated(id: 1) { name } }subscription($roomId: ID!) { messageAdded(roomId: $roomId) { id content } }Schema (Type System) 10
type User { id: ID! name: String! email: String }input CreateUserInput { name: String! email: String! }enum Role { ADMIN USER GUEST }union SearchResult = User | Postinterface Node { id: ID! }type User implements Node { id: ID! name: String! }scalar DateTimetype Query { user(id: ID!): User }type Mutation { createUser(input: CreateUserInput!): User }directive @auth(requires: Role!) on FIELD_DEFINITIONFragments & Directives 5
fragment UserFields on User { id name email }{ user { ...UserFields } }{ user { ...UserFields ... on Admin { permissions } } }@deprecated(reason: "Use newField")@cacheControl(maxAge: 3600)Introspection & Validation 5
{ __schema { types { name } } }{ __type(name: "User") { fields { name type { name } } } }{ __schema { queryType { name } mutationType { name } } }{ __type(name: "User") { fields { name args { name type { name } } } } }{ __schema { directives { name locations } } }Advanced Features 5
query($id: ID!, $withPosts: Boolean!) { user(id: $id) { name posts @include(if: $withPosts) { title } } }{ users(first: 10, after: "cursor") { pageInfo { hasNextPage endCursor } } }{ user { name } } # expected user.name; errors returned alongside dataquery @cost(complexity: 10) { user { name } }extend type User { age: Int }Parameter matrix
| Parameter | Effect | Example |
|---|---|---|
{ user { id name } } | The smallest legal query. A selection set needs at least one field, and naming a top-level field directly is equivalent to writing the query keyword. | { user { id name } } |
{ user(id: 1) { name } } | A field with arguments. Argument types must match the schema exactly, and an ID will not silently accept anything but the literal string form. | { user(id: 1) { name } } |
query GetUser($id: ID!) { user(id: $id) { name } } | A named operation with a variable. Lifting arguments out of the document text makes the query printable, cacheable and reusable. | query GetUser($id: ID!) { user(id: $id) { name } } |
me: user(id: 1) { name } you: user(id: 2) { name } | Aliases. When the same field is requested more than once with different arguments, aliases keep the response keys distinct instead of triggering the field-merging conflict. | { me: user(id: 1) { name } you: user(id: 2) { name } } |
name @include(if: $withName) | Conditional inclusion: the field is requested only when the variable is true, which suits detail views with expandable sections. | { user(id: 1) { name @include(if: $withName) } } |
name @skip(if: $skipName) | The complement of @include, skipping the field when the variable is true. Both directives can sit on the same field. | { user(id: 1) { name @skip(if: $skipName) } } |
fragment UserFields on User { id name email } | Fragment reuse. A named fragment must carry its on type condition and must be declared at the top level of the document, not inside a selection set. | fragment UserFields on User { id name email } |
{ user { ...UserFields ... on Admin { permissions } } } | An inline fragment combined with a union or interface, used to reach fields that only exist on one concrete member type. | { user { ...UserFields ... on Admin { permissions } } } |
{ users(first: 10) { edges { node { name } } } } | The Relay-style connection shape: edges and node decouple list entries from pagination metadata. | { users(first: 10) { edges { node { name } } } } |
pageInfo { hasNextPage endCursor } | Cursor pagination metadata for first/after: it carries the cursor for the next page and avoids the cost of deep offset paging. | { users(first: 10, after: "cursor") { pageInfo { hasNextPage endCursor } } } |
mutation { createUser(name: "Alice") { id } } | A write operation must spell out the mutation keyword explicitly, and the selection set still declares what comes back. | mutation { createUser(name: "Alice") { id } } |
mutation CreateUser($input: CreateUserInput!) { createUser(input: $input) { id } } | Passing a single input object is the standard shape once arguments get numerous; the bang marks the argument as non-null. | mutation CreateUser($input: CreateUserInput!) { createUser(input: $input) { id } } |
bulkCreate(input: [{ name: "A" }, { name: "B" }]) | List arguments allow batch writes, but the server still has to implement the transaction and per-item error reporting itself. | mutation { bulkCreate(input: [{ name: "A" }, { name: "B" }]) { id } } |
subscription { newMessage { content } } | Subscriptions need a persistent connection plus resolver support, which not every transport implements; the server also owns reconnection handling. | subscription { newMessage { content } } |
type User { id: ID! name: String! email: String } | An object type. Non-null markers are only enforced by the server, so this is where genuine invariants actually get expressed. | type User { id: ID! name: String! email: String } |
input CreateUserInput { name: String! email: String! } | An input type may only appear as an argument, never as a return value, which is why the input and output sides are split field by field. | input CreateUserInput { name: String! email: String! } |
enum Role { ADMIN USER GUEST } | An enum constrains a field to a fixed set of values, safer than free-text String and enumerable on the client. | enum Role { ADMIN USER GUEST } |
union SearchResult = User | Post | A union means one of these types; an inline fragment is required before any concrete field can be selected. | union SearchResult = User | Post |
interface Node { id: ID! } | An interface captures the field contract several types share, and each object declares conformance with implements. | type User implements Node { id: ID! name: String! } |
scalar DateTime | A custom scalar whose serialisation and parsing live in server code, so the wire format has to be agreed with every client. | scalar DateTime |
directive @auth(requires: Role!) on FIELD_DEFINITION | Declares where a custom directive may appear, and is the entry point for field-level authorisation and complexity budgets. | directive @auth(requires: Role!) on FIELD_DEFINITION |
@deprecated(reason: "Use newField") | Marks a field deprecated, so generated client code warns rather than failing outright. | @deprecated(reason: "Use newField") |
@cacheControl(maxAge: 3600) | A cache hint: a gateway or server that understands it translates the directive into response cache headers. | @cacheControl(maxAge: 3600) |
{ __schema { types { name } } } | Introspection over every type, which is how client generators and schema browsers get their starting point. | { __schema { types { name } } } |
{ __type(name: "User") { fields { name args { name type { name } } } } } | Introspection for one named type, listing its fields and arguments. The most direct check when a field name is suspected wrong. | { __type(name: "User") { fields { name name args { name type { name } } } } } |
{ __schema { queryType { name } mutationType { name } } } | Confirms the names of the root operation types, which stops a mutation being sent to a read-only gateway by accident. | { __schema { queryType { name } mutationType { name } } } |
query @cost(complexity: 10) { user { name } } | Declares the cost of an operation; combined with a server-side ceiling it blocks abusive deeply nested queries. | query @cost(complexity: 10) { user { name } } |
extend type User { age: Int } | Extends an existing type, which is how a modular schema split avoids redeclaring the same type in several files. | extend type User { age: Int } |
Common pitfalls
SymptomA query fails with Cannot query field "xxx" on type "Query" even though the schema clearly declares it.
CauseThe schema has several entry files and the field is defined in one of them, or a gateway filters introspection by path so the client only sees a trimmed subset.
FixConfirm what the server really exposes with { __schema { queryType { fields { name } } } }, then check whether introspection itself requires authorisation.
SymptomOne request selects the same field twice with different arguments, and only one result comes back.
CauseWithout aliases the response field-merging rules require same-name fields to agree on arguments and raise a conflict otherwise.
FixDistinguish them with aliases such as me: user(id: 1) and you: user(id: 2).
SymptomGraphQL gets treated like REST and one deeply nested query takes the database down.
CauseFields nest without limit, so the client effectively decides how much work one request costs; with no depth or complexity ceiling in place, nothing stops it.
FixSet maximum depth and complexity on the server (a cost directive plus validation), and batch nested lookups with DataLoader.
SymptomA field has a value but the client renders null; the schema marks it nullable String and the client assumed otherwise.
CauseNullable and non-null fields are asymmetric at the type level, and a server returning null for a row where the field is empty is entirely spec-compliant.
FixMake generated client code treat it as nullable; if it really is a business invariant, mark it String! in the schema and guarantee it server-side.
SymptomPart of a mutation succeeds, the call reports an error, and the data is left half-written.
CauseThe specification guarantees no atomicity across sibling mutation fields: successes and errors are aggregated as separate entries of the same response.
FixWrap whatever has to be atomic in a single field, run the transaction server-side and roll the whole thing back on failure before raising the error.
SymptomA subscription connects but no event ever arrives, or the connection drops repeatedly.
CauseSubscriptions depend on a long-lived connection and on the transport and resolver both supporting them; a proxy or gateway may time the connection out, or the resolver never registered the event.
FixVerify the transport (WebSocket or SSE) and the resolver implementation, fall back to polling if needed, and implement reconnection plus deduplication.
SymptomcacheControl is declared but the response carries no cache headers, so every repeat request hits the server.
CauseCache hints only take effect where a gateway or server resolver implements them, and many implementations treat the directive as nothing more than schema documentation.
FixConfigure the cache rules explicitly at the gateway instead of assuming the directive will be honoured.
SymptomIntrospection works in development but production replies that introspection is not allowed.
CauseProduction disables introspection for safety, which breaks client code generation.
FixKeep the schema document under version control and publish it deliberately, or retain read-only introspection inside a controlled network.
Troubleshooting
1Confirm which root types the schema exposes
grep -n "type Query\|type Mutation" schema.graphqlsGrepping the local document is the quickest way to verify a field name and catch typos.
2List every type name for cross-checking
grep -n "^type \|^interface \|^union \|^enum " schema.graphqlsThis is the fastest way to check the contract without a client, using only the file on disk.
3Look for missing non-null markers
grep -n ": String\|: ID\|: Int\b" schema.graphqlsA field without the bang makes clients treat a value that is really mandatory as optional.
4Verify that depth and complexity limits are actually enabled
grep -n "maxDepth\|maxComplexity\|cost" serverWithout a ceiling, resource consumption is entirely the client decision.
5Check whether N+1 access has been batched
grep -n "DataLoader\|dataloader" serverRow-by-row lookups for nested fields are the most common performance incident in a GraphQL server.
6Confirm the modular schema split is complete
grep -rn "extend type" schema.graphqlsIn a multi-entry setup a single missing extend type drops a whole module worth of fields.
Command Examples
Variables plus conditional fields in one query
query GetUser($id: ID!, $withName: Boolean!) {
user(id: $id) {
name @include(if: $withName)
email @skip(if: $withName)
}
}Variables are declared outside the query with an explicit type such as ID! or Boolean! and their values travel in the request variables, which keeps untrusted values out of the query string and lets the server reuse a parsed document. @include keeps a field when the condition is true and @skip drops it when true, so pairing them lets one document serve two response shapes.
Output
{
"data": {
"user": {
"name": "Alice",
"email": null
}
}
}Aliases resolve response-merging conflicts on the same field
{ me: user(id: 1) { name } you: user(id: 2) { name } }Requesting user twice with different arguments in one document is a hard conflict: the response-merging rule only accepts identically-parameterised fields under the same name. Renaming them to me: and you: turns them into two independent response fields, which both fetches two objects in parallel and lifts the one-argument-per-field restriction.
Output
{
"data": {
"me": {
"name": "Alice"
},
"you": {
"name": "Bob"
}
}
}Relay connection pagination: edges, node and a pageInfo cursor
{ users(first: 10, after: "cursor") { edges { node { name } } pageInfo { hasNextPage endCursor } } }first and after are the cursor arguments of the Relay spec, where after takes the previous page endCursor. Whether another page exists is decided by pageInfo.hasNextPage, not by guessing that this page returned fewer than first rows: when filters change those two checks disagree.
Output
{
"data": {
"users": {
"edges": [
{ "node": { "name": "Alice" } }
],
"pageInfo": {
"hasNextPage": true,
"endCursor": "Y3Vyc29y"
}
}
}
}Submit mutations through input variables, not string concatenation
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) { id }
}Mutable arguments belong in an input type such as CreateUserInput! and travel as variables: a schema change then surfaces as a validation error, whereas concatenated mutation strings leave the server parsing raw text and defeat validation and query caching. Select only { id } in the response: the mutation returns the full object by default, so a missing selection set ships the email address and other fields along with it.
Common Pitfalls
- Missing field-level authorisation is a direct privilege escalation: as long as the client can pass any id it can read and write other people data, and GraphQL has no natural guard equivalent to omitting a URL parameter. Every entry point like user(id: $id) must verify the caller identity against the target object inside the resolver; an @auth directive on the schema that nothing enforces is only a comment.
- Leaving introspection on in production hands attackers the entire schema: types, arguments, enum values and deprecated fields, making it easy to enumerate internal fields absent from the docs. Turning it off instead breaks any front-end pipeline that pulls the schema for code generation, so version the schema file and publish it deliberately.
- When a resolver queries per field, users { posts { comments { author { ... } } } } turns one request into hundreds or thousands of database round trips, and a handful of concurrent requests exhausts the connection pool and times out the whole site. List fields must be batch-loaded (DataLoader or similar) or joined directly in SQL.
- Top-level mutation fields are not idempotent, and automatic client or gateway retries can execute the same change twice: duplicate users, double charges, duplicate messages sent. Either make the write idempotent with a client-supplied key de-duplicated server-side, or explicitly reject replays.
- When a user-scoped field such as profile or email carries @cacheControl and the gateway keys the cache by field name rather than by identity, one user response is cached and served to another, which shows up as mixed-up data rather than an error. Either key the cache by identity or leave such fields out of shared caches.
Tips
- GraphQL returns only the fields the client asks for; the server need not return everything.
- Variables use a $ prefix and are defined outside the query to avoid string concatenation.
- The Schema Definition Language (SDL) is core to GraphQL: define types first, then implement resolvers.
- Introspection queries (__schema/__type) can generate TypeScript types and editor tooling.
- Prefer the Relay connection spec (edges/node/pageInfo) for pagination, supporting forward and backward paging.
FAQ
Should I use @include or @skip to return fields conditionally?
Both take an if: $boolean argument; they differ only in direction. @include(if: $withName) means "add the field only when the condition holds", e.g. query($id: ID!, $withPosts: Boolean!) { user(id: $id) { name posts @include(if: $withPosts) { title } } }. @skip(if: $skipName) means "drop the field when the condition holds", which suits fields that must be suppressed in a particular combination. The client just passes true or false as a variable, so no string concatenation is needed, and the server treats both directives identically.
How do I read fields safely when a union type may be either User or Post?
Narrow by type with inline fragments. Define the shared shape once as fragment UserFields on User { id name email }, then write { user { ...UserFields ... on Admin { permissions } } } — the ... on Admin block applies only when the runtime type really is Admin and is skipped otherwise. Paired with the SDL declaration union SearchResult = User | Post, this lets the client fetch type-specific fields without guessing at the shape.
Should pagination use first/after cursors or offsets?
The sheet recommends the Relay connection spec: { users(first: 10, after: "cursor") { edges { node { name } } pageInfo { hasNextPage endCursor } } }, where first sets the page size, after takes the endCursor from the previous page, and pageInfo.hasNextPage tells you whether more remain. Cursors are more robust than offsets because rows inserted while you page neither get skipped nor duplicated. The compact form { users(first: 10) { edges { node { name } } } } follows the same spec.
How do I discover field names and arguments before writing a query?
Ask the server for its schema using introspection. { __schema { types { name } } } lists every type, { __type(name: "User") { fields { name type { name } } } } shows one type's fields, { __type(name: "User") { fields { name args { name type { name } } } } } adds each field's arguments with their types, { __schema { queryType { name } mutationType { name } } } reveals the names of the query and mutation roots, and { __schema { directives { name locations } } } lists available directives. The same introspection output is what you feed to TypeScript generators and editor tooling.
Does an error in one field fail the entire query?
No — GraphQL explicitly allows data and errors to come back together. The sheet annotates { user { name } } with a comment noting it expects user.name: if resolving that field throws, the response still carries every field that did resolve, and the failing field is reported in the errors array with its path and reason. This is the key difference from REST: the client must handle the data and errors channels at the same time rather than assuming one excludes the other.
Official References
Each command links to its official documentation below, so you can verify the latest usage and read deeper.
Maintained by LaoHand
Publicly updated on Jul 21, 2026, continuously proofread against official docs.
Contact Us
Wrong command or description? Send us corrections, business inquiries or product feedback by email.
Contact Us