A query language for APIs that allows clients to request exactly the data they need, paired with Apollo - a comprehensive GraphQL platform and client library.
GraphQL is a query language and runtime for APIs that provides a complete description of the data in your API. Unlike REST where you get fixed data structures, GraphQL lets clients specify exactly what data they need.
- Single Endpoint: All queries go to one endpoint (typically
/graphql) - Strongly Typed: Schema defines all possible data and operations
- Client-Specified Queries: Clients request only the fields they need
- Hierarchical: Queries mirror the shape of returned data
- Introspective: Schema is queryable and self-documenting
- No Over/Under-fetching: Get exactly what you ask for, nothing more or less
Apollo is the most popular GraphQL implementation, providing:
- Apollo Server: GraphQL server for Node.js
- Apollo Client: State management and data fetching for frontend
- Apollo Studio: Tools for testing, monitoring, and documenting
┌──────────────────────────────┐
│ CLIENT │
│ React / Mobile / Web App │
└───────────────┬──────────────┘
│
Query / Mutation / Subscription
│
▼
┌──────────────────────────────┐
│ GRAPHQL SERVER │
│ /graphql │
└───────────────┬──────────────┘
│
Parse Query
│
▼
┌──────────────────────────────┐
│ SCHEMA │
│ Types / Queries / Mutations │
└───────────────┬──────────────┘
│
Field-by-field execution
│
▼
┌──────────────────────────────┐
│ RESOLVERS │
│ functions per field │
└───────────────┬──────────────┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ DATABASE │ │ REST API │ │ CACHE │
└──────────┘ └──────────┘ └──────────┘
│
▼
┌──────────────────────────────┐
│ RESULT ASSEMBLER │
│ merges resolver outputs │
└───────────────┬──────────────┘
│
▼
┌──────────────────────────────┐
│ GRAPHQL RESPONSE │
│ { data, errors } │
└───────────────┬──────────────┘
│
▼
┌──────────────────────────────┐
│ CLIENT │
│ UI updates (no overfetching) │
└──────────────────────────────┘
Query received
│
▼
Validate against schema
│
▼
Break into fields
│
▼
Run resolver per field
│
▼
Fetch data (DB / API / cache)
│
▼
Combine results
│
▼
Return JSON
Schema
│
├── Types
│ └── User, Post, Comment
│
├── Query (READ)
│ └── user(id)
│
├── Mutation (WRITE)
│ └── createUser()
│
└── Subscription (REAL-TIME)
└── userUpdated()
| Term | Definition |
|---|---|
| Schema | Definition of types, queries, mutations, and subscriptions |
| Query | Read operation to fetch data (like GET in REST) |
| Mutation | Write operation to modify data (like POST/PUT/DELETE in REST) |
| Subscription | Real-time updates via WebSocket connection |
| Resolver | Function that returns data for a field in the schema |
| Type | Definition of an object structure with fields |
| Field | Property on a type that can be queried |
| Argument | Parameter passed to a field to modify its behavior |
| Fragment | Reusable piece of query logic |
| Directive | Modifier that affects query execution (@include, @skip) |
| Variables | Dynamic values passed to queries/mutations |
The schema is the contract between client and server, defining all available types and operations.
GraphQL has five built-in scalar types that represent primitive values.
# Scalar Types (primitive values)
type User {
id: ID! # Unique identifier, ! means non-nullable (required)
name: String! # Text data
age: Int # Integer number (nullable)
score: Float # Decimal number
isActive: Boolean! # true or false
}Object types define the structure of your data models.
# Object Type with relationships
type User {
id: ID!
name: String!
email: String!
posts: [Post!]! # Array of Posts (non-null array of non-null items)
profile: Profile # One-to-one relationship (nullable)
createdAt: String!
}
type Post {
id: ID!
title: String!
content: String!
author: User! # Reference to User type
comments: [Comment!]!
published: Boolean!
}
type Comment {
id: ID!
text: String!
author: User!
post: Post!
}Input types are used for passing complex objects as arguments.
# Input type for mutations
input CreateUserInput {
name: String!
email: String!
age: Int
}
input UpdateUserInput {
name: String
email: String
age: Int
}
input PostFilter {
published: Boolean
authorId: ID
searchTerm: String
}Enums define a set of allowed values for a field.
enum Role {
ADMIN
USER
MODERATOR
GUEST
}
enum PostStatus {
DRAFT
PUBLISHED
ARCHIVED
}
type User {
id: ID!
name: String!
role: Role! # Must be one of the enum values
}Interfaces define common fields that multiple types share.
# Interface that other types can implement
interface Node {
id: ID!
createdAt: String!
}
type User implements Node {
id: ID!
createdAt: String!
name: String!
email: String!
}
type Post implements Node {
id: ID!
createdAt: String!
title: String!
content: String!
}Unions allow a field to return one of several types.
# Union type for search results
union SearchResult = User | Post | Comment
type Query {
search(term: String!): [SearchResult!]!
}
# Query with union
query {
search(term: "graphql") {
... on User {
name
email
}
... on Post {
title
content
}
... on Comment {
text
}
}
}The schema must define at least a Query type. Mutation and Subscription are optional.
# Root Query type (required) - all read operations
type Query {
user(id: ID!): User
users(limit: Int, offset: Int): [User!]!
post(id: ID!): Post
posts(filter: PostFilter): [Post!]!
me: User
}
# Root Mutation type (optional) - all write operations
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): Boolean!
createPost(title: String!, content: String!): Post!
}
# Root Subscription type (optional) - real-time updates
type Subscription {
userCreated: User!
postPublished: Post!
commentAdded(postId: ID!): Comment!
}Queries are used to fetch data from the server. They specify exactly which fields you want.
The simplest form requests specific fields from a type.
# Fetch single user
query {
user(id: "123") {
id
name
email
}
}
# Response
{
"data": {
"user": {
"id": "123",
"name": "John Doe",
"email": "john@example.com"
}
}
}You can request related data in a single query, following relationships.
# Fetch user with their posts and comments
query {
user(id: "123") {
id
name
posts {
id
title
comments {
id
text
author {
name
}
}
}
}
}Arguments allow you to filter, paginate, or customize the data returned.
# Query with multiple arguments
query {
posts(
limit: 10
offset: 0
filter: { published: true }
sortBy: "createdAt"
order: DESC
) {
id
title
author {
name
}
}
}Naming queries helps with debugging and makes code more maintainable.
# Named query
query GetUserProfile {
user(id: "123") {
id
name
email
}
}
# Multiple queries in one request
query {
user1: user(id: "123") {
name
}
user2: user(id: "456") {
name
}
}Variables make queries dynamic and reusable. They're defined at the top and used with $ prefix.
# Query definition with variables
query GetUser($userId: ID!, $includeEmail: Boolean = false) {
user(id: $userId) {
id
name
email @include(if: $includeEmail)
}
}
# Variables (sent separately)
{
"userId": "123",
"includeEmail": true
}Aliases let you rename fields in the response or query the same field with different arguments.
query {
adminUsers: users(role: ADMIN) {
id
name
}
regularUsers: users(role: USER) {
id
name
}
fullName: name # Rename 'name' field to 'fullName'
}Fragments are reusable pieces of query logic that reduce duplication.
# Define fragment
fragment UserInfo on User {
id
name
email
createdAt
}
# Use fragment
query {
user(id: "123") {
...UserInfo
posts {
id
title
}
}
allUsers {
...UserInfo
}
}
# Inline fragment for type-specific fields
query {
search(term: "graphql") {
... on User {
name
email
}
... on Post {
title
content
}
}
}Directives modify query execution based on conditions.
query GetUser($userId: ID!, $withPosts: Boolean!, $skipComments: Boolean!) {
user(id: $userId) {
id
name
posts @include(if: $withPosts) { # Include only if true
id
title
comments @skip(if: $skipComments) { # Skip if true
id
text
}
}
}
}Mutations modify data on the server (create, update, delete operations). They're similar to POST, PUT, DELETE in REST.
Mutations can return the created/updated data, allowing you to get fresh data immediately.
# Create user
mutation {
createUser(input: {
name: "John Doe"
email: "john@example.com"
age: 30
}) {
id
name
email
createdAt
}
}
# Response
{
"data": {
"createUser": {
"id": "123",
"name": "John Doe",
"email": "john@example.com",
"createdAt": "2024-01-01T00:00:00Z"
}
}
}Using variables makes mutations safer and prevents injection attacks.
# Mutation definition
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
}
}
# Variables
{
"input": {
"name": "John Doe",
"email": "john@example.com",
"age": 30
}
}Update operations typically require an ID and partial data.
mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {
updateUser(id: $id, input: $input) {
id
name
email
updatedAt
}
}
# Variables
{
"id": "123",
"input": {
"name": "Jane Doe"
}
}Delete operations often return a boolean or the deleted object.
mutation DeleteUser($id: ID!) {
deleteUser(id: $id) # Returns boolean
}
# Or return deleted object
mutation DeleteUser($id: ID!) {
deleteUser(id: $id) {
id
name
deletedAt
}
}You can execute multiple mutations in sequence (not parallel).
mutation {
user1: createUser(input: { name: "User 1", email: "user1@example.com" }) {
id
}
user2: createUser(input: { name: "User 2", email: "user2@example.com" }) {
id
}
}Subscriptions enable real-time updates via WebSocket. Useful for chat apps, live feeds, notifications.
Client subscribes to events and receives updates when they occur.
# Subscribe to new comments on a post
subscription {
commentAdded(postId: "123") {
id
text
author {
name
}
createdAt
}
}
# Server pushes data when new comment is added
{
"data": {
"commentAdded": {
"id": "789",
"text": "Great post!",
"author": {
"name": "Jane Doe"
},
"createdAt": "2024-01-01T12:00:00Z"
}
}
}Variables allow dynamic subscription parameters.
subscription OnCommentAdded($postId: ID!) {
commentAdded(postId: $postId) {
id
text
author {
name
}
}
}
# Variables
{
"postId": "123"
}# User status updates
subscription {
userStatusChanged {
id
name
isOnline
}
}
# New messages in chat
subscription {
messageReceived(chatId: "abc") {
id
text
sender {
name
}
timestamp
}
}
# Live data updates
subscription {
stockPriceUpdated(symbol: "AAPL") {
symbol
price
change
timestamp
}
}Apollo Server is a production-ready GraphQL server for Node.js.
// Install: npm install @apollo/server graphql
const { ApolloServer } = require('@apollo/server');
const { startStandaloneServer } = require('@apollo/server/standalone');
// Define schema
const typeDefs = `
type User {
id: ID!
name: String!
email: String!
}
type Query {
users: [User!]!
user(id: ID!): User
}
type Mutation {
createUser(name: String!, email: String!): User!
}
`;
// Sample data
const users = [
{ id: '1', name: 'John Doe', email: 'john@example.com' },
{ id: '2', name: 'Jane Smith', email: 'jane@example.com' }
];
// Define resolvers
const resolvers = {
Query: {
users: () => users,
user: (parent, args) => users.find(u => u.id === args.id)
},
Mutation: {
createUser: (parent, args) => {
const newUser = {
id: String(users.length + 1),
name: args.name,
email: args.email
};
users.push(newUser);
return newUser;
}
}
};
// Create server
const server = new ApolloServer({
typeDefs,
resolvers
});
// Start server
startStandaloneServer(server, {
listen: { port: 4000 }
}).then(({ url }) => {
console.log(`Server ready at ${url}`);
});Context provides shared data to all resolvers (auth, database, etc.).
const resolvers = {
Query: {
me: (parent, args, context) => {
// Access context (user, db, etc.)
if (!context.user) {
throw new Error('Not authenticated');
}
return context.user;
},
users: async (parent, args, context) => {
// Access database from context
return await context.db.users.findMany();
}
},
// Field resolver for nested data
User: {
posts: async (parent, args, context) => {
// parent contains the User object
return await context.db.posts.findMany({
where: { authorId: parent.id }
});
}
}
};
// Server with context
const server = new ApolloServer({
typeDefs,
resolvers
});
startStandaloneServer(server, {
context: async ({ req }) => {
// Get user from token
const token = req.headers.authorization || '';
const user = await getUserFromToken(token);
return {
user,
db: prisma // Database client
};
}
});Data sources abstract data fetching logic and provide caching.
const { RESTDataSource } = require('@apollo/datasource-rest');
class UsersAPI extends RESTDataSource {
baseURL = 'https://api.example.com/';
async getUser(id) {
return this.get(`users/${id}`);
}
async getUsers() {
return this.get('users');
}
async createUser(user) {
return this.post('users', { body: user });
}
}
// Use in resolvers
const resolvers = {
Query: {
user: (parent, { id }, { dataSources }) => {
return dataSources.usersAPI.getUser(id);
}
}
};
// Add to server
const server = new ApolloServer({
typeDefs,
resolvers
});
startStandaloneServer(server, {
context: async () => {
return {
dataSources: {
usersAPI: new UsersAPI()
}
};
}
});const { ApolloServer } = require('@apollo/server');
const { expressMiddleware } = require('@apollo/server/express4');
const { ApolloServerPluginDrainHttpServer } = require('@apollo/server/plugin/drainHttpServer');
const { createServer } = require('http');
const { makeExecutableSchema } = require('@graphql-tools/schema');
const { WebSocketServer } = require('ws');
const { useServer } = require('graphql-ws/lib/use/ws');
const express = require('express');
const { PubSub } = require('graphql-subscriptions');
const pubsub = new PubSub();
const typeDefs = `
type Message {
id: ID!
text: String!
sender: String!
}
type Query {
messages: [Message!]!
}
type Mutation {
sendMessage(text: String!, sender: String!): Message!
}
type Subscription {
messageAdded: Message!
}
`;
const resolvers = {
Mutation: {
sendMessage: (parent, { text, sender }) => {
const message = { id: Date.now(), text, sender };
pubsub.publish('MESSAGE_ADDED', { messageAdded: message });
return message;
}
},
Subscription: {
messageAdded: {
subscribe: () => pubsub.asyncIterator(['MESSAGE_ADDED'])
}
}
};
// Create schema and servers
const schema = makeExecutableSchema({ typeDefs, resolvers });
const app = express();
const httpServer = createServer(app);
const wsServer = new WebSocketServer({
server: httpServer,
path: '/graphql'
});
const serverCleanup = useServer({ schema }, wsServer);
const server = new ApolloServer({
schema,
plugins: [
ApolloServerPluginDrainHttpServer({ httpServer }),
{
async serverWillStart() {
return {
async drainServer() {
await serverCleanup.dispose();
}
};
}
}
]
});
await server.start();
app.use('/graphql', express.json(), expressMiddleware(server));
httpServer.listen(4000);Apollo Client is a comprehensive state management library for JavaScript that manages both local and remote data with GraphQL.
// Install: npm install @apollo/client graphql
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';
// Create client
const client = new ApolloClient({
uri: 'http://localhost:4000/graphql',
cache: new InMemoryCache()
});
// Wrap app with provider
function App() {
return (
<ApolloProvider client={client}>
<YourApp />
</ApolloProvider>
);
}import { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
// HTTP link
const httpLink = createHttpLink({
uri: 'http://localhost:4000/graphql'
});
// Auth link to add token to headers
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem('token');
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : ''
}
};
});
// Create client with auth
const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache()
});The useQuery hook executes a GraphQL query and returns loading, error, and data states.
import { useQuery, gql } from '@apollo/client';
// Define query
const GET_USERS = gql`
query GetUsers {
users {
id
name
email
}
}
`;
function Users() {
const { loading, error, data, refetch } = useQuery(GET_USERS);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
{data.users.map(user => (
<div key={user.id}>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
))}
<button onClick={() => refetch()}>Refresh</button>
</div>
);
}const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
posts {
id
title
}
}
}
`;
function UserProfile({ userId }) {
const { loading, error, data } = useQuery(GET_USER, {
variables: { id: userId },
// Additional options
fetchPolicy: 'cache-first', // cache-first, network-only, cache-and-network
pollInterval: 5000, // Poll every 5 seconds
skip: !userId // Skip query if no userId
});
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
<h1>{data.user.name}</h1>
<p>{data.user.email}</p>
<h2>Posts</h2>
{data.user.posts.map(post => (
<div key={post.id}>{post.title}</div>
))}
</div>
);
}The useMutation hook returns a function to trigger the mutation and tracks its state.
import { useMutation, gql } from '@apollo/client';
const CREATE_USER = gql`
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
}
}
`;
function CreateUserForm() {
const [createUser, { data, loading, error }] = useMutation(CREATE_USER, {
// Refetch queries after mutation
refetchQueries: [{ query: GET_USERS }],
// Or update cache manually
update(cache, { data: { createUser } }) {
cache.modify({
fields: {
users(existingUsers = []) {
const newUserRef = cache.writeFragment({
data: createUser,
fragment: gql`
fragment NewUser on User {
id
name
email
}
`
});
return [...existingUsers, newUserRef];
}
}
});
},
// Handle completion
onCompleted: (data) => {
console.log('User created:', data.createUser);
},
onError: (error) => {
console.error('Error creating user:', error);
}
});
const handleSubmit = (e) => {
e.preventDefault();
const formData = new FormData(e.target);
createUser({
variables: {
input: {
name: formData.get('name'),
email: formData.get('email')
}
}
});
};
return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
<button type="submit" disabled={loading}>
{loading ? 'Creating...' : 'Create User'}
</button>
{error && <p>Error: {error.message}</p>}
{data && <p>User created: {data.createUser.name}</p>}
</form>
);
}The useSubscription hook listens to real-time updates via WebSocket.
import { useSubscription, gql } from '@apollo/client';
const MESSAGE_ADDED = gql`
subscription OnMessageAdded {
messageAdded {
id
text
sender
timestamp
}
}
`;
function Messages() {
const { data, loading } = useSubscription(MESSAGE_ADDED, {
onSubscriptionData: ({ client, subscriptionData }) => {
console.log('New message:', subscriptionData.data.messageAdded);
}
});
if (loading) return <p>Connecting...</p>;
return (
<div>
{data && (
<div>
<strong>{data.messageAdded.sender}:</strong> {data.messageAdded.text}
</div>
)}
</div>
);
}import { ApolloClient, InMemoryCache, split, HttpLink } from '@apollo/client';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { getMainDefinition } from '@apollo/client/utilities';
import { createClient } from 'graphql-ws';
// HTTP link for queries and mutations
const httpLink = new HttpLink({
uri: 'http://localhost:4000/graphql'
});
// WebSocket link for subscriptions
const wsLink = new GraphQLWsLink(
createClient({
url: 'ws://localhost:4000/graphql'
})
);
// Split based on operation type
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink
);
const client = new ApolloClient({
link: splitLink,
cache: new InMemoryCache()
});Apollo Client's cache is a normalized store that eliminates data duplication and keeps your UI consistent.
import { InMemoryCache } from '@apollo/client';
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
// Custom read/merge logic for fields
users: {
merge(existing = [], incoming) {
return [...existing, ...incoming];
}
}
}
},
User: {
// Define how objects are identified
keyFields: ['id'],
fields: {
// Local-only field
isSelected: {
read(existing = false) {
return existing;
}
}
}
}
}
});import { useApolloClient, gql } from '@apollo/client';
function Component() {
const client = useApolloClient();
const readUser = () => {
const data = client.readQuery({
query: gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`,
variables: { id: '123' }
});
console.log(data);
};
const readFragment = () => {
const user = client.readFragment({
id: 'User:123', // Cache ID
fragment: gql`
fragment UserName on User {
name
email
}
`
});
console.log(user);
};
return <button onClick={readUser}>Read from Cache</button>;
}import { useApolloClient, gql } from '@apollo/client';
function Component() {
const client = useApolloClient();
const updateCache = () => {
client.writeQuery({
query: gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`,
variables: { id: '123' },
data: {
user: {
__typename: 'User',
id: '123',
name: 'Updated Name',
email: 'updated@example.com'
}
}
});
};
const updateFragment = () => {
client.writeFragment({
id: 'User:123',
fragment: gql`
fragment UserName on User {
name
}
`,
data: {
name: 'New Name'
}
});
};
return <button onClick={updateCache}>Update Cache</button>;
}Apollo Client can manage local state alongside remote data.
// Define local schema
const typeDefs = gql`
extend type User {
isSelected: Boolean!
}
extend type Query {
selectedUsers: [User!]!
}
`;
const cache = new InMemoryCache({
typePolicies: {
User: {
fields: {
isSelected: {
read(existing = false) {
return existing;
}
}
}
},
Query: {
fields: {
selectedUsers: {
read(_, { cache }) {
// Read all users and filter selected
const users = cache.readQuery({
query: gql`
query {
users {
id
name
isSelected @client
}
}
`
});
return users?.users.filter(u => u.isSelected) || [];
}
}
}
}
}
});
// Use in component
function UserList() {
const { data } = useQuery(gql`
query GetUsers {
users {
id
name
isSelected @client
}
}
`);
const [toggleSelect] = useMutation(gql`
mutation ToggleSelect($id: ID!) {
toggleSelect(id: $id) @client
}
`);
return (
<div>
{data?.users.map(user => (
<div key={user.id}>
{user.name}
<input
type="checkbox"
checked={user.isSelected}
onChange={() => toggleSelect({ variables: { id: user.id } })}
/>
</div>
))}
</div>
);
}Proper error handling improves user experience and helps debug issues.
import { useQuery, gql } from '@apollo/client';
function Component() {
const { loading, error, data } = useQuery(GET_USERS, {
onError: (error) => {
console.error('Query error:', error);
// Log to error tracking service
}
});
if (error) {
// GraphQL errors
if (error.graphQLErrors.length > 0) {
error.graphQLErrors.forEach(({ message, locations, path }) => {
console.log(GraphQL error: ${message});
});
}
// Network errors
if (error.networkError) {
console.log('Network error:', error.networkError);
}
return <p>Error loading data: {error.message}</p>;
}
return <div>{/* Render data */}</div>;
}import { ApolloClient, InMemoryCache, ApolloLink } from '@apollo/client';
import { onError } from '@apollo/client/link/error';
// Error link
const errorLink = onError(({ graphQLErrors, networkError, operation }) => {
if (graphQLErrors) {
graphQLErrors.forEach(({ message, locations, path, extensions }) => {
console.log(`[GraphQL error]: Message: ${message}, Path: ${path}`);
// Handle specific error codes
if (extensions.code === 'UNAUTHENTICATED') {
// Redirect to login
}
});
}
if (networkError) {
console.log(`[Network error]: ${networkError}`);
}
});
const client = new ApolloClient({
link: ApolloLink.from([errorLink, httpLink]),
cache: new InMemoryCache()
});const { GraphQLError } = require('graphql');
const { ApolloServerErrorCode } = require('@apollo/server/errors');
const resolvers = {
Query: {
user: async (parent, { id }, context) => {
if (!context.user) {
throw new GraphQLError('You must be logged in', {
extensions: {
code: 'UNAUTHENTICATED'
}
});
}
const user = await context.db.user.findUnique({ where: { id } });
if (!user) {
throw new GraphQLError('User not found', {
extensions: {
code: 'NOT_FOUND',
userId: id
}
});
}
return user;
}
},
Mutation: {
createUser: async (parent, { input }, context) => {
try {
return await context.db.user.create({ data: input });
} catch (error) {
if (error.code === 'P2002') {
throw new GraphQLError('Email already exists', {
extensions: {
code: 'DUPLICATE_EMAIL',
field: 'email'
}
});
}
throw error;
}
}
}
};
// Custom error formatter
const server = new ApolloServer({
typeDefs,
resolvers,
formatError: (formattedError, error) => {
// Don't expose internal errors to client
if (formattedError.extensions.code === 'INTERNAL_SERVER_ERROR') {
return {
message: 'Something went wrong',
extensions: {
code: 'INTERNAL_SERVER_ERROR'
}
};
}
return formattedError;
}
});Pagination prevents loading too much data at once. GraphQL supports multiple pagination strategies.
Simple but less efficient for large datasets.
# Schema
type Query {
users(offset: Int, limit: Int): [User!]!
usersConnection(offset: Int, limit: Int): UsersConnection!
}
type UsersConnection {
nodes: [User!]!
totalCount: Int!
}
# Query
query GetUsers($offset: Int, $limit: Int) {
usersConnection(offset: $offset, limit: $limit) {
nodes {
id
name
}
totalCount
}
}// React implementation
function UserList() {
const [page, setPage] = useState(0);
const limit = 10;
const { loading, data } = useQuery(GET_USERS, {
variables: {
offset: page * limit,
limit
}
});
const totalPages = data ? Math.ceil(data.usersConnection.totalCount / limit) : 0;
return (
<div>
{data?.usersConnection.nodes.map(user => (
<div key={user.id}>{user.name}</div>
))}
<button disabled={page === 0} onClick={() => setPage(p => p - 1)}>
Previous
</button>
<span>Page {page + 1} of {totalPages}</span>
<button disabled={page >= totalPages - 1} onClick={() => setPage(p => p + 1)}>
Next
</button>
</div>
);
}More efficient and handles data changes better.
# Schema
type Query {
users(first: Int, after: String, last: Int, before: String): UserConnection!
}
type UserConnection {
edges: [UserEdge!]!
pageInfo: PageInfo!
}
type UserEdge {
node: User!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
# Query
query GetUsers($first: Int, $after: String) {
users(first: $first, after: $after) {
edges {
node {
id
name
}
cursor
}
pageInfo {
hasNextPage
endCursor
}
}
}// Apollo Client with fetchMore
function UserList() {
const { loading, data, fetchMore } = useQuery(GET_USERS, {
variables: { first: 10 }
});
const loadMore = () => {
fetchMore({
variables: {
after: data.users.pageInfo.endCursor
},
updateQuery: (prev, { fetchMoreResult }) => {
if (!fetchMoreResult) return prev;
return {
users: {
...fetchMoreResult.users,
edges: [...prev.users.edges, ...fetchMoreResult.users.edges]
}
};
}
});
};
return (
<div>
{data?.users.edges.map(({ node }) => (
<div key={node.id}>{node.name}</div>
))}
{data?.users.pageInfo.hasNextPage && (
<button onClick={loadMore} disabled={loading}>
Load More
</button>
)}
</div>
);
}import { useQuery } from '@apollo/client';
import InfiniteScroll from 'react-infinite-scroll-component';
function InfiniteUserList() {
const { data, loading, fetchMore } = useQuery(GET_USERS, {
variables: { first: 20 }
});
const loadMore = () => {
fetchMore({
variables: {
after: data.users.pageInfo.endCursor
}
});
};
const users = data?.users.edges.map(edge => edge.node) || [];
return (
<InfiniteScroll
dataLength={users.length}
next={loadMore}
hasMore={data?.users.pageInfo.hasNextPage || false}
loader={<h4>Loading...</h4>}
>
{users.map(user => (
<div key={user.id}>{user.name}</div>
))}
</InfiniteScroll>
);
}Schema Design
- Keep schemas simple and intuitive
- Use descriptive names for types and fields
- Make fields non-nullable (
!) when appropriate - Use Input types for complex mutation arguments
- Document schema with descriptions
- Version schema carefully to avoid breaking changes
Performance
- Implement DataLoader to solve N+1 query problem
- Use field-level caching strategically
- Limit query depth and complexity
- Implement pagination for large lists
- Use fragments to reduce query size
- Enable query batching when appropriate
Security
- Validate all inputs on server
- Implement proper authentication and authorization
- Use persisted queries in production
- Limit query depth to prevent abuse
- Implement rate limiting
- Sanitize error messages (don't leak internals)
Client-Side
- Use fragments to share field selections
- Leverage Apollo Client cache effectively
- Implement optimistic UI updates for better UX
- Handle loading and error states properly
- Use variables instead of string interpolation
- Normalize cache for efficient updates
Development
- Use TypeScript for type safety
- Generate types from schema (graphql-codegen)
- Write tests for resolvers
- Document API with schema descriptions
- Use Apollo Studio for monitoring
- Version control your schema
Over-fetching / Under-fetching
- Requesting too many fields you don't need
- Not requesting enough fields, requiring additional queries
- Solution: Request exactly what you need per component
N+1 Problem
- Making separate database queries for each item in a list
- Example: Fetching author for each post separately
- Solution: Use DataLoader to batch requests
Not Using Fragments
- Duplicating field selections across queries
- Makes refactoring difficult
- Solution: Extract common fields into fragments
Cache Invalidation Issues
- Not updating cache after mutations
- Stale data displayed to users
- Solution: Use
refetchQueries,update, or cache eviction
Poor Error Handling
- Not handling GraphQL errors differently from network errors
- Showing cryptic error messages to users
- Solution: Implement proper error boundaries and user-friendly messages
Exposing Sensitive Data
- Returning sensitive data in errors
- Not implementing field-level authorization
- Solution: Filter resolver responses based on user permissions
Ignoring Schema Design
- Using verbs instead of nouns for types
- Inconsistent naming conventions
- Solution: Follow GraphQL schema design best practices
Not Implementing Pagination
- Returning entire lists without pagination
- Poor performance for large datasets
- Solution: Always paginate lists
Mutations Without Return Values
- Not returning updated data from mutations
- Requiring additional queries to get fresh data
- Solution: Return the affected objects from mutations
Misusing Subscriptions
- Using subscriptions when polling would suffice
- Not handling subscription cleanup
- Solution: Use subscriptions only for truly real-time features
| Feature | GraphQL | REST |
|---|---|---|
| Endpoints | Single endpoint | Multiple endpoints |
| Data Fetching | Request exactly what you need | Fixed data structure |
| Over/Under-fetching | No | Yes |
| Versioning | Evolve schema without versions | Requires versioning (v1, v2) |
| Documentation | Self-documenting (introspection) | Requires separate docs |
| Caching | Complex (cache normalization) | Simple (HTTP caching) |
| Learning Curve | Steeper | Easier |
# Query (read)
query {
users { id name }
}
# Mutation (write)
mutation {
createUser(input: { name: "John" }) { id }
}
# Subscription (real-time)
subscription {
userCreated { id name }
}
# With variables
query GetUser($id: ID!) {
user(id: $id) { name }
}
# With fragments
fragment UserFields on User {
id
name
email
}
query {
user(id: "1") {
...UserFields
}
}// Query
const { data, loading, error, refetch } = useQuery(QUERY);
// Mutation
const [mutate, { data, loading, error }] = useMutation(MUTATION);
// Subscription
const { data, loading } = useSubscription(SUBSCRIPTION);
// Lazy query
const [executeQuery, { data }] = useLazyQuery(QUERY);// Read
client.readQuery({ query, variables });
client.readFragment({ id, fragment });
// Write
client.writeQuery({ query, variables, data });
client.writeFragment({ id, fragment, data });
// Modify
cache.modify({
fields: {
users(existing) {
return [...existing, newUser];
}
}
});
// Evict
cache.evict({ id: 'User:123' });
cache.gc();| Policy | Description |
|---|---|
cache-first |
Use cache if available, else fetch (default) |
cache-only |
Use cache only, never fetch |
cache-and-network |
Use cache immediately, then fetch and update |
network-only |
Always fetch, update cache |
no-cache |
Always fetch, don't update cache |
GraphQL & Apollo: Complete Reference Guide
Table of Contents
1. Introduction to GraphQL
GraphQL is a query language and runtime for APIs that enables clients to request exactly the data they need. It provides a standard language for defining data, data types, and related data queries.
Key Features
GraphQL vs REST
GraphQL solves several REST API challenges:
The GraphQL Workflow
Client defines what data it needs ↓ Sends query to single GraphQL endpoint ↓ Server validates query against schema ↓ Resolvers fetch data from various sources ↓ Data assembled into exact shape requested ↓ Response returned as JSONUnlike REST, GraphQL has only one HTTP endpoint (usually
/graphql) and one HTTP method for most operations (POST). The actual operation (query, mutation, subscription) is specified inside the GraphQL document, not by the HTTP verb.queryGETorPOSTGETmutationPOSTPOST,PUT,PATCH,DELETEsubscriptionWebSocket(or SSE)2. Core Concepts
2.1 Schema
A GraphQL schema defines the structure and types of data that can be queried or mutated. It serves as a contract between server and clients, providing an abstraction layer that hides backend implementation details.
Schema Components:
2.2 Types
GraphQL has several type categories:
Scalar Types (primitive values):
Int: Signed 32-bit integerFloat: Signed double-precision floating-point valueString: UTF-8 character sequenceBoolean: true or falseID: Unique identifier (serialized as String)Object Types: User-defined types with fields
Enum: Limited set of values
Interface: Shared fields across types
Union: Field can be one of multiple types
Input Type: Structured input for mutations
2.3 Fields & Arguments
Fields are the data access points on types:
Arguments are parameters for fields:
2.4 Directives
Directives modify execution behavior:
@include(if: Boolean): Include field if condition is true@skip(if: Boolean): Skip field if condition is true@deprecated(reason: String): Mark field as deprecated2.5 Fragments
Fragments are reusable pieces of queries:
2.6 Operations
GraphQL defines three operation types:
3. Schema & Type System
3.1 Schema Definition Language (SDL)
GraphQL schemas are written in SDL, a human-readable syntax:
3.2 Type Modifiers
Non-null (
!): Field must always have a valuename: String!- required stringposts: [Post!]!- required list of required PostsList (
[]): Field contains an array[Post]- nullable list of nullable Posts[Post!]- nullable list of required Posts[Post]!- required list of nullable Posts[Post!]!- required list of required Posts3.3 Naming Conventions
Best Practices:
User,BlogPost)firstName,createdAt)userId,includeDeleted)ADMIN,ACTIVE_USER)3.4 Documentation
Add descriptions to types and fields:
4. Query Execution Flow
4.1 Complete Execution Pipeline
┌──────────────────┐ │ Client Query │ └────────┬─────────┘ ↓ ┌──────────────────┐ │ Parsing │ Convert query string to AST └────────┬─────────┘ ↓ ┌──────────────────┐ │ Validation │ Check against schema └────────┬─────────┘ • Field existence ↓ • Type matching ┌──────────────────┐ • Argument correctness │ Execution │ Resolve fields recursively └────────┬─────────┘ ↓ ┌──────────────────┐ │ Data Assembly │ Build response matching query shape └────────┬─────────┘ ↓ ┌──────────────────┐ │ Response │ Return JSON to client └──────────────────┘4.2 Abstract Syntax Tree (AST)
When a server receives a GraphQL query, it:
Example query:
The AST represents this hierarchically, making it easy to validate and execute.
4.3 Validation Phase
The server validates:
If validation fails: Server returns error immediately without executing
If validation succeeds: Execution phase begins
4.4 Execution Phase
The server walks the AST and:
Example Execution Flow:
Query:
{ user(id: "123") { name posts { title } } }Execution steps:
Query.user(id: "123")→ Returns User objectUser.name→ Returns "John Doe"User.posts→ Returns array of Post objectsPost.title→ Returns title string4.5 Response Format
GraphQL responses always have this structure:
{ "data": { // Requested data in the same shape as query }, "errors": [ // Array of errors (if any occurred) ] }Success example:
{ "data": { "user": { "name": "John Doe", "posts": [ { "title": "My First Post" }, { "title": "GraphQL Basics" } ] } } }Error example:
{ "data": null, "errors": [ { "message": "User not found", "locations": [{ "line": 2, "column": 3 }], "path": ["user"] } ] }5. Resolvers
Resolvers are functions that populate data for fields in your schema. They are the core of GraphQL execution.
5.1 Resolver Signature
Every resolver function has this signature:
Parameters:
parent (also called
rootorsource)undefinedargs
user(id: "123")→args = { id: "123" }context (also called
contextValue)user)info
5.2 Resolver Implementation Patterns
Basic Resolver:
Using Arguments:
Nested Field Resolvers:
Mutation Resolvers:
5.3 Resolver Chaining Example
5.4 Convention for Unused Parameters
When you don't need certain parameters, use underscores:
5.5 Default Resolvers
If you don't provide a resolver for a field, GraphQL uses a default resolver that:
parent[fieldName]Only define explicit resolvers when you need to:
6. Apollo Server
Apollo Server is an open-source, spec-compliant GraphQL server that integrates with various Node.js frameworks.
6.1 Basic Server Setup
6.2 Context
Context is an object shared across all resolvers for a single operation.
Common Context Contents:
6.3 Data Sources
Data Sources are classes that encapsulate fetching data from a particular service.
RESTDataSource Example:
Benefits of RESTDataSource:
Using Data Sources:
6.4 Error Handling
Custom Errors:
Format Errors:
6.5 Plugins
Plugins hook into server lifecycle events:
6.6 Mocking Data
Mock data for development and testing:
Note: Apollo Server returns exactly 2 items for every list field when mocking is enabled.
6.7 GraphQL Playground / Sandbox
Apollo Server includes Apollo Sandbox - a web-based GraphQL IDE:
http://localhost:4000Disable in production:
7. Apollo Client
Apollo Client is a comprehensive state management library for JavaScript that enables you to manage both local and remote data with GraphQL.
7.1 Setup & Configuration
Installation:
Basic Setup:
Advanced Configuration:
7.2 Queries with useQuery
Basic Query:
Query with Variables:
Query Options:
7.3 Lazy Queries with useLazyQuery
Execute queries on demand rather than on component mount:
7.4 Mutations with useMutation
Basic Mutation:
Updating Cache After Mutation:
7.5 Subscriptions with useSubscription
Setup WebSocket Link:
Using Subscriptions:
7.6 Local State Management
Reactive Variables:
Field Policies for Local State:
8. Architecture Overview
8.1 Complete System Architecture
┌─────────────────────────────────────────────────────────────┐ │ CLIENT SIDE │ ├─────────────────────────────────────────────────────────────┤ │ React/Vue/Angular App │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ Apollo Client │ │ │ │ ┌────────────┐ ┌──────────┐ ┌─────────────┐ │ │ │ │ │ Cache │ │ Link │ │ DevTools │ │ │ │ │ │ (InMemory) │ │ Chain │ │ │ │ │ │ │ └────────────┘ └──────────┘ └─────────────┘ │ │ │ └──────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ║ HTTP/WebSocket ║ ┌─────────────────────────────────────────────────────────────┐ │ SERVER SIDE │ ├─────────────────────────────────────────────────────────────┤ │ ┌──────────────────────────────────────────────────────┐ │ │ │ Apollo Server │ │ │ │ ┌────────────┐ ┌──────────┐ ┌─────────────┐ │ │ │ │ │ Schema │ │ Resolvers│ │ Data Sources│ │ │ │ │ │ (Type Defs)│ │ │ │ │ │ │ │ │ └────────────┘ └──────────┘ └─────────────┘ │ │ │ └──────────────────────────────────────────────────────┘ │ │ ║ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Data Layer (REST APIs, DBs, Microservices, etc.) │ │ │ └─────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘8.2 Complete Workflow (Client ↔ Server)
React Component ↓ useQuery / useMutation Apollo Client ↓ Checks cache first ↓ If needed, sends GraphQL operation Apollo Link Chain ↓ Adds auth headers, error handling HTTP/WebSocket Transport ↓ Apollo Server ↓ Receives request ↓ Parses query to AST ↓ Validates against Schema ↓ Executes Resolvers Resolvers ↓ Access context (user, dataSources) ↓ Fetch data from sources Data Sources/DataLoaders ↓ REST APIs, Databases, etc. ↓ Return data Resolvers ↓ Transform and return data Apollo Server ↓ Assemble response ↓ Return JSON Apollo Client ↓ Update cache ↓ Notify components React Component ↓ Re-renders with new data8.3 Hierarchical Component Summary
9. Data Fetching Patterns
9.1 The N+1 Problem
Problem:
If you have 100 posts, this makes 101 database calls!
9.2 DataLoader Solution
DataLoader batches and caches requests within a single operation:
How DataLoader Works:
.load()calls in a single tick9.3 RESTDataSource
Apollo's RESTDataSource handles REST API calls efficiently:
Benefits:
9.4 Pagination Patterns
Offset-Based Pagination:
Cursor-Based Pagination:
Relay-Style Pagination with Apollo:
10. Caching Strategies
10.1 Apollo Client Cache
Apollo Client uses a normalized cache by default:
10.2 Fetch Policies
Control how queries interact with the cache:
10.3 Cache Manipulation
Reading from Cache:
Writing to Cache:
Cache Modification:
Evicting from Cache:
10.4 Cache Persistence
Persist cache to localStorage:
10.5 Optimistic UI
Update UI immediately before server responds:
11. Subscriptions & Real-time Data
11.1 Server-Side Subscriptions
Using PubSub:
WebSocket Server Setup:
11.2 Client-Side Subscriptions
Setup:
Using Subscriptions:
11.3 Alternative: Polling
For simpler real-time needs, use polling:
12. Performance Optimization
12.1 DataLoader (Batching & Caching)
Prevents N+1 queries:
12.2 Query Batching
Combine multiple queries into one HTTP request:
12.3 Persisted Queries (APQ)
Send query hash instead of full query string:
Benefits:
12.4 Query Complexity Analysis
Prevent expensive queries:
12.5 Field-Level Caching
Cache expensive resolver results:
12.6 Response Compression
Enable gzip compression:
12.7 Schema Optimization
13. Testing
13.1 Server Testing
Unit Testing Resolvers:
13.2 Client Testing
Using MockedProvider:
Testing Mutations:
13.3 Integration Testing
Test the full stack:
14. Code Generation
GraphQL Code Generator automatically generates TypeScript types from your schema.
14.1 Setup
Installation:
Configuration File (codegen.yml):
Alternative (codegen.ts):
Package.json Script:
{ "scripts": { "codegen": "graphql-codegen" } }14.2 Usage
Run codegen:
Generated Files:
graphql.ts- All GraphQL typesgql.ts- Typed gql functionfragment-masking.ts- Fragment utilitiesindex.ts- ExportsUsing Generated Types:
14.3 Benefits
14.4 Best Practices
gqlfunction instead ofgraphql-tag15. Best Practices & Common Pitfalls
15.1 Common Pitfalls & Solutions
cache-and-networkfetch policy@deprecateddirective, avoid breaking changes15.2 Best Practices
Schema Design:
✅ Use descriptive, meaningful names
✅ Document all types and fields
✅ Use enums for fixed sets of values
✅ Implement pagination for lists
✅ Make IDs non-nullable
✅ Use Input types for complex mutations
✅ Deprecate fields rather than removing them
❌ Don't use abbreviations or unclear names
❌ Don't return null for lists (return empty array)
❌ Don't use generic field names like "data" or "result"
Resolver Best Practices:
✅ Keep resolvers thin (delegate to services)
✅ Use DataLoaders for relationships
✅ Handle errors gracefully
✅ Validate input in resolvers
✅ Use context for shared resources
✅ Keep business logic in services, not resolvers
❌ Don't make direct database calls in resolvers
❌ Don't put business logic in resolvers
❌ Don't ignore error handling
Client Best Practices:
✅ Use fragments for reusable selections
✅ Implement error boundaries
✅ Show loading states
✅ Use optimistic UI for better UX
✅ Normalize cache with unique IDs
✅ Use code generation for type safety
❌ Don't fetch more data than needed
❌ Don't ignore cache policies
❌ Don't skip error handling
Performance:
✅ Use DataLoader to prevent N+1
✅ Implement pagination
✅ Use query batching
✅ Enable persisted queries
✅ Set reasonable query complexity limits
✅ Cache expensive operations
❌ Don't allow unbounded lists
❌ Don't ignore query depth limits
❌ Don't skip performance monitoring
Security:
✅ Validate all inputs
✅ Implement authentication and authorization
✅ Use query depth limiting
✅ Rate limit operations
✅ Sanitize user input
✅ Use HTTPS in production
❌ Don't trust client input
❌ Don't expose sensitive data
❌ Don't skip authorization checks
15.3 Architecture Decision Tree
15.4 Monitoring & Debugging
Server-Side:
Client-Side:
16. Required Packages
16.1 Server-Side Packages
16.2 Client-Side Packages
16.3 Optional Packages
16.4 Package Purposes
@apollo/server@apollo/clientgraphqlgraphql-taggqltemplate literal for operations@graphql-tools/schema@graphql-tools/mock@graphql-codegen/cli@graphql-codegen/client-preset@apollo/datasource-restdataloadergraphql-subscriptionsgraphql-wsConclusion
This guide covers the complete GraphQL and Apollo ecosystem from fundamentals to advanced patterns. Key takeaways:
Continue learning: