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