GraphQL lets you describe exactly what data you want and get back exactly that — no over-fetching, no under-fetching. This reference covers every concept from schema definition to client queries in one place.
Quick reference
| Operation | Syntax | What it does |
|---|---|---|
| Query | query { user { name } } |
Fetch data (read) |
| Mutation | mutation { createUser(input: {…}) { id } } |
Write/change data |
| Subscription | subscription { messageAdded { text } } |
Real-time push |
| Fragment | fragment F on User { id name } |
Reusable field set |
| Variable | query($id: ID!) { user(id: $id) { name } } |
Parameterised operation |
| Alias | me: user(id: 1) { name } |
Rename response field |
| Inline fragment | ... on Admin { adminLevel } |
Conditional fields on union |
Directive @include |
field @include(if: $show) |
Conditionally include field |
Directive @skip |
field @skip(if: $hide) |
Conditionally skip field |
| Introspection | { __schema { types { name } } } |
Inspect the schema |
Schema definition language (SDL)
Scalar types
| Scalar | Description |
|---|---|
Int |
32-bit signed integer |
Float |
Double-precision float |
String |
UTF-8 character sequence |
Boolean |
true or false |
ID |
Unique identifier (serialised as String) |
Custom scalars: scalar Date, scalar JSON, scalar Upload
Type definitions
# Object type
type User {
id: ID! # ! = non-null
name: String!
email: String!
age: Int # nullable
role: Role! # enum
posts: [Post!]! # non-null list of non-null items
createdAt: String!
}
# Enum
enum Role {
ADMIN
EDITOR
VIEWER
}
# Input type (for mutations / arguments)
input CreateUserInput {
name: String!
email: String!
role: Role = VIEWER # default value
}
# Interface
interface Node {
id: ID!
}
type Post implements Node {
id: ID!
title: String!
body: String!
author: User!
tags: [String!]!
publishedAt: String
}
# Union type
union SearchResult = User | Post | Comment
# Root types
type Query {
user(id: ID!): User
users(limit: Int = 10, offset: Int = 0): [User!]!
search(query: String!): [SearchResult!]!
}
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): Boolean!
}
type Subscription {
userCreated: User!
postPublished(authorId: ID): Post!
}
Queries
Basic query
query {
users {
id
name
email
}
}
Query with arguments
query {
user(id: "42") {
name
email
posts {
title
publishedAt
}
}
}
Named query (best practice for production)
query GetUser {
user(id: "42") {
id
name
email
}
}
Query with variables
Always prefer variables over string interpolation — they're typed and injection-safe.
query GetUser($id: ID!) {
user(id: $id) {
id
name
role
}
}
Variables JSON:
{ "id": "42" }
Aliases — fetch the same field twice
query {
admin: user(id: "1") {
name
email
}
editor: user(id: "2") {
name
email
}
}
Nested queries
query {
users {
id
name
posts {
id
title
tags
author {
name
}
}
}
}
Fragments
Fragments let you extract a set of fields and reuse them across multiple queries.
fragment UserFields on User {
id
name
email
role
}
query {
user(id: "1") {
...UserFields
posts {
title
}
}
me: user(id: "99") {
...UserFields
}
}
Inline fragments — union types
query {
search(query: "graphql") {
... on User {
name
email
}
... on Post {
title
publishedAt
}
... on Comment {
body
author {
name
}
}
}
}
Directives
@include and @skip
query GetUser($id: ID!, $withPosts: Boolean!, $skipEmail: Boolean!) {
user(id: $id) {
id
name
email @skip(if: $skipEmail)
posts @include(if: $withPosts) {
title
}
}
}
Variables:
{ "id": "1", "withPosts": true, "skipEmail": false }
@deprecated (schema directive)
type User {
id: ID!
name: String!
username: String @deprecated(reason: "Use name instead")
}
Mutations
Create
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
role
}
}
Variables:
{
"input": {
"name": "Ana Petrović",
"email": "ana@example.com",
"role": "EDITOR"
}
}
Update
mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {
updateUser(id: $id, input: $input) {
id
name
email
}
}
Delete
mutation DeleteUser($id: ID!) {
deleteUser(id: $id)
}
Multiple mutations in sequence
Mutations in a single request run in order (unlike queries, which run in parallel).
mutation {
createCategory(name: "Tech") {
id
}
createPost(title: "Hello World", categoryId: "…") {
id
title
}
}
Subscriptions
subscription {
userCreated {
id
name
email
}
}
With variable:
subscription OnNewPost($authorId: ID!) {
postPublished(authorId: $authorId) {
id
title
author {
name
}
}
}
Introspection
Introspection lets you query the schema itself.
# All types
{
__schema {
types {
name
kind
}
}
}
# All fields on a type
{
__type(name: "User") {
fields {
name
type {
name
kind
ofType { name kind }
}
}
}
}
# Available queries
{
__schema {
queryType {
fields {
name
description
}
}
}
}
Server setup
Node.js — Apollo Server 4
npm install @apollo/server graphql
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
const typeDefs = `#graphql
type User {
id: ID!
name: String!
email: String!
}
type Query {
users: [User!]!
user(id: ID!): User
}
type Mutation {
createUser(name: String!, email: String!): User!
}
`;
const users = [{ id: '1', name: 'Ana', email: 'ana@example.com' }];
const resolvers = {
Query: {
users: () => users,
user: (_, { id }) => users.find(u => u.id === id) ?? null,
},
Mutation: {
createUser: (_, { name, email }) => {
const user = { id: String(users.length + 1), name, email };
users.push(user);
return user;
},
},
};
const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, { listen: { port: 4000 } });
console.log(`Server ready at ${url}`);
Node.js — Next.js App Router route handler
// app/api/graphql/route.ts
import { ApolloServer } from '@apollo/server';
import { startServerAndCreateNextHandler } from '@as-integrations/next';
const server = new ApolloServer({ typeDefs, resolvers });
const handler = startServerAndCreateNextHandler(server);
export { handler as GET, handler as POST };
Python — Strawberry
pip install strawberry-graphql[fastapi] uvicorn
import strawberry
from fastapi import FastAPI
from strawberry.fastapi import GraphQLRouter
@strawberry.type
class User:
id: str
name: str
email: str
users_db = [User(id="1", name="Ana", email="ana@example.com")]
@strawberry.type
class Query:
@strawberry.field
def users(self) -> list[User]:
return users_db
@strawberry.field
def user(self, id: str) -> User | None:
return next((u for u in users_db if u.id == id), None)
schema = strawberry.Schema(query=Query)
graphql_app = GraphQLRouter(schema)
app = FastAPI()
app.include_router(graphql_app, prefix="/graphql")
Go — gqlgen
go get github.com/99designs/gqlgen
go run github.com/99designs/gqlgen init
After running go generate ./... from your schema.graphqls:
// resolver.go
func (r *queryResolver) Users(ctx context.Context) ([]*model.User, error) {
return r.DB.FindAllUsers(ctx)
}
func (r *mutationResolver) CreateUser(
ctx context.Context, input model.CreateUserInput,
) (*model.User, error) {
return r.DB.CreateUser(ctx, input)
}
Client — fetch API (no library)
async function gql(query, variables = {}) {
const res = await fetch('/api/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, variables }),
});
const { data, errors } = await res.json();
if (errors?.length) throw new Error(errors[0].message);
return data;
}
// Usage
const data = await gql(
`query GetUser($id: ID!) { user(id: $id) { name email } }`,
{ id: '1' }
);
console.log(data.user.name);
Client — Apollo Client (React)
npm install @apollo/client graphql
// app/providers.tsx
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';
const client = new ApolloClient({
uri: '/api/graphql',
cache: new InMemoryCache(),
});
export function Providers({ children }: { children: React.ReactNode }) {
return <ApolloProvider client={client}>{children}</ApolloProvider>;
}
useQuery
import { useQuery, gql } from '@apollo/client';
const GET_USERS = gql`
query GetUsers {
users {
id
name
email
}
}
`;
function UserList() {
const { data, loading, error } = useQuery(GET_USERS);
if (loading) return <p>Loading…</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<ul>
{data.users.map((user) => (
<li key={user.id}>{user.name} — {user.email}</li>
))}
</ul>
);
}
useMutation
import { useMutation, gql } from '@apollo/client';
const CREATE_USER = gql`
mutation CreateUser($name: String!, $email: String!) {
createUser(name: $name, email: $email) {
id
name
}
}
`;
function CreateUserForm() {
const [createUser, { loading, error }] = useMutation(CREATE_USER, {
refetchQueries: ['GetUsers'], // refresh the list after create
});
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const form = new FormData(e.currentTarget);
createUser({ variables: { name: form.get('name'), email: form.get('email') } });
};
return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
<button disabled={loading}>Create</button>
{error && <p>{error.message}</p>}
</form>
);
}
useSubscription
import { useSubscription, gql } from '@apollo/client';
const USER_CREATED = gql`
subscription {
userCreated {
id
name
}
}
`;
function LiveFeed() {
const { data } = useSubscription(USER_CREATED);
return <p>Latest: {data?.userCreated.name}</p>;
}
Pagination patterns
Offset-based (simple)
query {
users(limit: 10, offset: 20) {
id
name
}
}
Cursor-based / Relay connection (recommended for large datasets)
query {
usersConnection(first: 10, after: "cursor==") {
edges {
cursor
node {
id
name
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
Common mistakes
| Mistake | Problem | Fix |
|---|---|---|
Fetching __typename manually everywhere |
Noise in queries | Apollo caches it automatically — only add when needed for union disambiguation |
| N+1 queries in resolvers | Each user triggers a DB call per post | Use DataLoader to batch and cache per request |
Returning null instead of throwing |
Client can't tell if "no data" or "error" | Throw a descriptive error from the resolver |
| Skipping named operations | Hard to debug in logs and Apollo Studio | Always name your queries/mutations |
| Putting business logic in resolvers | Hard to test, hard to reuse | Move logic to a service layer, resolvers just delegate |
@deprecated on input fields |
Not supported in SDL | Document in description instead; remove after migration |
| Exposing introspection in production | Schema leaks sensitive structure | Disable introspection on public endpoints |
FAQ
What's the difference between a query and a mutation? Semantically: queries are read-only, mutations cause side effects. Technically: queries run in parallel, mutations run in series within a single request.
Can I send multiple queries in one request?
Yes — that's called query batching. Send an array of { query, variables } objects. Apollo Client does this automatically when you configure BatchHttpLink.
What is N+1 and how do I fix it? When a list query triggers one DB call per item (e.g., fetching 100 users and then 100 separate queries for each user's posts). Fix with DataLoader: it batches all IDs collected during a request tick into a single query.
GraphQL vs REST — when to choose which? Use GraphQL when: clients need flexible queries, you have multiple clients with different data needs, or you're building a BFF (backend-for-frontend). Use REST when: you have a simple CRUD API, caching by URL matters (CDN), or your team is unfamiliar with GraphQL.
How do I handle authentication?
Pass a JWT in the Authorization: Bearer <token> header. In Apollo Server, extract it in the context function and attach the decoded user. Resolvers then access context.user to check permissions.
What is schema stitching vs federation?
Schema stitching merges multiple schemas in the gateway layer (older approach, more manual). Apollo Federation is the modern approach — each service owns its schema slice and annotates it with @key directives; the router assembles the supergraph automatically.