Skip to main content
{/}codefunc
GuidesJuly 7, 2026 · 8 min read

GraphQL for Frontend Developers: Queries, Mutations, and Caching

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

graphqlapolloreactapifrontendcaching

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.

GraphQL vs REST

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

Basic concepts

Queries

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

Mutations modify data:

mutation CreatePost($input: CreatePostInput!) {
  createPost(input: $input) {
    id
    title
    createdAt
  }
}

Variables:

{
  "input": {
    "title": "New Post",
    "body": "Content here..."
  }
}

Fragments

Reuse field selections:

fragment UserFields on User {
  id
  name
  email
  avatarUrl
}

query GetUsers {
  users {
    ...UserFields
  }
}

query GetCurrentUser {
  me {
    ...UserFields
    settings {
      theme
    }
  }
}

Apollo Client

Apollo is the most popular GraphQL client for React.

Setup

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>
  );
}

useQuery hook

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>
  );
}

useMutation hook

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>
  );
}

Cache updates

Automatic updates

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

Manual cache updates

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];
        },
      },
    });
  },
});

Optimistic updates

Show expected result immediately:

const [updatePost] = useMutation(UPDATE_POST, {
  optimisticResponse: {
    updatePost: {
      __typename: 'Post',
      id: postId,
      title: newTitle,
      body: newBody,
      updatedAt: new Date().toISOString(),
    },
  },
});

urql

urql is a lighter alternative to Apollo.

Setup

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>;
}

useQuery

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>
  );
}

useMutation

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>
  );
}

TypeScript integration

Code generation

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

Typed queries

// 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>;
}

Pagination

Cursor-based pagination

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>
      )}
    </>
  );
}

Apollo cache merge

const client = new ApolloClient({
  cache: new InMemoryCache({
    typePolicies: {
      Query: {
        fields: {
          posts: {
            keyArgs: false,
            merge(existing = { edges: [] }, incoming) {
              return {
                ...incoming,
                edges: [...existing.edges, ...incoming.edges],
              };
            },
          },
        },
      },
    },
  }),
});

Subscriptions

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>
  );
}

WebSocket setup

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
);

Error handling

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>;
}

Best practices

Query co-location

Keep queries near components that use them:

components/
  UserProfile/
    index.tsx
    UserProfile.graphql
    UserProfile.test.tsx

Fragment co-location

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
    }
  }
`;

Avoid over-fetching

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
  }
}

Debugging

Use the JSON Formatter to inspect GraphQL responses. Validate response structure with JSON Validator. Generate TypeScript types from responses with JSON to TypeScript.

Apollo DevTools

Browser extension shows:

  • Active queries
  • Cache contents
  • Mutations
  • Network requests

Practical takeaway

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.

Sources

Try it on codefunc

Related articles

3

Search tools

Find a developer tool