and Tooling in Practice
Replacing REST endpoints is not the goal of GraphQL, the goal is to load exactly the data a component needs and cache it at the right moment. Anyone running Vue GraphQL without a cache strategy loads the same data over and over and loses the biggest advantage of the approach.
Table of Contents
- 1. Why Vue and GraphQL fit together so well
- 2. Setting up Apollo Client in Vue 3 correctly
- 3. Structuring queries: useQuery and composables
- 4. InMemoryCache: normalization and cache policies
- 5. Fragments: making queries modular and reusable
- 6. Mutations and optimistic updates
- 7. Handling errors and loading states correctly
- 8. Tooling: Apollo DevTools, GraphQL Codegen, VSCode
- 9. Strategies compared: fetch-policy options
- 10. Summary
- 11. FAQ
1. Why Vue and GraphQL fit together so well
Vue GraphQL is more than a technology combination, it is a paradigm shift in how components consume data. In a classic REST architecture, the server defines which data an endpoint returns. In a Vue GraphQL architecture, the component itself defines which fields it needs. That leads to less over-fetching, less under-fetching, and a directly leaner JavaScript payload in the browser.
Vue's component-based structure fits particularly well with GraphQL's fragment architecture. Each component declares its own data requirements as a fragment, and Apollo Client assembles these fragments into complete queries. The result is that refactoring a component, adding or removing fields, automatically adjusts the query without a backend developer having to implement a new endpoint. For teams iterating quickly, Vue GraphQL is a significant speed gain.
At the same time, Vue GraphQL brings genuine complexity with it: cache invalidation, optimistic updates, subscription handling and a proper understanding of the normalized cache are topics that quickly trip up developers who are not prepared. This article covers all of these areas systematically and shows how to build a Vue GraphQL application that stays maintainable a year later without a fundamental restructuring.
2. Setting up Apollo Client in Vue 3 correctly
Getting started with Vue GraphQL begins with correctly configuring Apollo Client. The package @apollo/client together with @vue/apollo-composable forms the foundation. What matters is that Apollo Client is wired into the Vue application as a plugin, with an InMemoryCache instance that is already configured for your own schema structure from the start. A common mistake: the InMemoryCache is instantiated without typePolicies, which means Apollo cannot normalize the cache correctly once entities without a default id field are returned.
The ApolloLink is Apollo Client's middleware system. Typically the link chain consists of an authLink that attaches the Authorization header, and an httpLink that executes the actual HTTP request. For WebSocket-based subscriptions, a splitLink is added that automatically routes HTTP requests and WebSocket connections to the correct transport channel. Anyone who sets up this configuration cleanly avoids hard-to-debug authentication problems and race conditions during token refresh later on.
// src/plugins/apollo.js
// Apollo Client setup for Vue 3 with auth link and cache policies
import { ApolloClient, InMemoryCache, createHttpLink, from } from '@apollo/client/core'
import { setContext } from '@apollo/client/link/context'
import { provideApolloClient } from '@vue/apollo-composable'
const httpLink = createHttpLink({
uri: import.meta.env.VITE_GRAPHQL_URL,
})
// Attach JWT token to every request
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem('auth_token')
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : '',
},
}
})
const cache = new InMemoryCache({
typePolicies: {
// Normalize Product by slug, not by id
Product: {
keyFields: ['slug'],
},
// Merge paginated lists instead of replacing them
Query: {
fields: {
products: {
keyArgs: ['filter', 'category'],
merge(existing = { items: [] }, incoming) {
return { ...incoming, items: [...existing.items, ...incoming.items] }
},
},
},
},
},
})
export const apolloClient = new ApolloClient({
link: from([authLink, httpLink]),
cache,
defaultOptions: {
watchQuery: { fetchPolicy: 'cache-and-network' },
query: { fetchPolicy: 'network-only', errorPolicy: 'all' },
},
})
// Install into Vue app
export function installApollo(app) {
provideApolloClient(apolloClient)
}
3. Structuring queries: useQuery and composables
In Vue GraphQL with the Composition API, useQuery from @vue/apollo-composable is the central function for loading data. useQuery returns reactive refs: result holds the response data, loading shows the loading state, error holds any errors that occurred. These refs integrate seamlessly into Vue 3's reactive system, so templates update automatically as soon as new data arrives. The query is reactive, if a variable changes, Apollo automatically performs a new fetch.
Queries do not belong directly in components, but in dedicated composables in the src/composables/ folder. That has several advantages: the composable can be reused across multiple components, the query logic is testable without rendering a component, and the component itself stays lean and focused on presentation. The pattern useProduct(slug) as a composable that internally calls useQuery and only exposes { product, loading, error } to the outside is the standard pattern in well-structured Vue GraphQL projects.
4. InMemoryCache: normalization and cache policies
The InMemoryCache is the heart of every Vue GraphQL application built with Apollo Client. It works as a normalized, flat data store: each entity is stored under a key made up of type and ID, for example Product:42. When multiple queries return the same entity, it is stored only once in the cache. Every component subscribed to that entity automatically receives the update as soon as a mutation changes the entity. This concept is the decisive difference between Apollo-based Vue GraphQL and a plain fetch call inside a composable.
The typePolicies configuration is the key to a correctly functioning cache. With keyFields you define which fields make up an entity's unique key. With merge functions you control how incoming data is merged with existing data, crucial for pagination, where new pages should be appended to existing lists. With read functions you transform data when it is read from the cache, for example to add client-side computed fields. Anyone who configures typePolicies carefully builds a Vue GraphQL application in which data consistency is guaranteed automatically.
5. Fragments: making queries modular and reusable
GraphQL fragments are the means, in Vue GraphQL projects, of keeping queries modular and maintainable. A fragment defines a named selection of fields for a particular type. The component rendering that data defines its fragment, and the parent query includes that fragment. The result: when the product card component needs a new field, only the product card's fragment is adjusted, the query on the listing page that includes the fragment updates automatically.
Apollo Client can also read and write fragment data directly from the cache without executing a full query. The method cache.readFragment reads an entity from the cache by type and ID. cache.writeFragment writes data into the cache without executing a network request. This pattern is essential for optimistic updates: you write the expected response into the cache before the mutation is even sent, and the component immediately shows the new state, only once the mutation completes is the cache overwritten with the actual server response.
// src/graphql/fragments/product.js
// Reusable product fragment, import in any query that needs product data
import { gql } from '@apollo/client/core'
export const PRODUCT_CARD_FRAGMENT = gql`
fragment ProductCard on Product {
id
slug
name
price {
amount
currency
}
thumbnail {
url
altText
}
inStock
}
`
export const PRODUCT_DETAIL_FRAGMENT = gql`
fragment ProductDetail on Product {
...ProductCard
description
images {
url
altText
width
height
}
variants {
id
sku
attributes { name value }
price { amount currency }
}
}
${PRODUCT_CARD_FRAGMENT}
`
// src/composables/useProducts.js
// Composable that exposes paginated product list
import { useQuery } from '@vue/apollo-composable'
import { gql } from '@apollo/client/core'
import { PRODUCT_CARD_FRAGMENT } from '@/graphql/fragments/product'
import { computed, ref } from 'vue'
const PRODUCTS_QUERY = gql`
query Products($filter: ProductFilter, $page: Int!) {
products(filter: $filter, page: $page, perPage: 24) {
total
items { ...ProductCard }
}
}
${PRODUCT_CARD_FRAGMENT}
`
export function useProducts(filter) {
const page = ref(1)
const { result, loading, error, fetchMore } = useQuery(PRODUCTS_QUERY, () => ({
filter: filter.value,
page: page.value,
}), { fetchPolicy: 'cache-and-network' })
const products = computed(() => result.value?.products?.items ?? [])
const total = computed(() => result.value?.products?.total ?? 0)
function loadMore() {
page.value++
fetchMore({ variables: { page: page.value } })
}
return { products, total, loading, error, loadMore }
}
6. Mutations and optimistic updates
Mutations in Vue GraphQL with Apollo are executed via useMutation from @vue/apollo-composable. Unlike useQuery, a mutation is not executed automatically, only on explicit invocation of the returned mutate function. The most important feature for a good user experience is the optimisticResponse option: you supply the mutation's expected response before the request has even been sent. Apollo writes this optimistic response into the cache immediately, and all dependent queries and components update instantly. As soon as the real response arrives from the server, Apollo overwrites the optimistic response with the actual data.
Cache updates after mutations are a common topic in Vue GraphQL. When a mutation creates a new entity, that entity is not yet in the cache. You have to update the cache manually, by reading the existing list from the cache in the mutation's update function, adding the new entity, and writing the list back into the cache. When a mutation updates an existing entity, that happens automatically, Apollo normalizes the response and updates the entity in the cache under the known key. Anyone who understands this behavior rarely needs an explicit refetch after mutations.
7. Handling errors and loading states correctly
Error handling in Vue GraphQL has two levels: network errors, where the request does not come back at all or comes back with an HTTP error code, and GraphQL errors, where the request succeeded but the schema returns validation errors or application-level errors. Apollo distinguishes between these two error types, and the correct configuration of errorPolicy determines how mixed responses are handled, that is, responses that contain partial data and partial errors. With errorPolicy: 'all' both data and errors are returned, which makes sense in complex queries where an error in one branch should not invalidate the entire query.
Loading states in Vue GraphQL components should be designed explicitly. A common mistake is simply hiding the entire content with v-if="!loading". That leads to layout shifts and a poor user experience. A skeleton loading pattern is better: the component renders a placeholder version with the same structure and dimensions as the actual content while loading === true. Vue GraphQL makes this pattern easy, because the reactive loading ref can be evaluated directly in the template.
8. Tooling: Apollo DevTools, GraphQL Codegen, VSCode
The tooling ecosystem around Vue GraphQL is mature and speeds up development considerably. The Apollo Client DevTools for Chrome and Firefox let you inspect the entire cache, see active queries and mutations, and run queries directly in the browser. Once you have seen the normalized cache in the DevTools, how each entity is stored under its key and how queries reference cache entries, you understand Apollo's caching model far faster than by just reading the documentation.
GraphQL Code Generator is indispensable for larger Vue GraphQL projects. The tool reads the GraphQL schema and all .graphql files in the project and generates TypeScript types from them for every query, mutation and fragment. The result: complete type safety between the GraphQL schema and the Vue components. When a field is renamed in the schema, TypeScript immediately shows every affected spot in the frontend. The configuration is minimal, a single codegen.yml is enough, and integrating it into the dev server with --watch regenerates types automatically on every schema change.
// codegen.yml, GraphQL Code Generator config for Vue 3 + TypeScript
// Run: npx graphql-code-generator --config codegen.yml --watch
// Generates typed hooks and fragment types from schema + operations
overwrite: true
schema: "${VITE_GRAPHQL_URL}"
documents: "src/**/*.{graphql,gql,ts,vue}"
generates:
src/generated/graphql.ts:
plugins:
- typescript
- typescript-operations
- typescript-vue-apollo
config:
vueCompositionApiImportFrom: vue
withCompositionFunctions: true
withSmartQuery: false
useTypeImports: true
// After generation, fully typed composable usage
// src/composables/useProduct.ts
import { useProductQuery } from '@/generated/graphql'
import { computed } from 'vue'
export function useProduct(slug: string) {
// useProductQuery is auto-generated, fully typed result, variables, etc.
const { result, loading, error } = useProductQuery(
() => ({ slug }),
{ fetchPolicy: 'cache-and-network' }
)
const product = computed(() => result.value?.product ?? null)
return { product, loading, error }
}
9. Strategies compared: fetch-policy options
The fetchPolicy of a query is one of the most important levers in any Vue GraphQL application. The wrong policy leads either to unnecessarily many network requests or to stale data being shown to the user. Understanding the available options is fundamental to a performant Vue GraphQL architecture.
| fetchPolicy | Reads cache? | Network request? | Ideal use case |
|---|---|---|---|
cache-first |
Yes (primary) | Only on cache miss | Static reference data, categories |
cache-and-network |
Yes (immediately) | Always | Lists with frequent updates |
network-only |
No | Always | Critical data, checkout |
cache-only |
Yes | Never | Offline scenarios, optimistic UI |
no-cache |
No | Always | Sensitive data with no cache storage |
As a day-to-day rule of thumb for Vue GraphQL projects: cache-and-network as the default for all lists and detail pages, because the user sees data from the cache immediately while the latest state loads in the background. network-only for transactional queries such as order status or account balance, where stale data can have critical consequences. cache-first for configuration data and static lists such as categories or country selectors, which only change on deploys.
Mironsoft
Vue GraphQL · Apollo Client · TypeScript · Frontend Architecture
Need a Vue GraphQL architecture for your project?
We design and implement Vue GraphQL integrations with Apollo Client, from cache strategy through typed composables to a complete GraphQL Code Generator setup.
Schema design
Design the GraphQL schema, configure typePolicies and structure fragments
Performance optimization
fetchPolicy strategy, pagination with fetchMore and optimistic updates
Code generation
Set up GraphQL Code Generator and generate type-safe composables
10. Summary
Vue GraphQL with Apollo Client is not a drop-in replacement for REST calls, but a complete data management system for frontend applications. The normalized cache is the central advantage: each entity is stored once, and mutations automatically update all dependent views. Fragments make queries modular and tied to components. The fetchPolicy controls the balance between cache efficiency and data freshness. Optimistic updates give the user immediate feedback without waiting for the server response.
The tooling around Vue GraphQL is mature: Apollo DevTools for cache inspection, GraphQL Code Generator for type-safe composables, and the VSCode GraphQL extension for syntax highlighting and autocompletion directly inside query strings. Anyone who integrates these tools into a project from the start builds faster, makes fewer mistakes, and can carry out refactorings with confidence, because TypeScript and the generator immediately flag inconsistencies.
Vue GraphQL, the essentials at a glance
Cache normalization
Configure typePolicies and keyFields, each entity is stored once and automatically updated by mutations.
Fragments & composables
Components declare their own fragments, composables encapsulate useQuery, queries stay maintainable and reusable.
fetchPolicy strategy
cache-and-network as the default, network-only for transactional data, cache-first for static lists.
Code generator
GraphQL Code Generator generates TypeScript types and typed composables, complete type safety between schema and Vue.