Formatting GraphQL Queries for Readable Reviews and Playgrounds
Why compact GraphQL queries hurt reviews, how to format selection sets consistently, and a browser workflow before you paste into Apollo or GraphiQL.
A practical guide to using GraphQL in frontend applications — writing queries and mutations, client-side caching with Apollo and urql, and patterns for React integration.
By codefunc
GraphQL is an alternative to REST APIs that lets you request exactly the data you need. Instead of multiple endpoints returning fixed shapes, you query a single endpoint with a flexible query language. For frontend developers, this means less over-fetching, fewer requests, and better typing.
Bottom line: GraphQL shines when you need flexible data fetching across related entities. Use Apollo Client or urql for React apps — they handle caching, loading states, and refetching. Write queries that request only needed fields — format them with the GraphQL Formatter before review. Understand the normalized cache to avoid stale data issues. For simple CRUD apps, REST might be simpler; for complex data relationships, GraphQL pays off.
| Aspect | REST | GraphQL |
|---|---|---|
| Endpoints | Multiple (/users, /posts) |
Single (/graphql) |
| Response shape | Fixed by server | Defined by client |
| Over-fetching | Common | Avoided |
| Under-fetching | Requires multiple requests | Single request |
| Typing | Manual or OpenAPI | Built-in schema |
| Caching | HTTP caching | Client-side normalized |
Queries fetch data:
query GetUser {
user(id: "123") {
id
name
email
posts {
id
title
}
}
}
Response:
{
"data": {
"user": {
"id": "123",
"name": "Ada Lovelace",
"email": "ada@example.com",
"posts": [
{ "id": "1", "title": "First Post" },
{ "id": "2", "title": "Second Post" }
]
}
}
}
Mutations modify data:
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
createdAt
}
}
Variables:
{
"input": {
"title": "New Post",
"body": "Content here..."
}
}
Reuse field selections:
fragment UserFields on User {
id
name
email
avatarUrl
}
query GetUsers {
users {
...UserFields
}
}
query GetCurrentUser {
me {
...UserFields
settings {
theme
}
}
}
Apollo is the most popular GraphQL client for React.
npm install @apollo/client graphql
// lib/apollo.ts
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
const client = new ApolloClient({
link: new HttpLink({
uri: 'https://api.example.com/graphql',
headers: {
authorization: `Bearer ${getToken()}`,
},
}),
cache: new InMemoryCache(),
});
export default client;
// app/layout.tsx or App.tsx
import { ApolloProvider } from '@apollo/client';
import client from '@/lib/apollo';
function App({ children }) {
return (
<ApolloProvider client={client}>
{children}
</ApolloProvider>
);
}
import { useQuery, gql } from '@apollo/client';
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
posts {
id
title
}
}
}
`;
function UserProfile({ userId }) {
const { data, loading, error } = useQuery(GET_USER, {
variables: { id: userId },
});
if (loading) return <Spinner />;
if (error) return <Error message={error.message} />;
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>
);
}
import { useMutation, gql } from '@apollo/client';
const CREATE_POST = gql`
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
createdAt
}
}
`;
function CreatePostForm() {
const [createPost, { loading, error }] = useMutation(CREATE_POST, {
refetchQueries: ['GetPosts'], // Refetch after mutation
onCompleted: (data) => {
console.log('Created:', data.createPost);
},
});
const handleSubmit = async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
await createPost({
variables: {
input: {
title: formData.get('title'),
body: formData.get('body'),
},
},
});
};
return (
<form onSubmit={handleSubmit}>
<input name="title" placeholder="Title" required />
<textarea name="body" placeholder="Body" required />
<button type="submit" disabled={loading}>
{loading ? 'Creating...' : 'Create Post'}
</button>
{error && <p className="error">{error.message}</p>}
</form>
);
}
Apollo automatically updates cached entities by id and __typename:
const UPDATE_POST = gql`
mutation UpdatePost($id: ID!, $input: UpdatePostInput!) {
updatePost(id: $id, input: $input) {
id
title
body
updatedAt
}
}
`;
// Cache updates automatically because response includes id
For creates and deletes, update cache manually:
const [createPost] = useMutation(CREATE_POST, {
update(cache, { data: { createPost } }) {
cache.modify({
fields: {
posts(existingPosts = []) {
const newPostRef = cache.writeFragment({
data: createPost,
fragment: gql`
fragment NewPost on Post {
id
title
createdAt
}
`,
});
return [...existingPosts, newPostRef];
},
},
});
},
});
Show expected result immediately:
const [updatePost] = useMutation(UPDATE_POST, {
optimisticResponse: {
updatePost: {
__typename: 'Post',
id: postId,
title: newTitle,
body: newBody,
updatedAt: new Date().toISOString(),
},
},
});
urql is a lighter alternative to Apollo.
npm install urql graphql
import { createClient, Provider } from 'urql';
const client = createClient({
url: 'https://api.example.com/graphql',
fetchOptions: () => ({
headers: { authorization: `Bearer ${getToken()}` },
}),
});
function App({ children }) {
return <Provider value={client}>{children}</Provider>;
}
import { useQuery } from 'urql';
const GET_POSTS = `
query GetPosts {
posts {
id
title
}
}
`;
function PostList() {
const [result] = useQuery({ query: GET_POSTS });
const { data, fetching, error } = result;
if (fetching) return <Spinner />;
if (error) return <Error message={error.message} />;
return (
<ul>
{data.posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
import { useMutation } from 'urql';
const CREATE_POST = `
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
}
}
`;
function CreatePost() {
const [result, createPost] = useMutation(CREATE_POST);
const handleCreate = () => {
createPost({ input: { title: 'New Post', body: '...' } });
};
return (
<button onClick={handleCreate} disabled={result.fetching}>
Create Post
</button>
);
}
Use GraphQL Code Generator for type safety:
npm install -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typed-document-node
# codegen.yml
schema: "https://api.example.com/graphql"
documents: "src/**/*.graphql"
generates:
src/generated/graphql.ts:
plugins:
- typescript
- typescript-operations
- typed-document-node
npx graphql-codegen
// src/queries/user.graphql
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
// Generated types are used automatically
import { useQuery } from '@apollo/client';
import { GetUserDocument } from '@/generated/graphql';
function UserProfile({ userId }) {
const { data } = useQuery(GetUserDocument, {
variables: { id: userId },
});
// data.user is fully typed
return <h1>{data?.user?.name}</h1>;
}
query GetPosts($first: Int!, $after: String) {
posts(first: $first, after: $after) {
edges {
node {
id
title
}
cursor
}
pageInfo {
hasNextPage
endCursor
}
}
}
function PostList() {
const { data, fetchMore, loading } = useQuery(GET_POSTS, {
variables: { first: 10 },
});
const loadMore = () => {
fetchMore({
variables: {
after: data.posts.pageInfo.endCursor,
},
});
};
return (
<>
<ul>
{data?.posts.edges.map(({ node }) => (
<li key={node.id}>{node.title}</li>
))}
</ul>
{data?.posts.pageInfo.hasNextPage && (
<button onClick={loadMore} disabled={loading}>
Load More
</button>
)}
</>
);
}
const client = new ApolloClient({
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
posts: {
keyArgs: false,
merge(existing = { edges: [] }, incoming) {
return {
...incoming,
edges: [...existing.edges, ...incoming.edges],
};
},
},
},
},
},
}),
});
Real-time updates via WebSocket:
import { useSubscription, gql } from '@apollo/client';
const POST_CREATED = gql`
subscription OnPostCreated {
postCreated {
id
title
author {
name
}
}
}
`;
function LiveFeed() {
const { data, loading } = useSubscription(POST_CREATED);
if (loading) return <p>Waiting for posts...</p>;
return (
<div>
<p>New post: {data.postCreated.title}</p>
</div>
);
}
import { split, HttpLink } from '@apollo/client';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
import { getMainDefinition } from '@apollo/client/utilities';
const httpLink = new HttpLink({ uri: '/graphql' });
const wsLink = new GraphQLWsLink(
createClient({
url: 'wss://api.example.com/graphql',
})
);
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink
);
import { useQuery } from '@apollo/client';
function UserProfile({ userId }) {
const { data, loading, error } = useQuery(GET_USER, {
variables: { id: userId },
errorPolicy: 'all', // Return partial data with errors
});
// Network error
if (error?.networkError) {
return <p>Network error. Check your connection.</p>;
}
// GraphQL errors (validation, authorization)
if (error?.graphQLErrors.length) {
return (
<ul>
{error.graphQLErrors.map((err, i) => (
<li key={i}>{err.message}</li>
))}
</ul>
);
}
return <div>{data?.user?.name}</div>;
}
Keep queries near components that use them:
components/
UserProfile/
index.tsx
UserProfile.graphql
UserProfile.test.tsx
Components own their data requirements:
// UserAvatar.tsx
export const USER_AVATAR_FRAGMENT = gql`
fragment UserAvatar on User {
id
name
avatarUrl
}
`;
function UserAvatar({ user }) {
return <img src={user.avatarUrl} alt={user.name} />;
}
// Parent component composes fragments
const GET_USER = gql`
${USER_AVATAR_FRAGMENT}
query GetUser($id: ID!) {
user(id: $id) {
...UserAvatar
email
createdAt
}
}
`;
Request only needed fields:
# Bad: fetching everything
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
phone
address
company
posts { ... }
comments { ... }
followers { ... }
}
}
# Good: only what's displayed
query GetUserCard($id: ID!) {
user(id: $id) {
id
name
avatarUrl
}
}
Use the JSON Formatter to inspect GraphQL responses. Validate response structure with JSON Validator. Generate TypeScript types from responses with JSON to TypeScript.
Browser extension shows:
GraphQL is worth learning for apps with complex data relationships. Choose Apollo for full features and ecosystem, urql for simplicity. Use code generation for type safety. Keep queries co-located with components. Understand the normalized cache — it's powerful but has gotchas. Fragment co-location keeps data requirements maintainable. For simple REST-like CRUD, the GraphQL overhead may not be justified.
Why compact GraphQL queries hurt reviews, how to format selection sets consistently, and a browser workflow before you paste into Apollo or GraphiQL.
A practical guide to animations in React — CSS transitions and keyframes, Framer Motion for complex interactions, performance optimization, and accessibility considerations.
A practical guide to authentication for frontend developers — understanding OAuth flows, JWT structure, session cookies, and implementing secure auth in React applications.
Find a developer tool