Intermediate
35 min
Full Guide

GraphQL

Learn GraphQL queries, mutations, and subscriptions for flexible data fetching

What is GraphQL?

GraphQL is a query language and runtime for APIs developed by Facebook in 2012 and open-sourced in 2015. Unlike REST where the server defines what data is returned, GraphQL lets clients request exactly the data they need—no more, no less.

GraphQL operates through a single endpoint and uses a strong type system to describe data, enabling powerful developer tools and runtime validation.

📊 GraphQL vs REST

REST

  • • Multiple endpoints
  • • Over/under fetching
  • • Multiple requests needed
  • • Fixed data structure

GraphQL

  • • Single endpoint
  • • Get exactly what you need
  • • One request for all data
  • • Flexible client queries

Basic Query Syntax

// GraphQL Query Structure
// Query to get user data
query {
  user(id: "123") {
    id
    name
    email
    posts {
      title
      createdAt
    }
  }
}

// Response - matches query shape exactly
{
  "data": {
    "user": {
      "id": "123",
      "name": "John Doe",
      "email": "john@example.com",
      "posts": [
        { "title": "Hello World", "createdAt": "2024-01-15" },
        { "title": "GraphQL Basics", "createdAt": "2024-01-20" }
      ]
    }
  }
}

// Compare to REST - would need multiple requests:
// GET /api/users/123
// GET /api/users/123/posts
// And might get extra fields you don't need!

Making GraphQL Requests with Fetch

// Basic GraphQL request
async function graphqlRequest(query, variables = {}) {
  const response = await fetch('https://api.example.com/graphql', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer your-token'
    },
    body: JSON.stringify({
      query,
      variables
    })
  });

  const result = await response.json();

  if (result.errors) {
    throw new Error(result.errors[0].message);
  }

  return result.data;
}

// Usage - Query with variables
const GET_USER = `
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      name
      email
      avatar
    }
  }
`;

async function getUser(userId) {
  const data = await graphqlRequest(GET_USER, { id: userId });
  return data.user;
}

// Usage
const user = await getUser('123');
console.log(user.name); // "John Doe"

Query with Arguments

// Query with multiple arguments
const GET_POSTS = `
  query GetPosts($limit: Int!, $offset: Int, $status: PostStatus) {
    posts(limit: $limit, offset: $offset, status: $status) {
      id
      title
      excerpt
      author {
        name
        avatar
      }
      tags {
        name
      }
      publishedAt
    }
    postsCount(status: $status)
  }
`;

async function getPosts(page = 1, status = 'PUBLISHED') {
  const limit = 10;
  const offset = (page - 1) * limit;

  const data = await graphqlRequest(GET_POSTS, {
    limit,
    offset,
    status
  });

  return {
    posts: data.posts,
    total: data.postsCount,
    hasMore: data.postsCount > page * limit
  };
}

// Nested queries - get related data in one request
const GET_POST_WITH_COMMENTS = `
  query GetPost($id: ID!) {
    post(id: $id) {
      id
      title
      content
      author {
        id
        name
        bio
      }
      comments {
        id
        text
        author {
          name
        }
        createdAt
      }
    }
  }
`;

Mutations - Creating & Updating Data

// Mutation to create data
const CREATE_POST = `
  mutation CreatePost($input: CreatePostInput!) {
    createPost(input: $input) {
      id
      title
      slug
      status
      createdAt
    }
  }
`;

async function createPost(postData) {
  const data = await graphqlRequest(CREATE_POST, {
    input: {
      title: postData.title,
      content: postData.content,
      tags: postData.tags
    }
  });
  
  return data.createPost;
}

// Usage
const newPost = await createPost({
  title: 'My New Post',
  content: 'This is the content...',
  tags: ['javascript', 'graphql']
});

// Mutation to update data
const UPDATE_POST = `
  mutation UpdatePost($id: ID!, $input: UpdatePostInput!) {
    updatePost(id: $id, input: $input) {
      id
      title
      content
      updatedAt
    }
  }
`;

async function updatePost(id, updates) {
  const data = await graphqlRequest(UPDATE_POST, {
    id,
    input: updates
  });
  
  return data.updatePost;
}

// Mutation to delete
const DELETE_POST = `
  mutation DeletePost($id: ID!) {
    deletePost(id: $id) {
      success
      message
    }
  }
`;

async function deletePost(id) {
  const data = await graphqlRequest(DELETE_POST, { id });
  return data.deletePost.success;
}

Fragments - Reusable Query Pieces

// Define reusable fragments
const USER_FRAGMENT = `
  fragment UserFields on User {
    id
    name
    email
    avatar
    role
  }
`;

const POST_FRAGMENT = `
  fragment PostFields on Post {
    id
    title
    excerpt
    publishedAt
    author {
      ...UserFields
    }
  }
  ${USER_FRAGMENT}
`;

// Use fragments in queries
const GET_FEED = `
  query GetFeed($limit: Int!) {
    feed(limit: $limit) {
      ...PostFields
      comments(limit: 3) {
        id
        text
        author {
          ...UserFields
        }
      }
    }
  }
  ${POST_FRAGMENT}
`;

// Without fragments, you'd repeat these fields everywhere!
async function getFeed() {
  const data = await graphqlRequest(GET_FEED, { limit: 20 });
  return data.feed;
}

Aliases - Rename Fields

// Use aliases when you need the same field with different arguments
const GET_USER_POSTS = `
  query GetUserPosts($userId: ID!) {
    user(id: $userId) {
      name
      
      # Get published and draft posts in one query
      publishedPosts: posts(status: PUBLISHED, limit: 5) {
        id
        title
      }
      
      draftPosts: posts(status: DRAFT, limit: 5) {
        id
        title
      }
      
      # Different user comparisons
      followersCount: followers { count }
      followingCount: following { count }
    }
  }
`;

// Response structure
{
  "data": {
    "user": {
      "name": "John",
      "publishedPosts": [
        { "id": "1", "title": "Published Post" }
      ],
      "draftPosts": [
        { "id": "2", "title": "Work in Progress" }
      ],
      "followersCount": { "count": 150 },
      "followingCount": { "count": 75 }
    }
  }
}

Subscriptions - Real-Time Data

// GraphQL Subscriptions use WebSocket
// Subscription definition
const NEW_MESSAGE = `
  subscription OnNewMessage($chatId: ID!) {
    messageAdded(chatId: $chatId) {
      id
      text
      sender {
        id
        name
      }
      createdAt
    }
  }
`;

// Using with graphql-ws library
import { createClient } from 'graphql-ws';

const client = createClient({
  url: 'wss://api.example.com/graphql',
  connectionParams: {
    authToken: 'your-token'
  }
});

// Subscribe to messages
function subscribeToMessages(chatId, onMessage) {
  return client.subscribe(
    {
      query: NEW_MESSAGE,
      variables: { chatId }
    },
    {
      next: (data) => {
        onMessage(data.data.messageAdded);
      },
      error: (err) => console.error('Subscription error:', err),
      complete: () => console.log('Subscription complete')
    }
  );
}

// Usage
const unsubscribe = subscribeToMessages('chat-123', (message) => {
  console.log('New message:', message.text);
  addMessageToUI(message);
});

// Later: cleanup
unsubscribe();

Error Handling

// GraphQL returns errors alongside data
// Response can have both data and errors!
{
  "data": {
    "user": { "id": "1", "name": "John" },
    "posts": null  // This failed
  },
  "errors": [
    {
      "message": "Not authorized to view posts",
      "path": ["posts"],
      "extensions": {
        "code": "UNAUTHORIZED"
      }
    }
  ]
}

// Comprehensive error handling
async function graphqlRequest(query, variables = {}) {
  try {
    const response = await fetch('/graphql', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ query, variables })
    });

    if (!response.ok) {
      throw new Error(`Network error: ${response.status}`);
    }

    const result = await response.json();

    // Handle GraphQL errors
    if (result.errors) {
      const error = result.errors[0];
      
      // Check error type
      switch (error.extensions?.code) {
        case 'UNAUTHORIZED':
          throw new AuthError('Please log in');
        case 'VALIDATION_ERROR':
          throw new ValidationError(error.message, error.extensions.validationErrors);
        case 'NOT_FOUND':
          throw new NotFoundError(error.message);
        default:
          throw new GraphQLError(error.message);
      }
    }

    return result.data;
  } catch (error) {
    console.error('GraphQL request failed:', error);
    throw error;
  }
}

// Custom error classes
class GraphQLError extends Error {
  constructor(message) {
    super(message);
    this.name = 'GraphQLError';
  }
}

class AuthError extends GraphQLError {
  constructor(message) {
    super(message);
    this.name = 'AuthError';
  }
}

GraphQL Client Class

// Reusable GraphQL client
class GraphQLClient {
  constructor(endpoint, options = {}) {
    this.endpoint = endpoint;
    this.headers = {
      'Content-Type': 'application/json',
      ...options.headers
    };
  }

  setAuthToken(token) {
    this.headers['Authorization'] = `Bearer ${token}`;
  }

  async query(query, variables = {}) {
    return this.request(query, variables);
  }

  async mutate(mutation, variables = {}) {
    return this.request(mutation, variables);
  }

  async request(query, variables) {
    const response = await fetch(this.endpoint, {
      method: 'POST',
      headers: this.headers,
      body: JSON.stringify({ query, variables })
    });

    const result = await response.json();

    if (result.errors) {
      throw new Error(result.errors[0].message);
    }

    return result.data;
  }
}

// Usage
const client = new GraphQLClient('https://api.example.com/graphql');
client.setAuthToken('jwt-token');

// Queries
const users = await client.query(`
  query { users { id name } }
`);

// Mutations
const newUser = await client.mutate(`
  mutation($name: String!, $email: String!) {
    createUser(name: $name, email: $email) {
      id name email
    }
  }

  
`, { name: 'John', email: 'john@example.com' });

💡 GraphQL Best Practices

  • Request only needed fields - Avoid over-fetching
  • Use fragments - DRY and maintainable queries
  • Name your operations - Easier debugging and caching
  • Use variables - Never string interpolation
  • Handle partial responses - Data and errors can coexist
  • Implement pagination - Use cursor-based pagination for large datasets