GraphQL gives clients precise control over what data they fetch — no more over-fetching (getting unused fields) or under-fetching (making multiple requests). Firebase supports using GraphQL in browsers; Firefox DevTools makes it easy to inspect GraphQL queries. This guide covers client-side GraphQL with practical patterns.
What is GraphQL?
GraphQL is a query language for APIs. Instead of multiple REST endpoints, you send a single query describing exactly what you need:
# REST: GET /user/1, GET /user/1/posts, GET /post/5/comments — 3 requests
# GraphQL: one request, get everything
query {
user(id: 1) {
name
email
posts(limit: 5) {
title
publishedAt
comments {
text
author {
name
}
}
}
}
}
The server returns exactly this shape — no extra fields, no missing fields.
Making GraphQL Requests
GraphQL uses standard HTTP POST:
async function graphqlRequest(query, variables = {}) {
const response = await fetch('https://api.example.com/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${getToken()}`,
},
body: JSON.stringify({ query, variables }),
});
const { data, errors } = await response.json();
if (errors) {
throw new GraphQLError(errors);
}
return data;
}
// Usage
const { user } = await graphqlRequest(`
query GetUser($id: ID!) {
user(id: $id) {
name
email
}
}
`, { id: '123' });
Queries vs. Mutations vs. Subscriptions
# Query: read data (GET equivalent)
query GetTodos {
todos {
id
title
completed
}
}
# Mutation: write data (POST/PUT/DELETE equivalent)
mutation CreateTodo($title: String!) {
createTodo(input: { title: $title }) {
id
title
completed
}
}
# Subscription: real-time data (WebSocket)
subscription OnTodoUpdated {
todoUpdated {
id
title
completed
}
}
Fragments: Reusable Field Sets
fragment UserFields on User {
id
name
email
avatarUrl
}
query GetUsers {
users {
...UserFields
posts {
title
}
}
}
query GetUser($id: ID!) {
user(id: $id) {
...UserFields
bio
}
}
Using Apollo Client
Apollo Client is the most popular GraphQL client for React:
import { ApolloClient, InMemoryCache, gql, useQuery, useMutation } from '@apollo/client';
const client = new ApolloClient({
uri: 'https://api.example.com/graphql',
cache: new InMemoryCache(),
headers: { Authorization: `Bearer ${token}` },
});
// React hooks
const GET_TODOS = gql`
query GetTodos {
todos { id title completed }
}
`;
function TodoList() {
const { loading, error, data } = useQuery(GET_TODOS);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<ul>
{data.todos.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}
Using urql (Lighter Alternative)
import { createClient, cacheExchange, fetchExchange } from 'urql';
const client = createClient({
url: 'https://api.example.com/graphql',
exchanges: [cacheExchange, fetchExchange],
});
Caching Strategies
GraphQL clients cache query results. Apollo's InMemoryCache normalizes by id:
const cache = new InMemoryCache({
typePolicies: {
Todo: {
keyFields: ['id'], // Normalize todos by id
},
Query: {
fields: {
todos: {
// Merge paginated results
merge(existing = [], incoming) {
return [...existing, ...incoming];
},
},
},
},
},
});
Optimistic Updates
Show UI changes immediately, before server confirms:
const [updateTodo] = useMutation(UPDATE_TODO, {
optimisticResponse: {
updateTodo: {
__typename: 'Todo',
id: todo.id,
completed: !todo.completed, // Assume success
},
},
// If mutation fails, cache automatically reverts
});
Debugging GraphQL in Firefox
Firefox DevTools Network panel shows GraphQL requests:
- Open DevTools → Network → filter to "XHR"
- Click any GraphQL request
- Request body shows the query + variables as JSON
- Response shows the data/errors
To see the full query:
POST https://api.example.com/graphql
Body: {"query":"query GetTodos { todos { id title } }","variables":{}}
For complex GraphQL operations with Apollo, the Apollo DevTools browser extension adds a dedicated panel showing:
- Active queries and their results
- Cache contents
- Mutation history
Error Handling
GraphQL returns HTTP 200 even for errors — check the errors field:
const { data, errors } = await response.json();
if (errors) {
errors.forEach(error => {
console.error(error.message);
console.error(error.locations); // Line/column in query
console.error(error.path); // Which field caused the error
console.error(error.extensions?.code); // Custom error code
});
}
// Partial success: data can be non-null even when errors exist
// (partial results with errors for failed fields)
GraphQL vs REST: When to Choose
Choose GraphQL when:
- Frontend needs vary across clients (mobile vs. web vs. partner API)
- Over-fetching is causing performance issues
- Rapid product iteration (add fields without API versioning)
- Real-time subscriptions needed
Choose REST when:
- Simple CRUD with stable schemas
- Third-party integration (REST is universal)
- HTTP caching is important (GET requests are cacheable; GraphQL POST is not)
- Team is less familiar with GraphQL
Many mature products use both: REST for stable public APIs, GraphQL for internal client-server communication.
Top comments (0)