What is GraphQL?
GraphQL is a query language for APIs and a runtime for executing those queries. It provides a complete and understandable description of the data in your API, giving clients the power to ask for exactly what they need.
🆚 GraphQL vs REST
// REST - Multiple endpoints, over-fetching
GET /api/users/123
// Returns: { id, name, email, bio, avatar, posts, ... }
GET /api/users/123/posts
// Returns: [{ id, title, content, author, comments, ... }]
// GraphQL - Single endpoint, exact data
POST /graphql
{
query {
user(id: 123) {
name
email
posts {
title
}
}
}
}
// Returns only: { user: { name, email, posts: [{ title }] } }
// GraphQL Advantages:
// ✅ Request exactly what you need (no over/under-fetching)
// ✅ Single request for nested resources
// ✅ Strongly typed schema
// ✅ Self-documenting
// ✅ Faster iteration (no endpoint versioning)
// ✅ Real-time with subscriptions
// REST Advantages:
// ✅ Simpler for simple APIs
// ✅ Better HTTP caching
// ✅ More familiar
// ✅ File upload easier
📝 GraphQL Basics
Schema and Types
// Schema defines API structure
type User {
id: ID! # ! means required
name: String!
email: String!
age: Int
posts: [Post!]! # Array of Posts (never null)
createdAt: String
}
type Post {
id: ID!
title: String!
content: String!
author: User!
comments: [Comment!]!
published: Boolean!
}
type Comment {
id: ID!
text: String!
author: User!
post: Post!
}
# Query type - read operations
type Query {
user(id: ID!): User
users: [User!]!
post(id: ID!): Post
posts(limit: Int): [Post!]!
}
# Mutation type - write operations
type Mutation {
createUser(name: String!, email: String!): User!
updateUser(id: ID!, name: String, email: String): User!
deleteUser(id: ID!): Boolean!
createPost(title: String!, content: String!): Post!
}
# Subscription type - real-time updates
type Subscription {
postAdded: Post!
userUpdated(id: ID!): User!
}
# Built-in scalar types:
# - Int: 32-bit integer
# - Float: floating-point number
# - String: UTF-8 string
# - Boolean: true or false
# - ID: unique identifier
Queries
// Basic query
{
users {
id
name
email
}
}
// Query with arguments
{
user(id: "123") {
name
email
}
}
// Nested query
{
user(id: "123") {
name
posts {
title
comments {
text
author {
name
}
}
}
}
}
// Multiple queries
{
user1: user(id: "123") {
name
}
user2: user(id: "456") {
name
}
}
// Query with variables
query GetUser($id: ID!) {
user(id: $id) {
name
email
}
}
// Variables: { "id": "123" }
// Default values
query GetPosts($limit: Int = 10) {
posts(limit: $limit) {
title
}
}
// Fragments (reusable fields)
fragment UserInfo on User {
id
name
email
}
{
user1: user(id: "123") {
...UserInfo
}
user2: user(id: "456") {
...UserInfo
}
}
// Inline fragments (type conditions)
{
search(text: "hello") {
... on User {
name
email
}
... on Post {
title
content
}
}
}
Mutations
// Create user
mutation {
createUser(name: "John", email: "john@example.com") {
id
name
email
}
}
// Update user
mutation {
updateUser(id: "123", name: "John Doe") {
id
name
email
}
}
// Delete user
mutation {
deleteUser(id: "123")
}
// Mutation with variables
mutation CreateUser($name: String!, $email: String!) {
createUser(name: $name, email: $email) {
id
name
email
createdAt
}
}
// Variables: { "name": "John", "email": "john@example.com" }
// Multiple mutations (executed sequentially)
mutation {
user1: createUser(name: "John", email: "john@example.com") {
id
}
user2: createUser(name: "Jane", email: "jane@example.com") {
id
}
}
// Return related data after mutation
mutation {
createPost(title: "My Post", content: "Content here") {
id
title
author {
name
email
}
comments {
id
text
}
}
}
Subscriptions
// Subscribe to new posts
subscription {
postAdded {
id
title
author {
name
}
}
}
// Subscribe with variables
subscription OnUserUpdated($userId: ID!) {
userUpdated(id: $userId) {
id
name
email
}
}
// WebSocket connection required for subscriptions
// Server pushes updates to client in real-time
🌐 Making GraphQL Requests
Using Fetch
// Basic query
async function fetchUser(id) {
let response = await fetch('https://api.example.com/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
query: `
query {
user(id: "${id}") {
id
name
email
}
}
`
})
});
let { data, errors } = await response.json();
if (errors) {
console.error('GraphQL errors:', errors);
throw new Error(errors[0].message);
}
return data.user;
}
// With variables
async function fetchUser(id) {
let response = await fetch('https://api.example.com/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
query: `
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`,
variables: {
id: id
}
})
});
let { data, errors } = await response.json();
if (errors) {
throw new Error(errors[0].message);
}
return data.user;
}
// Mutation
async function createUser(name, email) {
let response = await fetch('https://api.example.com/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
query: `
mutation CreateUser($name: String!, $email: String!) {
createUser(name: $name, email: $email) {
id
name
email
}
}
`,
variables: {
name,
email
}
})
});
let { data, errors } = await response.json();
if (errors) {
throw new Error(errors[0].message);
}
return data.createUser;
}
GraphQL Client Class
class GraphQLClient {
constructor(endpoint, options = {}) {
this.endpoint = endpoint;
this.headers = options.headers || {};
}
async query(query, variables = {}) {
let response = await fetch(this.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...this.headers
},
body: JSON.stringify({
query,
variables
})
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
let result = await response.json();
if (result.errors) {
throw new GraphQLError(result.errors);
}
return result.data;
}
async mutate(mutation, variables = {}) {
return this.query(mutation, variables);
}
setHeader(key, value) {
this.headers[key] = value;
}
setAuth(token) {
this.setHeader('Authorization', `Bearer ${token}`);
}
}
class GraphQLError extends Error {
constructor(errors) {
super(errors[0].message);
this.name = 'GraphQLError';
this.errors = errors;
}
}
// Usage
let client = new GraphQLClient('https://api.example.com/graphql');
client.setAuth('your-token');
// Query
let user = await client.query(`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`, { id: '123' });
// Mutation
let newUser = await client.mutate(`
mutation CreateUser($name: String!, $email: String!) {
createUser(name: $name, email: $email) {
id
name
email
}
}
`, { name: 'John', email: 'john@example.com' });
📚 Apollo Client
// Install: npm install @apollo/client graphql
import { ApolloClient, InMemoryCache, gql } from '@apollo/client';
// Create client
let client = new ApolloClient({
uri: 'https://api.example.com/graphql',
cache: new InMemoryCache(),
headers: {
authorization: `Bearer ${token}`
}
});
// Query with gql tag
let GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
posts {
id
title
}
}
}
`;
let { data } = await client.query({
query: GET_USER,
variables: { id: '123' }
});
console.log(data.user);
// Mutation
let CREATE_USER = gql`
mutation CreateUser($name: String!, $email: String!) {
createUser(name: $name, email: $email) {
id
name
email
}
}
`;
let { data } = await client.mutate({
mutation: CREATE_USER,
variables: {
name: 'John',
email: 'john@example.com'
}
});
// Subscription
import { split, HttpLink, WebSocketLink } from '@apollo/client/core';
import { getMainDefinition } from '@apollo/client/utilities';
let httpLink = new HttpLink({
uri: 'https://api.example.com/graphql'
});
let wsLink = new WebSocketLink({
uri: 'ws://api.example.com/graphql',
options: {
reconnect: true
}
});
// Split based on operation type
let link = split(
({ query }) => {
let definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink
);
let client = new ApolloClient({
link,
cache: new InMemoryCache()
});
// Subscribe
let POSTS_SUBSCRIPTION = gql`
subscription {
postAdded {
id
title
author {
name
}
}
}
`;
let subscription = client.subscribe({
query: POSTS_SUBSCRIPTION
}).subscribe({
next: ({ data }) => {
console.log('New post:', data.postAdded);
},
error: error => {
console.error('Subscription error:', error);
}
});
💡 Practical Examples
Blog Application
class BlogAPI {
constructor(client) {
this.client = client;
}
async getPosts(limit = 10) {
return this.client.query(`
query GetPosts($limit: Int!) {
posts(limit: $limit) {
id
title
content
author {
id
name
}
comments {
id
text
author {
name
}
}
createdAt
}
}
`, { limit });
}
async getPost(id) {
return this.client.query(`
query GetPost($id: ID!) {
post(id: $id) {
id
title
content
author {
id
name
email
}
comments {
id
text
author {
name
}
createdAt
}
}
}
`, { id });
}
async createPost(title, content) {
return this.client.mutate(`
mutation CreatePost($title: String!, $content: String!) {
createPost(title: $title, content: $content) {
id
title
content
author {
name
}
createdAt
}
}
`, { title, content });
}
async addComment(postId, text) {
return this.client.mutate(`
mutation AddComment($postId: ID!, $text: String!) {
addComment(postId: $postId, text: $text) {
id
text
author {
name
}
createdAt
}
}
`, { postId, text });
}
async deletePost(id) {
return this.client.mutate(`
mutation DeletePost($id: ID!) {
deletePost(id: $id)
}
`, { id });
}
}
// Usage
let client = new GraphQLClient('https://api.example.com/graphql');
let blog = new BlogAPI(client);
let posts = await blog.getPosts(10);
let post = await blog.getPost('123');
let newPost = await blog.createPost('My Post', 'Content here');
await blog.addComment('123', 'Great post!');
Pagination
// Offset-based pagination
{
posts(offset: 0, limit: 20) {
id
title
}
}
// Cursor-based pagination (Relay-style)
{
posts(first: 20, after: "cursor") {
edges {
node {
id
title
}
cursor
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
// Fetch all pages
async function* fetchAllPosts(client) {
let cursor = null;
let hasNextPage = true;
while (hasNextPage) {
let result = await client.query(`
query GetPosts($cursor: String) {
posts(first: 20, after: $cursor) {
edges {
node {
id
title
}
cursor
}
pageInfo {
hasNextPage
endCursor
}
}
}
`, { cursor });
yield result.posts.edges.map(edge => edge.node);
hasNextPage = result.posts.pageInfo.hasNextPage;
cursor = result.posts.pageInfo.endCursor;
}
}
// Usage
for await (let posts of fetchAllPosts(client)) {
console.log('Page:', posts);
}
Caching Strategy
class CachedGraphQLClient extends GraphQLClient {
constructor(endpoint, options = {}) {
super(endpoint, options);
this.cache = new Map();
this.ttl = options.ttl || 60000; // 1 minute
}
getCacheKey(query, variables) {
return JSON.stringify({ query, variables });
}
async query(query, variables = {}) {
let key = this.getCacheKey(query, variables);
if (this.cache.has(key)) {
let { data, timestamp } = this.cache.get(key);
if (Date.now() - timestamp < this.ttl) {
return data;
}
}
let data = await super.query(query, variables);
this.cache.set(key, {
data,
timestamp: Date.now()
});
return data;
}
invalidate(pattern) {
if (pattern) {
for (let key of this.cache.keys()) {
if (key.includes(pattern)) {
this.cache.delete(key);
}
}
} else {
this.cache.clear();
}
}
}
// Usage
let client = new CachedGraphQLClient('https://api.example.com/graphql', {
ttl: 30000 // 30 seconds
});
let user = await client.query('...', { id: '123' }); // From server
let userAgain = await client.query('...', { id: '123' }); // From cache
client.invalidate('user'); // Clear user-related cache
Error Handling
async function safeQuery(client, query, variables) {
try {
return await client.query(query, variables);
} catch (error) {
if (error instanceof GraphQLError) {
// Handle specific GraphQL errors
error.errors.forEach(err => {
if (err.extensions?.code === 'UNAUTHENTICATED') {
console.log('Not authenticated, redirecting...');
redirectToLogin();
} else if (err.extensions?.code === 'FORBIDDEN') {
console.log('Access denied');
} else if (err.extensions?.code === 'NOT_FOUND') {
console.log('Resource not found');
} else {
console.error('GraphQL error:', err.message);
}
});
} else {
// Network or other errors
console.error('Request failed:', error);
}
throw error;
}
}
// Usage
try {
let user = await safeQuery(client, GET_USER, { id: '123' });
} catch (error) {
// Error already logged
}
⚡ Advanced Features
// Directives
// @include - conditionally include field
query GetUser($id: ID!, $withPosts: Boolean!) {
user(id: $id) {
name
email
posts @include(if: $withPosts) {
title
}
}
}
// @skip - conditionally skip field
query GetUser($id: ID!, $skipEmail: Boolean!) {
user(id: $id) {
name
email @skip(if: $skipEmail)
}
}
// Aliases - rename fields in result
{
admin: user(id: "1") {
name
}
moderator: user(id: "2") {
name
}
}
// Interfaces - abstract type
interface Node {
id: ID!
}
type User implements Node {
id: ID!
name: String!
}
type Post implements Node {
id: ID!
title: String!
}
// Unions - one of several types
union SearchResult = User | Post | Comment
{
search(text: "hello") {
... on User {
name
}
... on Post {
title
}
... on Comment {
text
}
}
}
// Input types - complex arguments
input CreateUserInput {
name: String!
email: String!
age: Int
}
mutation {
createUser(input: {
name: "John"
email: "john@example.com"
age: 30
}) {
id
name
}
}
🎯 Key Takeaways
- Request Exactly What You Need: No over-fetching or under-fetching
- Single Endpoint: POST to /graphql for all operations
- Strongly Typed: Schema defines structure, self-documenting
- Queries: Read operations, can be nested for related data
- Mutations: Write operations, executed sequentially
- Subscriptions: Real-time updates via WebSocket
- Variables: Pass dynamic values separately from query
- Fragments: Reusable field selections