GraphQL Reference
GraphQL Reference
GraphQL queries, mutations, subscriptions, fragments, variables, directives, schema definition, resolvers, and the N+1 problem — with real-world patterns for Apollo, urql, and server-side implementations.
Queries — reading data
# Basic query
query {
user(id: "123") {
id
name
email
}
}
# Named query (always name your queries — required in production)
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
avatar {
url
width
height
}
}
}
# Variables (sent separately from the query)
{ "id": "123" }
# Nested queries
query GetUserWithPosts($userId: ID!, $postsLimit: Int = 10) {
user(id: $userId) {
id
name
posts(limit: $postsLimit, orderBy: CREATED_AT_DESC) {
edges {
node {
id
title
publishedAt
tags {
name
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
# Aliases — fetch same field twice with different args
query ComparePrices {
usdPrice: product(id: "42", currency: USD) {
price
currency
}
eurPrice: product(id: "42", currency: EUR) {
price
currency
}
}
Mutations — writing data
# Basic mutation
mutation {
createUser(input: { name: "Alice", email: "alice@example.com" }) {
id
name
createdAt
}
}
# Named mutation with variables (always use variables — never string-interpolate)
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
createdAt
}
}
# Variables:
{ "input": { "name": "Alice", "email": "alice@example.com", "role": "ADMIN" } }
# Multiple mutations in one request (execute sequentially, unlike query fields)
mutation BatchUpdate($userId: ID!, $postId: ID!) {
updateUser(id: $userId, input: { status: ACTIVE }) {
id
status
}
publishPost(id: $postId) {
id
publishedAt
}
}
# Mutation with error handling (union return type pattern)
mutation DeleteUser($id: ID!) {
deleteUser(id: $id) {
... on DeleteUserSuccess {
deletedId
}
... on UserNotFoundError {
message
}
... on UnauthorizedError {
message
requiredPermission
}
}
}
# This is better than throwing errors — the client can type-check responses
Fragments — reusable field sets
# Fragment definition
fragment UserFields on User {
id
name
email
avatar { url }
createdAt
}
# Use fragment in query
query GetUsers {
users {
...UserFields # spread fragment
role # fields outside fragment
}
}
# Inline fragment — conditional fields based on type (for interfaces/unions)
query GetTimeline {
feed {
id
createdAt
author { ...UserFields }
... on Post {
title
body
comments { totalCount }
}
... on Photo {
url
caption
dimensions { width height }
}
... on Video {
url
duration
thumbnailUrl
}
}
}
# __typename — get the concrete type (needed for caching + inline fragments)
query GetFeedItem($id: ID!) {
node(id: $id) {
__typename
id
... on Post { title }
... on Video { url duration }
}
}
Schema definition language (SDL)
# Scalar types
scalar Date # custom scalar (add coercion in resolver)
scalar Upload # file uploads (apollo-upload-server)
scalar JSON # arbitrary JSON blob
# Object type
type User {
id: ID! # ! = non-null
name: String!
email: String!
role: UserRole! # enum
posts: [Post!]! # list of non-null Post, list itself non-null
profile: Profile # nullable (omitted = null)
createdAt: Date!
}
# Enum
enum UserRole {
ADMIN
EDITOR
VIEWER
}
# Interface — shared fields
interface Node {
id: ID!
}
type Post implements Node {
id: ID!
title: String!
body: String!
author: User!
publishedAt: Date
}
# Union — one of several types (no shared fields)
union SearchResult = User | Post | Product
# Input type — for mutation arguments
input CreateUserInput {
name: String!
email: String!
role: UserRole = VIEWER # default value
sendWelcomeEmail: Boolean = true
}
# Root types
type Query {
user(id: ID!): User
users(limit: Int = 20, offset: Int = 0): [User!]!
searchUsers(query: String!): [User!]!
}
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): DeleteUserResult!
}
type Subscription {
userCreated: User!
messageAdded(roomId: ID!): Message!
}
Directives — built-in and custom
# Built-in directives in queries
query GetProfile($showEmail: Boolean!, $preview: Boolean = false) {
user(id: "123") {
name
email @include(if: $showEmail) # include field only if true
bio @skip(if: $preview) # skip field if true
avatar {
url
@deprecated(reason: "Use avatarUrl instead — remove after 2026-06-01")
}
}
}
# @defer — stream parts of the response as they resolve (experimental)
query GetUserWithSlowData {
user(id: "123") {
name
email
... on User @defer { # these fields sent in separate chunk
followers { count }
analytics { views }
}
}
}
# Schema directives (server-side)
# @deprecated — mark a field as deprecated
type User {
login: String! @deprecated(reason: "Use 'username' field instead")
username: String!
}
# Custom schema directive (e.g. @auth)
directive @auth(requires: UserRole = ADMIN) on FIELD_DEFINITION | OBJECT
type Mutation {
deleteUser(id: ID!): Boolean! @auth(requires: ADMIN)
createPost(input: CreatePostInput!): Post! @auth(requires: EDITOR)
}
# @cacheControl (Apollo Server)
type Product @cacheControl(maxAge: 3600) {
id: ID!
name: String!
price: Float! @cacheControl(maxAge: 60) # price refreshes more often
}
Resolvers and the N+1 problem
# Basic resolver (Node.js / Apollo Server)
const resolvers = {
Query: {
user: async (_, { id }, context) => {
return context.db.users.findById(id);
},
users: async (_, { limit, offset }, context) => {
return context.db.users.findAll({ limit, offset });
},
},
Mutation: {
createUser: async (_, { input }, context) => {
if (!context.user) throw new GraphQLError("Unauthorized", {
extensions: { code: "UNAUTHENTICATED" },
});
return context.db.users.create(input);
},
},
User: {
// Field resolver — called for each User in the response
posts: async (parent, { limit }, context) => {
return context.db.posts.findByAuthorId(parent.id, limit);
},
},
};
// N+1 problem:
// Query 100 users → 1 query for users + 100 queries for posts (one per user)
// Fix: DataLoader (batch + cache)
import DataLoader from "dataloader";
const postsByAuthorLoader = new DataLoader(async (authorIds) => {
const posts = await db.posts.findByAuthorIds(authorIds); // ONE query
// Return in same order as authorIds (DataLoader requirement)
return authorIds.map(id => posts.filter(p => p.authorId === id));
});
// In resolver:
User: {
posts: (parent, _, context) =>
context.loaders.postsByAuthor.load(parent.id), // batched automatically
}
The N+1 problem is the most common GraphQL performance issue. Every field resolver that queries a database needs a DataLoader. Failing to do this makes GraphQL APIs slower than REST as data grows.
Apollo Client — queries, mutations, cache
# Apollo Client setup (React)
import { ApolloClient, InMemoryCache, ApolloProvider, gql, useQuery, useMutation } from "@apollo/client";
const client = new ApolloClient({
uri: "/graphql",
cache: new InMemoryCache(),
defaultOptions: {
watchQuery: { errorPolicy: "all" },
},
});
# useQuery hook
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) { id name email }
}
`;
function UserProfile({ userId }) {
const { loading, error, data } = useQuery(GET_USER, {
variables: { id: userId },
skip: !userId, # don't run if userId is falsy
fetchPolicy: "cache-and-network", # show cached + update in background
});
if (loading) return ;
if (error) return ;
return {data.user.name};
}
# useMutation hook
const CREATE_USER = gql`mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) { id name }
}`;
function CreateUserForm() {
const [createUser, { loading, error }] = useMutation(CREATE_USER, {
update(cache, { data: { createUser } }) {
cache.modify({
fields: {
users(existing = []) {
const ref = cache.writeFragment({
data: createUser,
fragment: gql`fragment NewUser on User { id name }`,
});
return [...existing, ref];
},
},
});
},
});
return (
);
}
🔍 Free tool: npm Package Health Checker — check graphql-js, Apollo Client, and other GraphQL packages for known CVEs and maintenance status.
Founded
2023 in London, UK
Contact
hello@releaserun.com