GraphQL Conventions Worth Knowing

GraphQL is a declarative query language backed by a formal specification. It uses a strong type schema for your API, which lets clients request exactly the data they need in the shape they want. A few conventions are essential before we start working with Apollo Client:

  • Operations. Every action in GraphQL is an operation. Queries fetch data (read-only), mutations create, update, or delete data, and subscriptions push data from the server to clients when events occur, usually over WebSockets. In this article, we'll work only with queries and mutations.
  • Operation names. Client-side queries and mutations should have unique, descriptive names.
  • Variables and arguments. Operations can accept arguments like functions. You define variables within an operation and pass their values at runtime from the client.
  • Aliasing. You can rename verbose or vague field names with clearer names for the UI. Aliasing helps avoid conflicts when several fields return with the same name.
GraphQL basic conventions
GraphQL basic conventions. (Large preview)

Why Client-Side GraphQL?

In REST, a client fetches from endpoints that return fixed structures. That leads to overfetching — downloading more data than the app needs — or underfetching, where you must make additional requests to get what you actually require. GraphQL solves this by letting the client send a single query describing precisely which fields are needed. The server responds with JSON matching that exact shape.

Client-side GraphQL is the infrastructure on the front end that interfaces with a GraphQL server. It manages several jobs for you:

  • Data management. You can send queries and mutations without constructing HTTP requests by hand, freeing time to build the application itself.
  • Caching. Fetched data is stored and retrieved intelligently so duplicate resources aren't re-requested. The cache can recognize when two resources are the same, which is valuable in a complex app.
  • Optimistic UI. This technique simulates a mutation's result and updates the UI before the server responds. When the real response arrives, the simulated data is replaced with actual data, keeping the interface consistent.

Introduction to Apollo Client

Apollo Client is a community-driven GraphQL client for JavaScript and native platforms. It is interoperable with other frameworks (React, Angular, Vue.js) and brings a strong set of features to the table: a state-management tool called Apollo Link, a zero-config caching system, declarative data fetching, pagination support, and Optimistic UI. Apollo Client manages state for both server-fetched data and locally created data. It can also coexist with other state-management tools like Redux without conflict.

Key built-in capabilities worth highlighting:

  • Caching. Apollo Client caches data on the fly with no extra configuration.
  • Optimistic UI. The UI temporarily renders the final state of an operation while the mutation is in flight; the real server data replaces it when complete.
  • Pagination. The fetchMore function from the useQuery hook handles the technical work of fetching lists, either in batches or in one go.

Building the Pet Shop App

Our project is a simple pet shop. We will implement four features: fetching pets from the server, creating a pet with a name, type, and image, applying Optimistic UI, and using pagination to segment data.

To get started:

  1. Clone the repository on the starter branch.
  2. Install the Apollo Client Developer Tools extension for Chrome.
  3. Run npm install to get dependencies.
  4. Run npm run app to start the client.
  5. Run npm run server to start the back-end server.

The app opens at a configured port (often https://localhost:1234/). You will notice no pets are displayed yet — that functionality is what we'll build next.

Cloned starter branch UI
Cloned starter branch UI. (Large preview)

Once the Apollo Client Developer Tools extension is installed, open the developer tools and click the Apollo tray icon. You should see the extension's panel, which includes the GraphQL Playground for writing and testing queries and mutations before using them in the app.

Apollo Client Developer Tools
Apollo Client Developer Tools. (Large preview)

Querying With Apollo Client

Set up the Apollo Client instance in client/src/client.js. This module creates the client, connects it to the API, and exports it for use throughout the application.

import { ApolloClient } from 'apollo-client'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { HttpLink } from 'apollo-link-http'

const link = new HttpLink({ uri: 'https://localhost:4000/' })
const cache = new InMemoryCache()
const client = new ApolloClient({
  link,
  cache
})
export default client

Here is what each piece does:

  • ApolloClient is the core wrapper that handles HTTP communication, data caching, and UI updates.
  • InMemoryCache provides the normalized data store Apollo Client uses to manage cached data.
  • HttpLink is the standard network interface for GraphQL requests. It works as middleware and fetches results from the GraphQL server. It replaces the need for libraries such as Axios or window.fetch.

The link variable holds an HttpLink instance pointing to the server at https://localhost:4000/. The cache variable is a new InMemoryCache instance. The exported client wraps both in ApolloClient.

To make Apollo available across the entire component tree, wrap the app in ApolloProvider inside client/src/index.js. This provider works like React’s Context.Provider, placing the client in context so any component can access it.

import React from 'react'
import ReactDOM from 'react-dom'
import { BrowserRouter } from 'react-router-dom'
import { ApolloProvider } from '@apollo/react-hooks'
import App from './components/App'
import client from './client'
import './index.css'
const Root = () => (
  <BrowserRouter>
    <ApolloProvider client={client}>
      <App />
    </ApolloProvider>
  </BrowserRouter>
);
ReactDOM.render(<Root />, document.getElementById('app'))
if (module.hot) {
  module.hot.accept()
}

With the client in place, define a query in client/src/pages/Pets.js that requests only the fields you need.

import React, {useState} from 'react'
import gql from 'graphql-tag'
import { useQuery, useMutation } from '@apollo/react-hooks'
import PetsList from '../components/PetsList'
import NewPetModal from '../components/NewPetModal'
import Loader from '../components/Loader'

const GET_PETS = gql`
  query getPets {
    pets {
      id
      name
      type
      img
    }
  }
`;

export default function Pets () {
  const [modal, setModal] = useState(false)
  const { loading, error, data } = useQuery(GET_PETS);

  if (loading) return <Loader />;

  if (error) return <p>An error occured!</p>;

  const onSubmit = input => {
    setModal(false)
  }
  

  if (modal) {
    return <NewPetModal onSubmit={onSubmit} onCancel={() => setModal(false)} />
  }
  return (
    <div className="page pets-page">
      <section>
        <div className="row betwee-xs middle-xs">
          <div className="col-xs-10">
            <h1>Pets</h1>
          </div>
          <div className="col-xs-2">
            <button onClick={() => setModal(true)}>new pet</button>
          </div>
        </div>
      </section>
      <section>
        <PetsList pets={data.pets}/>
      </section>
    </div>
  )
}

The gql Tag

GraphQL operations are written as strings parsed with the gql template literal tag. This tag converts the query string into a GraphQL AST (abstract syntax tree).

  • A query operation fetches pets from the server. The operation must declare its type before its name. Here the query is named GET_PETS and requests the pets field with the specific subfields (id, name, type, img). Field names follow GraphQL’s camelCase convention.
  • useQuery, imported from @apollo/react-hooks, is the React hook for executing queries. Pass the GET_PETS query string to it.
  • When the component renders, useQuery returns an object with loading, error, and data properties, which are destructured to drive the UI.
  • The hook handles async state internally, so no async-await is needed in the component.

The response properties work as follows:

  • loading is false by default and is used to render a Loader component while data is fetched.
  • error captures any request failures.
  • data carries the actual server response. The PetsList component receives pets as a prop set to data.pets.

To run the full application, start the client with npm run app and the server with npm run server in separate terminals.

VScode CLI partitioned to start both the client and the server.
VScode CLI partitioned to start both the client and the server. (Large preview)
Pets queried from the server.
Pets queried from the server.

Creating Data With Mutations

Mutations follow the same pattern as queries with minor differences. In client/src/pages/Pets.js, add the mutation setup.

....

const GET_PETS = gql`
  query getPets {
    pets {
      id
      name
      type
      img
    }
  }
`;

const NEW_PETS = gql`
  mutation CreateAPet($newPet: NewPetInput!) {
    addPet(input: $newPet) {
      id
      name
      type
      img
    }
  }
`;

  const Pets = () => {
  const [modal, setModal] = useState(false)
  const { loading, error, data } = useQuery(GET_PETS);
  const [createPet, newPet] = useMutation(NEW_PETS);
  const onSubmit = input => {
    setModal(false)
    createPet({
      variables: { newPet: input }
    });
  }

  if (loading || newPet.loading) return <Loader />;
  
  if (error || newPet.error) return <p>An error occured</p>;
  
  if (modal) {
    return <NewPetModal onSubmit={onSubmit} onCancel={() => setModal(false)} />
  }
  return (
    <div className="page pets-page">
      <section>
        <div className="row betwee-xs middle-xs">
          <div className="col-xs-10">
            <h1>Pets</h1>
          </div>
          <div className="col-xs-2">
            <button onClick={() => setModal(true)}>new pet</button>
          </div>
        </div>
      </section>
      <section>
        <PetsList pets={data.pets}/>
      </section>
    </div>
  )
}

export default Pets

1. The mutation Operation

For create, update, or delete actions, use the mutation operation type. This example defines CreateAPet with a required argument $newPet of type NewPetInput. The ! suffix makes the variable mandatory.

2. The addPet Function

Inside the mutation, addPet accepts an input argument mapped to $newPet. The returned fields must match the query set: id, name, type, and img.

3. The useMutation Hook

useMutation is the primary hook for mutations. Call it with the NEW_PETS GraphQL string. The hook returns a tuple containing a mutate function and an object describing the mutation’s current state. The destructured function is named createPet, and the state object is newPets.

4. The createPet Function

Inside the onSubmit handler, after the modal state is set, createPet is called with variables set to { newPet: input }. The input value holds the form fields such as name and type.

Mutation without instant update
Mutation without instant update.

Notice that the newly created pet does not appear immediately in the UI — only after a page refresh. The server data is updated, but the client view is not. The next section explains why.

Managing Cache Synchronization

The new pet is not rendered instantly because the newly created data does not align with the cache Apollo Client is using. When a mutation creates or deletes multiple entries, you must update any queries that reference those entries so the cached data matches the back-end state.

One approach is to refetch affected queries using refetchQueries. In createPet, you would add refetchQueries: [{ query: GET_PETS }]. This approach is the simplest but bypasses direct cache manipulation, which is the focus here.

The preferred approach is the update helper function. Apollo Client’s update function reads and writes directly to the cache, syncing it with the mutation’s server-side effects.

Writing to the Cache

Add the cache update logic in client/src/pages/Pets.js.

......
const Pets = () => {
  const [modal, setModal] = useState(false)
  const { loading, error, data } = useQuery(GET_PETS);
  const [createPet, newPet] = useMutation(NEW_PETS, {
    update(cache, { data: { addPet } }) {
      const data = cache.readQuery({ query: GET_PETS });
      cache.writeQuery({
        query: GET_PETS,
        data: { pets: [addPet, ...data.pets] },
      });
    },
    }
  );
  .....

The update function takes two arguments. The first is the Apollo cache instance. The second is the mutation response; the data property is destructured and aliased to the addPet mutation result.

Identify the query to update — here, GET_PETS — and read its cached data. Then write back to that query by passing an object with the query set to GET_PETS and a data field containing the pets array that appends the new addPet result to the existing pet list.

With this update function in place, newly created pets appear automatically without a refresh.

Pets updates instantly
Pets updates instantly.

Optimistic UI: Faster feedback without spinners

Loaders and spinners still have their place, but for mutations whose outcome is predictable, waiting on the network is unnecessary friction. Optimistic UI is the convention of rendering the expected result of a mutation immediately, then replacing that temporary state with the server's real response when it arrives. Apollo Client supports this pattern directly through the cache, so the UI updates synchronously while the request is still in flight.

Delaying the network for demonstration

To see the effect clearly, the example first introduces an artificial delay on the mutation link. In client/src/client.js, import setContext from apollo-link-context. The callback passed to setContext returns a promise whose setTimeout is set to 800ms, and ApolloLink.from applies that delay to the HTTP link.

import { ApolloClient } from 'apollo-client'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { HttpLink } from 'apollo-link-http'
import { setContext } from 'apollo-link-context'
import { ApolloLink } from 'apollo-link'
const http = new HttpLink({ uri: "https://localhost:4000/" });
const delay = setContext(
  request => 
    new Promise((success, fail) => {
      setTimeout(() => {
        success()
      }, 800)
    })
)
const link = ApolloLink.from([
  delay,
  http
])
const cache = new InMemoryCache()
const client = new ApolloClient({
  link,
  cache
})
export default client

Writing the optimistic response

With the delay in place, the optimistic update is configured in client/src/pages/Pets.js by adding an optimisticResponse object to the mutation call. When the user creates a pet, Apollo writes this object to the cache immediately, before the server responds.

.....

const Pets = () => {
  const [modal, setModal] = useState(false)
  const { loading, error, data } = useQuery(GET_PETS);
  const [createPet, newPet] = useMutation(NEW_PETS, {
    update(cache, { data: { addPet } }) {
      const data = cache.readQuery({ query: GET_PETS });
      cache.writeQuery({
        query: GET_PETS,
        data: { pets: [addPet, ...data.pets] },
      });
    },
    }
  );
  const onSubmit = input => {
    setModal(false)
    createPet({
      variables: { newPet: input },
      optimisticResponse: {
        __typename: 'Mutation',
        addPet: {
          __typename: 'Pet',
          id: Math.floor(Math.random() * 10000 + ''),
          name: input.name,
          type: input.type,
          img: 'https://via.placeholder.com/200'
        }
      }
    });
  }
  .....

That object mirrors the shape of the mutation result:

  • __typename: Apollo injects this into queries to know the type of each entity; it uses those types to build cache identifiers. The optimistic response must set it — for the mutation named addPet, the type is Pet.
  • id: Since the server-generated ID isn't known yet, one can be fabricated with Math.floor.
  • name: Taken directly from input.name.
  • type: Likewise set from input.type.
  • img: The server normally generates images, so a placeholder is used to stand in until the real URL arrives.

Once the mutation resolves, Apollo discards the optimistic entry and replaces it with the actual server response, keeping the cache consistent.

Final Outcome of the pet shop app
Final result of our app.

Where to go from here

Combined with pagination and the other cache features covered earlier, optimistic UI makes Apollo Client a practical choice for data-heavy React applications. The hooks API keeps the setup declarative, and the same library works with Vue and Angular if you switch frameworks later.

This walkthrough has only touched the surface. The supporting repository is available on GitHub — cloning it and adding pagination or experimenting with the cache is a reasonable next step for practicing these patterns.