Skip to content

Instantly share code, notes, and snippets.

@mosioc
Last active June 21, 2026 18:32
Show Gist options
  • Select an option

  • Save mosioc/0fd67fb5d6b9e26a5b3d03f187352fc7 to your computer and use it in GitHub Desktop.

Select an option

Save mosioc/0fd67fb5d6b9e26a5b3d03f187352fc7 to your computer and use it in GitHub Desktop.
GraphQL/Apollo Cheat Sheet

GraphQL/Apollo Cheat Sheet

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.


Core Concepts

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

Architecture Flow

                    ┌──────────────────────────────┐
                    │            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) │
                    └──────────────────────────────┘

Execution Model

Query received
     │
     ▼
Validate against schema
     │
     ▼
Break into fields
     │
     ▼
Run resolver per field
     │
     ▼
Fetch data (DB / API / cache)
     │
     ▼
Combine results
     │
     ▼
Return JSON

Schema Structure

Schema
│
├── Types
│     └── User, Post, Comment
│
├── Query (READ)
│     └── user(id)
│
├── Mutation (WRITE)
│     └── createUser()
│
└── Subscription (REAL-TIME)
      └── userUpdated()

Key Terms

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

Schema Definition Language (SDL)

The schema is the contract between client and server, defining all available types and operations.

Basic Types

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

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

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

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

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

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
    }
  }
}

Root Types

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

Queries are used to fetch data from the server. They specify exactly which fields you want.

Basic Query

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"
    }
  }
}

Query with Nested Fields

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
        }
      }
    }
  }
}

Query with Arguments

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
    }
  }
}

Named Queries

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
  }
}

Query with Variables

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

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

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

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

Mutations modify data on the server (create, update, delete operations). They're similar to POST, PUT, DELETE in REST.

Basic Mutation

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"
    }
  }
}

Mutation with Variables

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 Mutation

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 Mutation

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
  }
}

Multiple Mutations

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

Subscriptions enable real-time updates via WebSocket. Useful for chat apps, live feeds, notifications.

Basic Subscription

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"
    }
  }
}

Subscription with Variables

Variables allow dynamic subscription parameters.

subscription OnCommentAdded($postId: ID!) {
  commentAdded(postId: $postId) {
    id
    text
    author {
      name
    }
  }
}

# Variables
{
  "postId": "123"
}

Common Subscription Patterns

# 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 Setup

Apollo Server is a production-ready GraphQL server for Node.js.

Basic Server Setup

// 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}`);
});

Resolvers with Context

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

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()
      }
    };
  }
});

Subscriptions with Apollo Server

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 Setup

Apollo Client is a comprehensive state management library for JavaScript that manages both local and remote data with GraphQL.

Basic Client Setup (React)

// 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>
  );
}

Client with Authentication

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()
});

useQuery Hook

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>
  );
}

useQuery with Variables

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>
  );
}

useMutation Hook

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>
  );
}

useSubscription Hook

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>
  );
}

Client Setup with Subscriptions

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()
});

Caching & State Management

Apollo Client's cache is a normalized store that eliminates data duplication and keeps your UI consistent.

Cache Configuration

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;
          }
        }
      }
    }
  }
});

Reading from Cache

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>;
}

Writing to Cache

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>;
}

Local State Management

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>
  );
}

Error Handling

Proper error handling improves user experience and helps debug issues.

Client-Side Error Handling

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>;
}

Global Error Handling

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()
});

Server-Side Error Handling

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

Pagination prevents loading too much data at once. GraphQL supports multiple pagination strategies.

Offset-Based Pagination

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>
  );
}

Cursor-Based Pagination (Relay Connection)

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>
  );
}

Infinite Scroll with Apollo

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>
  );
}

Best Practices

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

Common Mistakes

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

Quick Reference

GraphQL vs REST

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

Common Operations

# 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
  }
}

Apollo Client Hooks

// 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);

Cache Operations

// 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();

Fetch Policies

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
@mosioc

mosioc commented Jun 21, 2026

Copy link
Copy Markdown
Author

GraphQL & Apollo: Complete Reference Guide

A comprehensive guide covering GraphQL concepts, Apollo Client & Server, architecture patterns, and best practices.


Table of Contents

  1. Introduction to GraphQL
  2. Core Concepts
  3. Schema & Type System
  4. Query Execution Flow
  5. Resolvers
  6. Apollo Server
  7. Apollo Client
  8. Architecture Overview
  9. Data Fetching Patterns
  10. Caching Strategies
  11. Subscriptions & Real-time Data
  12. Performance Optimization
  13. Testing
  14. Code Generation
  15. Best Practices & Common Pitfalls
  16. Required Packages

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

  • Declarative Data Fetching: Clients specify exactly what data they need
  • Strongly Typed: Every GraphQL API has a schema that defines types and operations
  • Single Endpoint: Unlike REST, GraphQL uses a single endpoint for all operations
  • Hierarchical: Queries mirror the shape of the data they return
  • Language Agnostic: Can be used with any server framework, programming language, or OS

GraphQL vs REST

GraphQL solves several REST API challenges:

  • Over-fetching: REST endpoints often return more data than needed
  • Under-fetching: Multiple REST calls may be needed to get related data
  • N+1 Problem: Efficiently handled through GraphQL's resolver architecture
  • Versioning: GraphQL schemas evolve without breaking changes

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 JSON

Unlike 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.

GraphQL Operation Purpose Typical HTTP Method REST Equivalent
query Read data GET or POST GET
mutation Create, update, delete POST POST, PUT, PATCH, DELETE
subscription Receive real-time updates WebSocket (or SSE) No direct REST equivalent

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:

  • Types: Define the shape of data
  • Queries: Read-only operations (entry points for data fetching)
  • Mutations: Write operations (create, update, delete)
  • Subscriptions: Real-time updates via WebSocket

2.2 Types

GraphQL has several type categories:

Scalar Types (primitive values):

  • Int: Signed 32-bit integer
  • Float: Signed double-precision floating-point value
  • String: UTF-8 character sequence
  • Boolean: true or false
  • ID: Unique identifier (serialized as String)

Object Types: User-defined types with fields

type User {
  id: ID!
  name: String!
  email: String!
}

Enum: Limited set of values

enum Role {
  ADMIN
  USER
  GUEST
}

Interface: Shared fields across types

interface Node {
  id: ID!
}

Union: Field can be one of multiple types

union SearchResult = User | Post | Comment

Input Type: Structured input for mutations

input CreateUserInput {
  name: String!
  email: String!
  role: Role
}

2.3 Fields & Arguments

Fields are the data access points on types:

type User {
  id: ID!
  name: String!
  posts: [Post!]!  # Relationship field
}

Arguments are parameters for fields:

type Query {
  user(id: ID!): User
  users(limit: Int, offset: Int): [User!]!
  searchPosts(query: String!, category: String): [Post!]!
}

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 deprecated
query GetUser($withEmail: Boolean!) {
  user(id: "123") {
    name
    email @include(if: $withEmail)
  }
}

2.5 Fragments

Fragments are reusable pieces of queries:

fragment UserFields on User {
  id
  name
  email
}

query GetUsers {
  users {
    ...UserFields
    posts {
      id
      title
    }
  }
}

2.6 Operations

GraphQL defines three operation types:

  1. Query (read):
query GetUser {
  user(id: "123") {
    name
    email
  }
}
  1. Mutation (write):
mutation CreateUser($input: CreateUserInput!) {
  createUser(input: $input) {
    id
    name
  }
}
  1. Subscription (real-time):
subscription OnPostAdded {
  postAdded {
    id
    title
    author {
      name
    }
  }
}

3. Schema & Type System

3.1 Schema Definition Language (SDL)

GraphQL schemas are written in SDL, a human-readable syntax:

# Scalar types
scalar DateTime
scalar Email

# Object types
type User {
  id: ID!
  name: String!
  email: Email!
  posts: [Post!]!
  createdAt: DateTime!
}

type Post {
  id: ID!
  title: String!
  content: String!
  author: User!
  comments: [Comment!]!
  publishedAt: DateTime
}

type Comment {
  id: ID!
  text: String!
  author: User!
  post: Post!
}

# Root types
type Query {
  user(id: ID!): User
  users(limit: Int, offset: Int): [User!]!
  post(id: ID!): Post
  posts(authorId: ID): [Post!]!
}

type Mutation {
  createUser(name: String!, email: Email!): User!
  updateUser(id: ID!, name: String, email: Email): User!
  deleteUser(id: ID!): Boolean!
  createPost(authorId: ID!, title: String!, content: String!): Post!
}

type Subscription {
  postAdded: Post!
  commentAdded(postId: ID!): Comment!
}

3.2 Type Modifiers

  • Non-null (!): Field must always have a value

    • name: String! - required string
    • posts: [Post!]! - required list of required Posts
  • List ([]): 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 Posts

3.3 Naming Conventions

Best Practices:

  • Types: PascalCase (e.g., User, BlogPost)
  • Fields: camelCase (e.g., firstName, createdAt)
  • Arguments: camelCase (e.g., userId, includeDeleted)
  • Enums: UPPER_SNAKE_CASE for values (e.g., ADMIN, ACTIVE_USER)

3.4 Documentation

Add descriptions to types and fields:

"""
Represents a user in the system.
Users can create posts and comments.
"""
type User {
  "Unique identifier for the user"
  id: ID!
  
  "User's full name"
  name: String!
  
  "User's email address"
  email: String!
  
  """
  All posts authored by this user.
  Results are sorted by creation date (newest first).
  """
  posts: [Post!]!
}

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:

  1. Parses the query string into an AST (Abstract Syntax Tree)
  2. Validates the AST against the schema
  3. Executes by walking the AST and calling resolvers

Example query:

query GetUser {
  user(id: "123") {
    name
    posts {
      title
    }
  }
}

The AST represents this hierarchically, making it easy to validate and execute.

4.3 Validation Phase

The server validates:

  • All queried fields exist in the schema
  • Arguments match their type definitions
  • Required arguments are provided
  • Field selections are valid for their types
  • No circular fragment references

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:

  1. Calls the root resolver for the operation
  2. For each field, calls its resolver function
  3. Resolvers may return:
    • Scalar values (string, number, etc.)
    • Objects (which trigger nested field resolution)
    • Promises (for async operations)
  4. Results are assembled into the response shape

Example Execution Flow:

Query:

{
  user(id: "123") {
    name
    posts {
      title
    }
  }
}

Execution steps:

  1. Query.user(id: "123") → Returns User object
  2. User.name → Returns "John Doe"
  3. User.posts → Returns array of Post objects
  4. For each Post: Post.title → Returns title string

4.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:

fieldName(parent, args, context, info) => result

Parameters:

  1. parent (also called root or source)

    • The result from the parent resolver
    • For root Query/Mutation fields, this is usually undefined
    • Enables resolver chaining for nested fields
  2. args

    • Object containing all GraphQL arguments provided for the field
    • Example: user(id: "123")args = { id: "123" }
  3. context (also called contextValue)

    • Object shared across all resolvers in a single operation
    • Typically contains:
      • Authentication information (user)
      • Database connections
      • DataSources
      • DataLoaders
    • Set once per request in server configuration
  4. info

    • Contains operation execution metadata:
      • Field name
      • Path to field from root
      • Parent type
      • Return type
      • Full AST of the query
    • Rarely used, but useful for advanced features

5.2 Resolver Implementation Patterns

Basic Resolver:

const resolvers = {
  Query: {
    // Root resolver - parent is undefined
    hello: () => "Hello, World!"
  }
};

Using Arguments:

const resolvers = {
  Query: {
    user: (parent, args, context, info) => {
      return context.dataSources.userAPI.getUserById(args.id);
    },
    
    users: (parent, { limit, offset }, context) => {
      // Destructure args for cleaner code
      return context.dataSources.userAPI.getUsers(limit, offset);
    }
  }
};

Nested Field Resolvers:

const resolvers = {
  Query: {
    user: (_, { id }, { dataSources }) => {
      return dataSources.userAPI.getUserById(id);
    }
  },
  
  User: {
    // Resolver for User.posts field
    // parent is the User object from Query.user
    posts: (parent, args, { dataSources }) => {
      return dataSources.postAPI.getPostsByAuthor(parent.id);
    },
    
    // Computed/virtual fields
    fullName: (parent) => {
      return `${parent.firstName} ${parent.lastName}`;
    },
    
    // Async resolvers
    avatar: async (parent, args, { dataSources }) => {
      return await dataSources.imageAPI.getAvatar(parent.avatarId);
    }
  }
};

Mutation Resolvers:

const resolvers = {
  Mutation: {
    createUser: async (_, { input }, { dataSources, user }) => {
      // Check authentication
      if (!user) {
        throw new Error('Not authenticated');
      }
      
      // Validate input
      if (!input.email.includes('@')) {
        throw new Error('Invalid email');
      }
      
      // Create user
      const newUser = await dataSources.userAPI.createUser(input);
      
      // Trigger subscription
      pubsub.publish('USER_CREATED', { userCreated: newUser });
      
      return newUser;
    },
    
    updateUser: async (_, { id, input }, { dataSources, user }) => {
      // Authorization check
      if (user.id !== id && user.role !== 'ADMIN') {
        throw new Error('Not authorized');
      }
      
      return await dataSources.userAPI.updateUser(id, input);
    }
  }
};

5.3 Resolver Chaining Example

// Query: { user(id: "123") { name, posts { title, author { name } } } }

const resolvers = {
  Query: {
    // Step 1: Root resolver
    user: (_, { id }, { dataSources }) => {
      return dataSources.userAPI.getUserById(id);
      // Returns: { id: "123", name: "John", firstName: "John", lastName: "Doe" }
    }
  },
  
  User: {
    // Step 2: Resolve User.name
    // parent = { id: "123", name: "John", ... }
    // Default resolver returns parent.name
    
    // Step 3: Resolve User.posts
    posts: (parent, _, { dataSources }) => {
      return dataSources.postAPI.getPostsByAuthor(parent.id);
      // Returns: [{ id: "1", title: "Post 1", authorId: "123" }, ...]
    }
  },
  
  Post: {
    // Step 4: For each Post, resolve Post.title
    // Default resolver returns parent.title
    
    // Step 5: For each Post, resolve Post.author
    author: (parent, _, { dataSources }) => {
      return dataSources.userAPI.getUserById(parent.authorId);
      // Returns: { id: "123", name: "John", ... }
    }
  }
  
  // Step 6: For each author, resolve User.name again
  // GraphQL is smart enough to use cached data if available
};

5.4 Convention for Unused Parameters

When you don't need certain parameters, use underscores:

const resolvers = {
  Query: {
    // One underscore for unused parent
    hello: (_) => "Hello!",
    
    // Two underscores for unused parent and args
    currentTime: (_, __) => new Date().toISOString(),
    
    // Use destructuring for needed params
    user: (_, { id }, { dataSources }) => {
      return dataSources.userAPI.getUserById(id);
    }
  }
};

5.5 Default Resolvers

If you don't provide a resolver for a field, GraphQL uses a default resolver that:

  • Returns parent[fieldName]
  • Works for simple data fetching scenarios
// These are equivalent:

// With explicit resolver
const resolvers = {
  User: {
    name: (parent) => parent.name
  }
};

// Without resolver (uses default)
const resolvers = {
  User: {
    // name resolver omitted - uses default
  }
};

Only define explicit resolvers when you need to:

  • Transform data
  • Fetch related data
  • Compute values
  • Handle async operations
  • Implement business logic

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

const { ApolloServer } = require('@apollo/server');
const { startStandaloneServer } = require('@apollo/server/standalone');

// 1. Define schema
const typeDefs = `
  type Query {
    hello: String
    user(id: ID!): User
  }
  
  type User {
    id: ID!
    name: String!
    email: String!
  }
`;

// 2. Define resolvers
const resolvers = {
  Query: {
    hello: () => 'Hello, World!',
    user: (_, { id }, { dataSources }) => {
      return dataSources.userAPI.getUserById(id);
    }
  }
};

// 3. Create server instance
const server = new ApolloServer({
  typeDefs,
  resolvers
});

// 4. Start server
const { url } = await startStandaloneServer(server, {
  listen: { port: 4000 }
});

console.log(`🚀 Server ready at ${url}`);

6.2 Context

Context is an object shared across all resolvers for a single operation.

const server = new ApolloServer({
  typeDefs,
  resolvers,
  
  // Context function runs for every request
  context: async ({ req }) => {
    // Extract auth token
    const token = req.headers.authorization || '';
    
    // Verify and get user
    const user = await getUserFromToken(token);
    
    // Return context object
    return {
      user,
      dataSources: {
        userAPI: new UserAPI(),
        postAPI: new PostAPI()
      },
      loaders: {
        userLoader: new DataLoader(ids => batchGetUsers(ids))
      }
    };
  }
});

Common Context Contents:

  • Authentication: Current user, permissions
  • Data Sources: REST APIs, database connections
  • DataLoaders: For batching and caching
  • Request metadata: IP address, user agent
  • Utilities: Logging, tracing

6.3 Data Sources

Data Sources are classes that encapsulate fetching data from a particular service.

RESTDataSource Example:

const { RESTDataSource } = require('@apollo/datasource-rest');

class UserAPI extends RESTDataSource {
  constructor() {
    super();
    this.baseURL = 'https://api.example.com/';
  }
  
  // GET request
  async getUser(id) {
    return this.get(`users/${id}`);
  }
  
  // GET with query parameters
  async getUsers({ limit = 10, offset = 0 }) {
    return this.get('users', {
      params: { limit, offset }
    });
  }
  
  // POST request
  async createUser(user) {
    return this.post('users', { body: user });
  }
  
  // PUT request
  async updateUser(id, updates) {
    return this.put(`users/${id}`, { body: updates });
  }
  
  // DELETE request
  async deleteUser(id) {
    return this.delete(`users/${id}`);
  }
  
  // Override to add auth headers
  willSendRequest(path, request) {
    request.headers.authorization = this.context.token;
  }
}

Benefits of RESTDataSource:

  • Automatic deduplication of requests
  • Built-in caching with HTTP headers
  • Error handling
  • Request/response interceptors

Using Data Sources:

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: async ({ req }) => ({
    token: req.headers.authorization,
    dataSources: {
      userAPI: new UserAPI(),
      postAPI: new PostAPI()
    }
  })
});

// In resolvers
const resolvers = {
  Query: {
    user: (_, { id }, { dataSources }) => {
      return dataSources.userAPI.getUser(id);
    }
  }
};

6.4 Error Handling

Custom Errors:

const { GraphQLError } = require('graphql');

const resolvers = {
  Query: {
    user: async (_, { id }, { dataSources, user }) => {
      // Authentication error
      if (!user) {
        throw new GraphQLError('Not authenticated', {
          extensions: { code: 'UNAUTHENTICATED' }
        });
      }
      
      const fetchedUser = await dataSources.userAPI.getUser(id);
      
      // Not found error
      if (!fetchedUser) {
        throw new GraphQLError('User not found', {
          extensions: {
            code: 'NOT_FOUND',
            userId: id
          }
        });
      }
      
      // Authorization error
      if (fetchedUser.id !== user.id && user.role !== 'ADMIN') {
        throw new GraphQLError('Not authorized', {
          extensions: { code: 'FORBIDDEN' }
        });
      }
      
      return fetchedUser;
    }
  }
};

Format Errors:

const server = new ApolloServer({
  typeDefs,
  resolvers,
  
  formatError: (formattedError, error) => {
    // Log error
    console.error('GraphQL Error:', error);
    
    // Hide internal errors in production
    if (error.message.startsWith('Database')) {
      return new GraphQLError('Internal server error', {
        extensions: { code: 'INTERNAL_SERVER_ERROR' }
      });
    }
    
    return formattedError;
  }
});

6.5 Plugins

Plugins hook into server lifecycle events:

const myPlugin = {
  async requestDidStart(requestContext) {
    console.log('Request started');
    
    return {
      async parsingDidStart(requestContext) {
        console.log('Parsing started');
      },
      
      async validationDidStart(requestContext) {
        console.log('Validation started');
      },
      
      async executionDidStart(requestContext) {
        console.log('Execution started');
        const start = Date.now();
        
        return {
          async executionDidEnd() {
            const duration = Date.now() - start;
            console.log(`Execution took ${duration}ms`);
          }
        };
      },
      
      async willSendResponse(requestContext) {
        console.log('Sending response');
      },
      
      async didEncounterErrors(requestContext) {
        console.error('Errors:', requestContext.errors);
      }
    };
  }
};

const server = new ApolloServer({
  typeDefs,
  resolvers,
  plugins: [myPlugin]
});

6.6 Mocking Data

Mock data for development and testing:

const { addMocksToSchema } = require('@graphql-tools/mock');
const { makeExecutableSchema } = require('@graphql-tools/schema');

// Create schema
const schema = makeExecutableSchema({ typeDefs });

// Add mocks
const schemaWithMocks = addMocksToSchema({
  schema,
  mocks: {
    Int: () => 6,
    Float: () => 22.1,
    String: () => 'Mock string',
    
    User: () => ({
      id: () => 'user_123',
      name: () => 'Mock User',
      email: () => 'mock@example.com'
    }),
    
    Post: () => ({
      id: () => 'post_456',
      title: () => 'Mock Post Title'
    })
  }
});

const server = new ApolloServer({
  schema: schemaWithMocks
});

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:

  • Automatically available in development mode
  • Access at http://localhost:4000
  • Features:
    • Query editor with autocomplete
    • Documentation explorer
    • Query history
    • Variable editor
    • Response viewer

Disable in production:

const server = new ApolloServer({
  typeDefs,
  resolvers,
  introspection: process.env.NODE_ENV !== 'production',
  plugins: [
    process.env.NODE_ENV === 'production'
      ? ApolloServerPluginLandingPageDisabled()
      : ApolloServerPluginLandingPageGraphQLPlayground()
  ]
});

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:

npm install @apollo/client graphql

Basic Setup:

import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';

// Create client instance
const client = new ApolloClient({
  uri: 'http://localhost:4000/graphql',
  cache: new InMemoryCache()
});

// Wrap app with ApolloProvider (React)
function App() {
  return (
    <ApolloProvider client={client}>
      <YourApp />
    </ApolloProvider>
  );
}

Advanced Configuration:

import { ApolloClient, InMemoryCache, HttpLink, from } from '@apollo/client';
import { onError } from '@apollo/client/link/error';
import { setContext } from '@apollo/client/link/context';

// Auth link - adds token to headers
const authLink = setContext((_, { headers }) => {
  const token = localStorage.getItem('token');
  return {
    headers: {
      ...headers,
      authorization: token ? `Bearer ${token}` : ''
    }
  };
});

// Error link - handles errors globally
const errorLink = onError(({ graphQLErrors, networkError }) => {
  if (graphQLErrors) {
    graphQLErrors.forEach(({ message, locations, path }) => {
      console.error(`[GraphQL error]: Message: ${message}`);
    });
  }
  if (networkError) {
    console.error(`[Network error]: ${networkError}`);
  }
});

// HTTP link
const httpLink = new HttpLink({
  uri: 'http://localhost:4000/graphql'
});

// Combine links
const client = new ApolloClient({
  link: from([errorLink, authLink, httpLink]),
  cache: new InMemoryCache({
    typePolicies: {
      User: {
        keyFields: ['id']
      }
    }
  }),
  defaultOptions: {
    watchQuery: {
      fetchPolicy: 'cache-and-network'
    }
  }
});

7.2 Queries with useQuery

Basic Query:

import { useQuery, gql } from '@apollo/client';

const GET_USERS = gql`
  query GetUsers {
    users {
      id
      name
      email
    }
  }
`;

function UserList() {
  const { loading, error, data } = 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}</li>
      ))}
    </ul>
  );
}

Query with Variables:

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 }
  });
  
  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>
      <ul>
        {data.user.posts.map(post => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  );
}

Query Options:

const { loading, error, data, refetch, fetchMore, networkStatus } = useQuery(
  GET_USERS,
  {
    // Variables
    variables: { limit: 10, offset: 0 },
    
    // Fetch policy
    fetchPolicy: 'cache-first', // 'cache-first' | 'network-only' | 'cache-only' | 'cache-and-network' | 'no-cache'
    
    // Polling (refetch every X ms)
    pollInterval: 5000,
    
    // Skip query execution
    skip: !userId,
    
    // Notify on network status changes
    notifyOnNetworkStatusChange: true,
    
    // Callback when data is received
    onCompleted: (data) => {
      console.log('Query completed:', data);
    },
    
    // Callback when error occurs
    onError: (error) => {
      console.error('Query error:', error);
    }
  }
);

7.3 Lazy Queries with useLazyQuery

Execute queries on demand rather than on component mount:

import { useLazyQuery } from '@apollo/client';

const SEARCH_USERS = gql`
  query SearchUsers($query: String!) {
    searchUsers(query: $query) {
      id
      name
      email
    }
  }
`;

function UserSearch() {
  const [searchUsers, { loading, data }] = useLazyQuery(SEARCH_USERS);
  const [query, setQuery] = useState('');
  
  const handleSearch = (e) => {
    e.preventDefault();
    searchUsers({ variables: { query } });
  };
  
  return (
    <div>
      <form onSubmit={handleSearch}>
        <input
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          placeholder="Search users..."
        />
        <button type="submit">Search</button>
      </form>
      
      {loading && <p>Loading...</p>}
      {data && (
        <ul>
          {data.searchUsers.map(user => (
            <li key={user.id}>{user.name}</li>
          ))}
        </ul>
      )}
    </div>
  );
}

7.4 Mutations with useMutation

Basic Mutation:

import { useMutation, gql } from '@apollo/client';

const CREATE_USER = gql`
  mutation CreateUser($name: String!, $email: String!) {
    createUser(name: $name, email: $email) {
      id
      name
      email
    }
  }
`;

function CreateUserForm() {
  const [createUser, { loading, error }] = useMutation(CREATE_USER);
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    
    try {
      const { data } = await createUser({
        variables: { name, email }
      });
      console.log('User created:', data.createUser);
      setName('');
      setEmail('');
    } catch (err) {
      console.error('Error creating user:', err);
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Name"
      />
      <input
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
      />
      <button type="submit" disabled={loading}>
        {loading ? 'Creating...' : 'Create User'}
      </button>
      {error && <p>Error: {error.message}</p>}
    </form>
  );
}

Updating Cache After Mutation:

const [createUser] = useMutation(CREATE_USER, {
  // Option 1: Refetch queries
  refetchQueries: [
    { query: GET_USERS },
    'GetUsers' // Can use operation name
  ],
  
  // Option 2: Update cache manually
  update(cache, { data: { createUser } }) {
    // Read existing data
    const existing = cache.readQuery({ query: GET_USERS });
    
    // Write updated data
    cache.writeQuery({
      query: GET_USERS,
      data: {
        users: [...existing.users, createUser]
      }
    });
  },
  
  // Option 3: Optimistic response
  optimisticResponse: {
    createUser: {
      __typename: 'User',
      id: 'temp-id',
      name: name,
      email: email
    }
  }
});

7.5 Subscriptions with useSubscription

Setup WebSocket Link:

import { WebSocketLink } from '@apollo/client/link/ws';
import { split } from '@apollo/client';
import { getMainDefinition } from '@apollo/client/utilities';

const wsLink = new WebSocketLink({
  uri: 'ws://localhost:4000/graphql',
  options: {
    reconnect: true,
    connectionParams: {
      authToken: localStorage.getItem('token')
    }
  }
});

const httpLink = new HttpLink({
  uri: 'http://localhost:4000/graphql'
});

// Split between HTTP and WebSocket
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()
});

Using Subscriptions:

const POST_ADDED = gql`
  subscription OnPostAdded {
    postAdded {
      id
      title
      author {
        name
      }
    }
  }
`;

function LatestPost() {
  const { data, loading } = useSubscription(POST_ADDED, {
    onSubscriptionData: ({ client, subscriptionData }) => {
      console.log('New post:', subscriptionData.data.postAdded);
      
      // Optionally update cache
      client.cache.modify({
        fields: {
          posts(existingPosts = []) {
            const newPostRef = client.cache.writeFragment({
              data: subscriptionData.data.postAdded,
              fragment: gql`
                fragment NewPost on Post {
                  id
                  title
                }
              `
            });
            return [newPostRef, ...existingPosts];
          }
        }
      });
    }
  });
  
  if (loading) return <p>Waiting for posts...</p>;
  if (!data) return null;
  
  return (
    <div>
      <h3>Latest Post</h3>
      <p>{data.postAdded.title}</p>
      <small>by {data.postAdded.author.name}</small>
    </div>
  );
}

7.6 Local State Management

Reactive Variables:

import { makeVar, useReactiveVar } from '@apollo/client';

// Create reactive variable
export const cartItemsVar = makeVar([]);

// Use in components
function Cart() {
  const cartItems = useReactiveVar(cartItemsVar);
  
  const addItem = (item) => {
    cartItemsVar([...cartItemsVar(), item]);
  };
  
  return (
    <div>
      <h2>Cart ({cartItems.length} items)</h2>
      {cartItems.map(item => (
        <div key={item.id}>{item.name}</div>
      ))}
    </div>
  );
}

Field Policies for Local State:

const client = new ApolloClient({
  cache: new InMemoryCache({
    typePolicies: {
      Query: {
        fields: {
          cartItems: {
            read() {
              return cartItemsVar();
            }
          }
        }
      }
    }
  })
});

// Query local state
const GET_CART = gql`
  query GetCart {
    cartItems @client
  }
`;

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 data

8.3 Hierarchical Component Summary

GraphQL & Apollo Ecosystem
├── GraphQL Core
│   ├── Schema & Types
│   │   ├── Query / Mutation / Subscription
│   │   ├── Scalar / Object / Enum / Interface / Union / Input
│   │   └── Type Modifiers (!, [])
│   ├── Fields & Arguments
│   ├── Resolvers
│   │   └── (parent, args, context, info) => data
│   ├── Directives
│   │   ├── @include(if: Boolean)
│   │   ├── @skip(if: Boolean)
│   │   └── @deprecated(reason: String)
│   └── Fragments
│
├── Query Execution Flow
│   ├── Parsing (string → AST)
│   ├── Validation (check against schema)
│   ├── Execution (resolve fields)
│   └── Response (assemble JSON)
│
├── Apollo Client (Frontend)
│   ├── Core Components
│   │   ├── ApolloClient
│   │   ├── InMemoryCache
│   │   ├── ApolloLink (HTTP/WebSocket)
│   │   └── ApolloProvider
│   ├── Hooks
│   │   ├── useQuery
│   │   ├── useLazyQuery
│   │   ├── useMutation
│   │   └── useSubscription
│   ├── Cache Management
│   │   ├── Normalized cache
│   │   ├── Type policies
│   │   ├── Cache updates
│   │   └── Reactive variables
│   ├── State Management
│   │   ├── Local state (@client)
│   │   ├── Reactive variables (makeVar)
│   │   └── Field policies
│   └── Advanced Features
│       ├── Optimistic UI
│       ├── Pagination
│       ├── Error & Loading handling
│       └── Fetch policies
│
├── Apollo Server (Backend)
│   ├── Configuration
│   │   ├── typeDefs (Schema)
│   │   ├── resolvers
│   │   ├── context
│   │   └── plugins
│   ├── Data Sources
│   │   ├── RESTDataSource
│   │   ├── Database connectors
│   │   └── Custom sources
│   ├── Middleware & Plugins
│   │   ├── Authentication
│   │   ├── Authorization
│   │   ├── Logging
│   │   └── Error tracking
│   ├── DataLoader
│   │   ├── Request batching
│   │   └── Request caching
│   └── Subscriptions
│       ├── PubSub
│       ├── WebSocket setup
│       └── Real-time resolvers
│
└── Performance & Optimization
    ├── Caching strategies
    ├── Query batching
    ├── Persisted queries
    ├── Query complexity analysis
    └── Field-level caching

9. Data Fetching Patterns

9.1 The N+1 Problem

Problem:

// This creates N+1 database calls
const resolvers = {
  Query: {
    posts: () => db.getAllPosts() // 1 query
  },
  Post: {
    // Called N times (once for each post)
    author: (post) => db.getUserById(post.authorId) // N queries
  }
};

If you have 100 posts, this makes 101 database calls!

9.2 DataLoader Solution

DataLoader batches and caches requests within a single operation:

const DataLoader = require('dataloader');

// Create loader
const userLoader = new DataLoader(async (userIds) => {
  // Receives array of IDs
  const users = await db.getUsersByIds(userIds);
  
  // Must return array in same order as input IDs
  return userIds.map(id => users.find(user => user.id === id));
});

// Use in context
const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: () => ({
    loaders: {
      userLoader: new DataLoader(async (ids) => {
        const users = await db.getUsersByIds(ids);
        return ids.map(id => users.find(u => u.id === id));
      })
    }
  })
});

// Use in resolvers
const resolvers = {
  Post: {
    author: (post, _, { loaders }) => {
      // DataLoader batches all calls in this request
      return loaders.userLoader.load(post.authorId);
    }
  }
};

How DataLoader Works:

  1. Collects all .load() calls in a single tick
  2. Batches them into one call to the batch function
  3. Caches results for the duration of the request
  4. Returns individual results to each caller

9.3 RESTDataSource

Apollo's RESTDataSource handles REST API calls efficiently:

const { RESTDataSource } = require('@apollo/datasource-rest');

class MoviesAPI extends RESTDataSource {
  constructor() {
    super();
    this.baseURL = 'https://api.movies.com/';
  }
  
  async getMovie(id) {
    return this.get(`movies/${id}`);
  }
  
  async getMovies() {
    return this.get('movies');
  }
  
  // Automatically deduplicates requests
  async getMovieReviews(id) {
    return this.get(`movies/${id}/reviews`);
  }
  
  // Respects HTTP cache headers
  async getPopularMovies() {
    return this.get('movies/popular', {
      params: { sort: 'popularity' }
    });
  }
  
  // Can override request configuration
  willSendRequest(path, request) {
    request.headers.authorization = this.context.token;
  }
}

Benefits:

  • Automatic deduplication of identical requests
  • Respects HTTP caching headers
  • Error handling
  • Request/response transformations

9.4 Pagination Patterns

Offset-Based Pagination:

type Query {
  posts(limit: Int, offset: Int): [Post!]!
  postsCount: Int!
}
const { loading, data, fetchMore } = useQuery(GET_POSTS, {
  variables: { limit: 10, offset: 0 }
});

const loadMore = () => {
  fetchMore({
    variables: { offset: data.posts.length },
    updateQuery: (prev, { fetchMoreResult }) => {
      if (!fetchMoreResult) return prev;
      return {
        posts: [...prev.posts, ...fetchMoreResult.posts]
      };
    }
  });
};

Cursor-Based Pagination:

type Query {
  posts(first: Int, after: String): PostConnection!
}

type PostConnection {
  edges: [PostEdge!]!
  pageInfo: PageInfo!
}

type PostEdge {
  cursor: String!
  node: Post!
}

type PageInfo {
  hasNextPage: Boolean!
  endCursor: String
}
const { data, fetchMore } = useQuery(GET_POSTS, {
  variables: { first: 10 }
});

const loadMore = () => {
  fetchMore({
    variables: {
      after: data.posts.pageInfo.endCursor
    }
  });
};

Relay-Style Pagination with Apollo:

const GET_POSTS = gql`
  query GetPosts($first: Int!, $after: String) {
    posts(first: $first, after: $after) {
      edges {
        cursor
        node {
          id
          title
        }
      }
      pageInfo {
        hasNextPage
        endCursor
      }
    }
  }
`;

function PostList() {
  const { data, loading, fetchMore } = useQuery(GET_POSTS, {
    variables: { first: 10 }
  });
  
  const loadMore = () => {
    if (!data.posts.pageInfo.hasNextPage) return;
    
    fetchMore({
      variables: {
        after: data.posts.pageInfo.endCursor
      }
    });
  };
  
  return (
    <div>
      {data.posts.edges.map(({ node }) => (
        <div key={node.id}>{node.title}</div>
      ))}
      {data.posts.pageInfo.hasNextPage && (
        <button onClick={loadMore}>Load More</button>
      )}
    </div>
  );
}

10. Caching Strategies

10.1 Apollo Client Cache

Apollo Client uses a normalized cache by default:

const cache = new InMemoryCache({
  typePolicies: {
    // Specify how to identify objects
    User: {
      keyFields: ['id']
    },
    
    // Merge strategies for fields
    Query: {
      fields: {
        posts: {
          // How to merge incoming data with existing
          merge(existing = [], incoming) {
            return [...existing, ...incoming];
          }
        }
      }
    }
  }
});

10.2 Fetch Policies

Control how queries interact with the cache:

useQuery(GET_USER, {
  // Policies:
  
  // 'cache-first' (default)
  // Return cached data if available, otherwise fetch from network
  fetchPolicy: 'cache-first',
  
  // 'cache-and-network'
  // Return cached data immediately, then fetch from network
  fetchPolicy: 'cache-and-network',
  
  // 'network-only'
  // Always fetch from network, update cache
  fetchPolicy: 'network-only',
  
  // 'cache-only'
  // Never fetch from network, error if not in cache
  fetchPolicy: 'cache-only',
  
  // 'no-cache'
  // Always fetch from network, don't update cache
  fetchPolicy: 'no-cache'
});

10.3 Cache Manipulation

Reading from Cache:

const user = client.readQuery({
  query: GET_USER,
  variables: { id: '123' }
});

const userFragment = client.readFragment({
  id: 'User:123',
  fragment: gql`
    fragment UserName on User {
      name
    }
  `
});

Writing to Cache:

client.writeQuery({
  query: GET_USER,
  variables: { id: '123' },
  data: {
    user: {
      __typename: 'User',
      id: '123',
      name: 'Updated Name'
    }
  }
});

client.writeFragment({
  id: 'User:123',
  fragment: gql`
    fragment UserName on User {
      name
    }
  `,
  data: {
    name: 'Updated Name'
  }
});

Cache Modification:

client.cache.modify({
  id: 'User:123',
  fields: {
    name(cachedName) {
      return cachedName.toUpperCase();
    },
    posts(existingPostRefs, { readField }) {
      // Filter out deleted post
      return existingPostRefs.filter(
        postRef => readField('id', postRef) !== deletedPostId
      );
    }
  }
});

Evicting from Cache:

client.cache.evict({ id: 'User:123' });
client.cache.gc(); // Garbage collect dangling references

10.4 Cache Persistence

Persist cache to localStorage:

import { InMemoryCache } from '@apollo/client';
import { persistCache, LocalStorageWrapper } from 'apollo3-cache-persist';

const cache = new InMemoryCache();

await persistCache({
  cache,
  storage: new LocalStorageWrapper(window.localStorage),
  maxSize: 1048576, // 1MB
  debug: true
});

const client = new ApolloClient({
  cache,
  uri: 'http://localhost:4000/graphql'
});

10.5 Optimistic UI

Update UI immediately before server responds:

const [deletePost] = useMutation(DELETE_POST, {
  optimisticResponse: {
    __typename: 'Mutation',
    deletePost: {
      __typename: 'Post',
      id: postId,
      // Other fields...
    }
  },
  update(cache, { data: { deletePost } }) {
    cache.modify({
      fields: {
        posts(existingPosts, { readField }) {
          return existingPosts.filter(
            postRef => readField('id', postRef) !== deletePost.id
          );
        }
      }
    });
  }
});

11. Subscriptions & Real-time Data

11.1 Server-Side Subscriptions

Using PubSub:

const { PubSub } = require('graphql-subscriptions');
const pubsub = new PubSub();

const typeDefs = gql`
  type Subscription {
    postAdded: Post!
    commentAdded(postId: ID!): Comment!
  }
  
  type Mutation {
    createPost(title: String!, content: String!): Post!
  }
`;

const resolvers = {
  Mutation: {
    createPost: async (_, { title, content }, { dataSources }) => {
      const post = await dataSources.postAPI.createPost({ title, content });
      
      // Publish event
      pubsub.publish('POST_ADDED', { postAdded: post });
      
      return post;
    }
  },
  
  Subscription: {
    postAdded: {
      subscribe: () => pubsub.asyncIterator(['POST_ADDED'])
    },
    
    commentAdded: {
      subscribe: (_, { postId }) => {
        return pubsub.asyncIterator([`COMMENT_ADDED_${postId}`]);
      }
    }
  }
};

WebSocket Server Setup:

const { ApolloServer } = require('@apollo/server');
const { expressMiddleware } = require('@apollo/server/express4');
const { ApolloServerPluginDrainHttpServer } = require('@apollo/server/plugin/drainHttpServer');
const { createServer } = require('http');
const express = require('express');
const { WebSocketServer } = require('ws');
const { useServer } = require('graphql-ws/lib/use/ws');
const { makeExecutableSchema } = require('@graphql-tools/schema');

const schema = makeExecutableSchema({ typeDefs, resolvers });

const app = express();
const httpServer = createServer(app);

// WebSocket server
const wsServer = new WebSocketServer({
  server: httpServer,
  path: '/graphql'
});

const serverCleanup = useServer({ schema }, wsServer);

// Apollo Server
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, () => {
  console.log('🚀 Server ready at http://localhost:4000/graphql');
});

11.2 Client-Side Subscriptions

Setup:

import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
import { split, HttpLink } from '@apollo/client';
import { getMainDefinition } from '@apollo/client/utilities';

const wsLink = new GraphQLWsLink(
  createClient({
    url: 'ws://localhost:4000/graphql',
    connectionParams: {
      authToken: localStorage.getItem('token')
    }
  })
);

const httpLink = new HttpLink({
  uri: 'http://localhost:4000/graphql'
});

const splitLink = split(
  ({ query }) => {
    const definition = getMainDefinition(query);
    return (
      definition.kind === 'OperationDefinition' &&
      definition.operation === 'subscription'
    );
  },
  wsLink,
  httpLink
);

Using Subscriptions:

const POST_ADDED = gql`
  subscription OnPostAdded {
    postAdded {
      id
      title
      author {
        name
      }
    }
  }
`;

function PostFeed() {
  const { data, loading } = useSubscription(POST_ADDED, {
    onSubscriptionData: ({ client, subscriptionData }) => {
      // Update cache
      client.cache.modify({
        fields: {
          posts(existingPosts = []) {
            const newPostRef = client.cache.writeFragment({
              data: subscriptionData.data.postAdded,
              fragment: gql`
                fragment NewPost on Post {
                  id
                  title
                }
              `
            });
            return [newPostRef, ...existingPosts];
          }
        }
      });
    }
  });
  
  return data ? (
    <div>New post: {data.postAdded.title}</div>
  ) : null;
}

11.3 Alternative: Polling

For simpler real-time needs, use polling:

const { data } = useQuery(GET_POSTS, {
  pollInterval: 5000 // Refetch every 5 seconds
});

// Stop/start polling
const { data, startPolling, stopPolling } = useQuery(GET_POSTS);

useEffect(() => {
  startPolling(5000);
  return () => stopPolling();
}, [startPolling, stopPolling]);

12. Performance Optimization

12.1 DataLoader (Batching & Caching)

Prevents N+1 queries:

const DataLoader = require('dataloader');

const userLoader = new DataLoader(async (userIds) => {
  console.log('Batch loading users:', userIds);
  const users = await db.getUsersByIds(userIds);
  return userIds.map(id => users.find(u => u.id === id));
});

// In context
context: () => ({
  loaders: {
    user: userLoader
  }
});

// In resolvers
Post: {
  author: (post, _, { loaders }) => {
    return loaders.user.load(post.authorId);
  }
}

12.2 Query Batching

Combine multiple queries into one HTTP request:

import { BatchHttpLink } from '@apollo/client/link/batch-http';

const link = new BatchHttpLink({
  uri: 'http://localhost:4000/graphql',
  batchMax: 10, // Max queries per batch
  batchInterval: 20 // Wait 20ms before sending
});

12.3 Persisted Queries (APQ)

Send query hash instead of full query string:

// Server
const server = new ApolloServer({
  persistedQueries: {
    cache: new RedisCache({ host: 'localhost' })
  }
});

// Client
import { createPersistedQueryLink } from '@apollo/client/link/persisted-queries';
import { sha256 } from 'crypto-hash';

const link = createPersistedQueryLink({ sha256 }).concat(httpLink);

Benefits:

  • Reduced network payload
  • Improved security (queries whitelist)
  • Better CDN caching

12.4 Query Complexity Analysis

Prevent expensive queries:

const { createComplexityLimitRule } = require('graphql-validation-complexity');

const server = new ApolloServer({
  validationRules: [
    createComplexityLimitRule(1000, {
      onCost: (cost) => console.log('Query cost:', cost),
      createError: (cost, documentNode) => {
        return new GraphQLError(
          `Query too complex: ${cost}. Maximum allowed: 1000`
        );
      }
    })
  ]
});

12.5 Field-Level Caching

Cache expensive resolver results:

class UserAPI extends RESTDataSource {
  async getUser(id) {
    return this.get(`users/${id}`, undefined, {
      cacheOptions: { ttl: 60 } // Cache for 60 seconds
    });
  }
}

12.6 Response Compression

Enable gzip compression:

const compression = require('compression');
app.use(compression());

12.7 Schema Optimization

  • Use pagination for lists
  • Avoid deeply nested queries
  • Use @defer and @stream directives (experimental)
  • Implement field-level authorization
  • Use DataLoader for all relationships

13. Testing

13.1 Server Testing

Unit Testing Resolvers:

const { createTestClient } = require('apollo-server-testing');
const { ApolloServer } = require('@apollo/server');

describe('GraphQL Server', () => {
  let server, query, mutate;
  
  beforeAll(() => {
    server = new ApolloServer({
      typeDefs,
      resolvers,
      context: () => ({
        user: { id: '1', role: 'ADMIN' },
        dataSources: {
          userAPI: new MockUserAPI()
        }
      })
    });
    
    const testClient = createTestClient(server);
    query = testClient.query;
    mutate = testClient.mutate;
  });
  
  it('fetches user by ID', async () => {
    const GET_USER = gql`
      query GetUser($id: ID!) {
        user(id: $id) {
          id
          name
          email
        }
      }
    `;
    
    const result = await query({
      query: GET_USER,
      variables: { id: '1' }
    });
    
    expect(result.errors).toBeUndefined();
    expect(result.data.user).toEqual({
      id: '1',
      name: 'Alice',
      email: 'alice@example.com'
    });
  });
  
  it('creates user with mutation', async () => {
    const CREATE_USER = gql`
      mutation CreateUser($name: String!, $email: String!) {
        createUser(name: $name, email: $email) {
          id
          name
          email
        }
      }
    `;
    
    const result = await mutate({
      mutation: CREATE_USER,
      variables: { name: 'Bob', email: 'bob@example.com' }
    });
    
    expect(result.data.createUser).toMatchObject({
      name: 'Bob',
      email: 'bob@example.com'
    });
  });
  
  it('handles errors correctly', async () => {
    const result = await query({
      query: GET_USER,
      variables: { id: 'nonexistent' }
    });
    
    expect(result.errors).toBeDefined();
    expect(result.errors[0].message).toContain('User not found');
  });
});

13.2 Client Testing

Using MockedProvider:

import { MockedProvider } from '@apollo/client/testing';
import { render, screen, waitFor } from '@testing-library/react';

const mocks = [
  {
    request: {
      query: GET_USER,
      variables: { id: '1' }
    },
    result: {
      data: {
        user: {
          id: '1',
          name: 'Alice',
          email: 'alice@example.com'
        }
      }
    }
  },
  {
    request: {
      query: GET_USER,
      variables: { id: 'error' }
    },
    error: new Error('User not found')
  }
];

test('renders user profile', async () => {
  render(
    <MockedProvider mocks={mocks} addTypename={false}>
      <UserProfile userId="1" />
    </MockedProvider>
  );
  
  expect(screen.getByText('Loading...')).toBeInTheDocument();
  
  await waitFor(() => {
    expect(screen.getByText('Alice')).toBeInTheDocument();
    expect(screen.getByText('alice@example.com')).toBeInTheDocument();
  });
});

test('handles errors', async () => {
  render(
    <MockedProvider mocks={mocks} addTypename={false}>
      <UserProfile userId="error" />
    </MockedProvider>
  );
  
  await waitFor(() => {
    expect(screen.getByText(/error/i)).toBeInTheDocument();
  });
});

Testing Mutations:

test('creates user on form submit', async () => {
  const createUserMock = {
    request: {
      query: CREATE_USER,
      variables: { name: 'Bob', email: 'bob@example.com' }
    },
    result: {
      data: {
        createUser: {
          id: '2',
          name: 'Bob',
          email: 'bob@example.com'
        }
      }
    }
  };
  
  const { getByPlaceholderText, getByText } = render(
    <MockedProvider mocks={[createUserMock]} addTypename={false}>
      <CreateUserForm />
    </MockedProvider>
  );
  
  fireEvent.change(getByPlaceholderText('Name'), {
    target: { value: 'Bob' }
  });
  fireEvent.change(getByPlaceholderText('Email'), {
    target: { value: 'bob@example.com' }
  });
  fireEvent.click(getByText('Create User'));
  
  await waitFor(() => {
    expect(screen.getByText('User created successfully')).toBeInTheDocument();
  });
});

13.3 Integration Testing

Test the full stack:

const request = require('supertest');
const { createTestServer } = require('./test-utils');

describe('Integration Tests', () => {
  let server, app;
  
  beforeAll(async () => {
    ({ server, app } = await createTestServer());
  });
  
  afterAll(async () => {
    await server.stop();
  });
  
  it('creates user and fetches it', async () => {
    // Create user
    const createResponse = await request(app)
      .post('/graphql')
      .send({
        query: `
          mutation {
            createUser(name: "Alice", email: "alice@example.com") {
              id
              name
            }
          }
        `
      });
    
    expect(createResponse.body.data.createUser.name).toBe('Alice');
    const userId = createResponse.body.data.createUser.id;
    
    // Fetch user
    const fetchResponse = await request(app)
      .post('/graphql')
      .send({
        query: `
          query {
            user(id: "${userId}") {
              id
              name
              email
            }
          }
        `
      });
    
    expect(fetchResponse.body.data.user).toEqual({
      id: userId,
      name: 'Alice',
      email: 'alice@example.com'
    });
  });
});

14. Code Generation

GraphQL Code Generator automatically generates TypeScript types from your schema.

14.1 Setup

Installation:

npm install -D @graphql-codegen/cli @graphql-codegen/client-preset

Configuration File (codegen.yml):

schema: 'http://localhost:4000/graphql'
documents: 'src/**/*.{ts,tsx}'
generates:
  src/generated/:
    preset: client
    presetConfig:
      gqlTagName: gql
    plugins:
      - typescript
      - typescript-operations
      - typescript-react-apollo
    config:
      withHooks: true
      withComponent: false

Alternative (codegen.ts):

import { CodegenConfig } from '@graphql-codegen/cli';

const config: CodegenConfig = {
  schema: 'http://localhost:4000/graphql',
  documents: ['src/**/*.tsx', 'src/**/*.ts'],
  generates: {
    './src/generated/': {
      preset: 'client',
      presetConfig: {
        gqlTagName: 'gql'
      }
    }
  }
};

export default config;

Package.json Script:

{
  "scripts": {
    "codegen": "graphql-codegen"
  }
}

14.2 Usage

Run codegen:

npm run codegen

Generated Files:

  • graphql.ts - All GraphQL types
  • gql.ts - Typed gql function
  • fragment-masking.ts - Fragment utilities
  • index.ts - Exports

Using Generated Types:

import { gql } from './generated/gql';
import { GetUserQuery, GetUserQueryVariables } from './generated/graphql';

const GET_USER = gql(`
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      name
      email
      posts {
        id
        title
      }
    }
  }
`);

function UserProfile({ userId }: { userId: string }) {
  const { data, loading, error } = useQuery<GetUserQuery, GetUserQueryVariables>(
    GET_USER,
    { variables: { id: userId } }
  );
  
  // data is fully typed!
  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error</p>;
  
  return (
    <div>
      <h1>{data.user.name}</h1>
      {data.user.posts.map(post => (
        <div key={post.id}>{post.title}</div>
      ))}
    </div>
  );
}

14.3 Benefits

  • Type Safety: Compile-time errors for mismatched queries
  • Autocomplete: IDE suggests available fields
  • Refactoring: Schema changes caught immediately
  • Documentation: Types serve as inline documentation
  • Single Source of Truth: Schema drives frontend types

14.4 Best Practices

  1. Run codegen after schema changes
  2. Commit generated files to version control
  3. Use generated gql function instead of graphql-tag
  4. Keep frontend types in sync with schema
  5. Use fragments for reusable type definitions

15. Best Practices & Common Pitfalls

15.1 Common Pitfalls & Solutions

Problem Solution
N+1 queries Use DataLoader for batching and caching
Over-fetching Request only needed fields with precise queries
Under-fetching Design comprehensive queries with fragments
Cache inconsistency Normalize cache, use unique IDs consistently
Stale data Use polling, subscriptions, or cache-and-network fetch policy
Large responses Implement cursor-based pagination
Slow resolvers Add field-level caching, optimize database queries
Complex mutations Break into smaller mutations, use optimistic updates
Schema versioning Use @deprecated directive, avoid breaking changes
Memory leaks Clean up subscriptions, use proper lifecycle hooks
Authentication errors Check auth in context, use consistent error handling

15.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

Need API?
├─ Real-time updates required? → Use Subscriptions
├─ Complex nested data? → GraphQL (flexible queries)
├─ Simple CRUD? → Could use REST or GraphQL
└─ Multiple data sources? → GraphQL (unified graph)

Client State Management?
├─ Server data only? → Apollo Client cache
├─ Local + server data? → Apollo + reactive variables
└─ Complex local state? → Apollo + Redux/Zustand

Performance Issues?
├─ Too many queries? → DataLoader
├─ Large payloads? → Pagination + field selection
├─ Slow resolvers? → Caching + optimize DB queries
└─ Network latency? → Persisted queries + batching

Schema Evolution?
├─ Adding fields? → Safe, just add them
├─ Deprecating fields? → Use @deprecated directive
├─ Changing types? → Create new fields, deprecate old
└─ Removing fields? → Deprecate first, remove after clients migrate

15.4 Monitoring & Debugging

Server-Side:

  • Use Apollo Studio for metrics and tracing
  • Log slow queries and errors
  • Monitor resolver execution times
  • Track cache hit rates

Client-Side:

  • Use Apollo Client DevTools browser extension
  • Monitor cache state
  • Track query performance
  • Log network errors

16. Required Packages

16.1 Server-Side Packages

# Core Apollo Server
npm install @apollo/server graphql

# For standalone server
npm install @apollo/server

# For Express integration
npm install @apollo/server express cors

# GraphQL utilities
npm install @graphql-tools/schema @graphql-tools/mock

# Data fetching
npm install @apollo/datasource-rest

# DataLoader (N+1 solution)
npm install dataloader

# Subscriptions
npm install graphql-subscriptions graphql-ws ws

# Code generation
npm install -D @graphql-codegen/cli @graphql-codegen/client-preset

16.2 Client-Side Packages

# Core Apollo Client
npm install @apollo/client graphql

# GraphQL tag (often included with @apollo/client)
npm install graphql-tag

# For subscriptions
npm install graphql-ws

# For cache persistence
npm install apollo3-cache-persist

# Code generation
npm install -D @graphql-codegen/cli @graphql-codegen/client-preset

16.3 Optional Packages

# Testing
npm install -D apollo-server-testing @apollo/client/testing

# Query batching
npm install @apollo/client

# Persisted queries
npm install @apollo/client crypto-hash

# Query complexity
npm install graphql-validation-complexity

# Caching
npm install apollo-server-cache-redis

16.4 Package Purposes

Package Purpose
@apollo/server Full-featured GraphQL server
@apollo/client Complete client with cache and state management
graphql Core GraphQL logic (parsing, validation)
graphql-tag gql template literal for operations
@graphql-tools/schema Schema creation utilities
@graphql-tools/mock Mocking data for development
@graphql-codegen/cli TypeScript type generation
@graphql-codegen/client-preset Client-side code generation preset
@apollo/datasource-rest REST API data source
dataloader Batching and caching for data fetching
graphql-subscriptions PubSub for subscriptions
graphql-ws WebSocket protocol for subscriptions

Conclusion

This guide covers the complete GraphQL and Apollo ecosystem from fundamentals to advanced patterns. Key takeaways:

  1. GraphQL provides a flexible, efficient API layer
  2. Apollo Server makes implementing GraphQL servers straightforward
  3. Apollo Client handles data fetching, caching, and state management
  4. Performance requires attention to N+1 queries, caching, and pagination
  5. Type safety through code generation improves developer experience
  6. Best practices prevent common pitfalls and ensure maintainable code

Continue learning:


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment