Table of Contents
Introduction
GraphQL revolutionne la maniere dont les applications frontend communiquent avec les APIs. Developpe par Facebook en 2012 et rendu open source en 2015, GraphQL offre une alternative puissante aux APIs REST traditionnelles. Combine avec Vue Apollo, il permet de creer des applications Vue.js hautement reactives avec une gestion optimale des donnees.
Pourquoi GraphQL ?
GraphQL resout plusieurs problemes inherents aux APIs REST :
- Over-fetching : Avec REST, vous recevez souvent plus de donnees que necessaire. GraphQL vous permet de specifier exactement les champs dont vous avez besoin
- Under-fetching : REST necessite parfois plusieurs requetes pour obtenir toutes les donnees. GraphQL permet de recuperer des donnees liees en une seule requete
- Typage fort : GraphQL est intrinsequement type, ce qui ameliore la documentation et la detection d’erreurs
- Evolution d’API : Pas besoin de versionner votre API, ajoutez simplement de nouveaux champs
Architecture GraphQL avec Vue
┌─────────────────────────────────────────────────────────────┐
│ Application Vue.js │
├─────────────────────────────────────────────────────────────┤
│ Composants Vue │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ useQuery() │ │useMutation()│ │useSubscription()│ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
├─────────┴────────────────┴───────────────┴──────────────────┤
│ Vue Apollo Composable │
├─────────────────────────────────────────────────────────────┤
│ Apollo Client │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Cache │ │ Link │ │ Dev Tools │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ Serveur GraphQL │
└─────────────────────────────────────────────────────────────┘
Schema GraphQL de Reference
Avant de plonger dans le code Vue, definissons un schema GraphQL complet qui servira de reference pour tous nos exemples :
# Types de base
type User {
id: ID!
email: String!
username: String!
avatar: String
role: UserRole!
listings: [Listing!]!
createdAt: DateTime!
}
enum UserRole {
USER
ADMIN
MODERATOR
}
type Listing {
id: ID!
title: String!
description: String!
price: Float!
category: Category!
author: User!
images: [String!]!
status: ListingStatus!
createdAt: DateTime!
updatedAt: DateTime!
}
enum ListingStatus {
DRAFT
PUBLISHED
ARCHIVED
}
type Category {
id: ID!
name: String!
slug: String!
listings: [Listing!]!
}
# Types d'input pour les mutations
input CreateListingInput {
title: String!
description: String!
price: Float!
categoryId: ID!
images: [String!]
}
input UpdateListingInput {
title: String
description: String
price: Float
categoryId: ID
status: ListingStatus
}
input ListingsFilter {
categoryId: ID
status: ListingStatus
minPrice: Float
maxPrice: Float
search: String
}
# Types de pagination
type ListingsConnection {
edges: [ListingEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type ListingEdge {
node: Listing!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
# Queries
type Query {
# Utilisateurs
me: User
user(id: ID!): User
users(limit: Int, offset: Int): [User!]!
# Listings
listing(id: ID!): Listing
listings(
filter: ListingsFilter
first: Int
after: String
last: Int
before: String
): ListingsConnection!
# Categories
categories: [Category!]!
category(slug: String!): Category
}
# Mutations
type Mutation {
# Auth
login(email: String!, password: String!): AuthPayload!
register(email: String!, password: String!, username: String!): AuthPayload!
logout: Boolean!
# Listings
createListing(input: CreateListingInput!): Listing!
updateListing(id: ID!, input: UpdateListingInput!): Listing!
deleteListing(id: ID!): Boolean!
publishListing(id: ID!): Listing!
}
type AuthPayload {
token: String!
user: User!
}
# Subscriptions
type Subscription {
listingCreated: Listing!
listingUpdated(id: ID!): Listing!
}
scalar DateTime
Configuration Complete d’Apollo Client
Installation des dependances
npm install @apollo/client @vue/apollo-composable graphql graphql-tag
Configuration de base
Creez un fichier src/apollo/client.ts pour centraliser la configuration :
// src/apollo/client.ts
import {
ApolloClient,
createHttpLink,
InMemoryCache,
ApolloLink,
from,
split
} from '@apollo/client/core'
import { onError } from '@apollo/client/link/error'
import { setContext } from '@apollo/client/link/context'
import { getMainDefinition } from '@apollo/client/utilities'
import { GraphQLWsLink } from '@apollo/client/link/subscriptions'
import { createClient } from 'graphql-ws'
// Configuration des URLs
const httpUri = import.meta.env.VITE_GRAPHQL_HTTP_URL || 'http://localhost:4000/graphql'
const wsUri = import.meta.env.VITE_GRAPHQL_WS_URL || 'ws://localhost:4000/graphql'
// Lien HTTP de base
const httpLink = createHttpLink({
uri: httpUri,
credentials: 'include' // Pour les cookies de session
})
// Lien WebSocket pour les subscriptions
const wsLink = new GraphQLWsLink(
createClient({
url: wsUri,
connectionParams: () => ({
authorization: localStorage.getItem('auth_token') || ''
}),
on: {
connected: () => console.log('WebSocket connecte'),
closed: () => console.log('WebSocket deconnecte')
}
})
)
// Lien d'authentification
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem('auth_token')
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : ''
}
}
})
// Gestion des erreurs
const errorLink = onError(({ graphQLErrors, networkError, operation }) => {
if (graphQLErrors) {
graphQLErrors.forEach(({ message, locations, path, extensions }) => {
console.error(
`[GraphQL Error]: Message: ${message}, Path: ${path}`,
{ locations, extensions }
)
// Gestion des erreurs d'authentification
if (extensions?.code === 'UNAUTHENTICATED') {
localStorage.removeItem('auth_token')
window.location.href = '/login'
}
})
}
if (networkError) {
console.error(`[Network Error]: ${networkError.message}`)
}
})
// Lien de logging (developpement uniquement)
const logLink = new ApolloLink((operation, forward) => {
const startTime = Date.now()
console.log(`[GraphQL] Starting: ${operation.operationName}`)
return forward(operation).map((response) => {
const duration = Date.now() - startTime
console.log(`[GraphQL] Completed: ${operation.operationName} in ${duration}ms`)
return response
})
})
// Split pour router queries/mutations vers HTTP et subscriptions vers WS
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query)
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
)
},
wsLink,
from([errorLink, logLink, authLink, httpLink])
)
// Configuration du cache
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
// Pagination cursor-based pour les listings
listings: {
keyArgs: ['filter'],
merge(existing, incoming, { args }) {
if (!args?.after) {
return incoming
}
return {
...incoming,
edges: [...(existing?.edges || []), ...incoming.edges]
}
}
}
}
},
Listing: {
fields: {
// Champ calcule pour le prix formate
formattedPrice: {
read(_, { readField }) {
const price = readField<number>('price')
return price ? `${price.toFixed(2)} EUR` : null
}
}
}
}
}
})
// Creation du client Apollo
export const apolloClient = new ApolloClient({
link: splitLink,
cache,
defaultOptions: {
watchQuery: {
fetchPolicy: 'cache-and-network',
errorPolicy: 'all'
},
query: {
fetchPolicy: 'cache-first',
errorPolicy: 'all'
},
mutate: {
errorPolicy: 'all'
}
},
connectToDevTools: import.meta.env.DEV
})
export default apolloClient
Integration dans main.ts
// src/main.ts
import { createApp, h, provide } from 'vue'
import { DefaultApolloClient } from '@vue/apollo-composable'
import App from './App.vue'
import apolloClient from './apollo/client'
const app = createApp({
setup() {
provide(DefaultApolloClient, apolloClient)
},
render: () => h(App)
})
app.mount('#app')
Queries avec useQuery()
Query de base
<script setup lang="ts">
import { useQuery } from '@vue/apollo-composable'
import gql from 'graphql-tag'
// Definition de la query
const GET_LISTINGS = gql`
query GetListings($filter: ListingsFilter, $first: Int, $after: String) {
listings(filter: $filter, first: $first, after: $after) {
edges {
node {
id
title
description
price
status
category {
id
name
}
author {
id
username
avatar
}
createdAt
}
cursor
}
pageInfo {
hasNextPage
endCursor
}
totalCount
}
}
`
// Utilisation de useQuery
const { result, loading, error, fetchMore, refetch } = useQuery(GET_LISTINGS, {
filter: { status: 'PUBLISHED' },
first: 10
})
// Computed pour acceder facilement aux donnees
const listings = computed(() => result.value?.listings?.edges?.map(e => e.node) || [])
const hasNextPage = computed(() => result.value?.listings?.pageInfo?.hasNextPage || false)
const totalCount = computed(() => result.value?.listings?.totalCount || 0)
// Fonction pour charger plus de resultats
const loadMore = () => {
if (!hasNextPage.value) return
fetchMore({
variables: {
after: result.value?.listings?.pageInfo?.endCursor
}
})
}
</script>
<template>
<div class="listings-container">
<div v-if="loading && !listings.length" class="loading">
Chargement des annonces...
</div>
<div v-else-if="error" class="error">
Erreur: {{ error.message }}
</div>
<template v-else>
<p class="count">{{ totalCount }} annonces trouvees</p>
<div class="listings-grid">
<article
v-for="listing in listings"
:key="listing.id"
class="listing-card"
>
<h3>{{ listing.title }}</h3>
<p>{{ listing.description }}</p>
<span class="price">{{ listing.price }} EUR</span>
<span class="category">{{ listing.category.name }}</span>
</article>
</div>
<button
v-if="hasNextPage"
@click="loadMore"
:disabled="loading"
class="load-more"
>
{{ loading ? 'Chargement...' : 'Charger plus' }}
</button>
</template>
</div>
</template>
Query avec variables reactives
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useQuery } from '@vue/apollo-composable'
import gql from 'graphql-tag'
const GET_LISTING = gql`
query GetListing($id: ID!) {
listing(id: $id) {
id
title
description
price
images
status
author {
id
username
avatar
}
category {
id
name
slug
}
createdAt
updatedAt
}
}
`
const props = defineProps<{
listingId: string
}>()
// Variables reactives - la query se re-execute automatiquement
const variables = computed(() => ({
id: props.listingId
}))
const { result, loading, error } = useQuery(GET_LISTING, variables, {
// Options avancees
fetchPolicy: 'cache-and-network',
notifyOnNetworkStatusChange: true
})
const listing = computed(() => result.value?.listing)
</script>
<template>
<div class="listing-detail">
<div v-if="loading" class="skeleton-loader">
<div class="skeleton-title"></div>
<div class="skeleton-content"></div>
</div>
<div v-else-if="error" class="error-state">
<p>Impossible de charger l'annonce</p>
<button @click="refetch">Reessayer</button>
</div>
<article v-else-if="listing">
<header>
<h1>{{ listing.title }}</h1>
<span class="status" :class="listing.status.toLowerCase()">
{{ listing.status }}
</span>
</header>
<div class="images-gallery">
<img
v-for="(image, index) in listing.images"
:key="index"
:src="image"
:alt="`Image ${index + 1} de ${listing.title}`"
/>
</div>
<div class="content">
<p class="description">{{ listing.description }}</p>
<p class="price">{{ listing.price }} EUR</p>
</div>
<footer>
<div class="author">
<img :src="listing.author.avatar" :alt="listing.author.username" />
<span>{{ listing.author.username }}</span>
</div>
<time :datetime="listing.createdAt">
Publie le {{ new Date(listing.createdAt).toLocaleDateString('fr-FR') }}
</time>
</footer>
</article>
</div>
</template>
Mutations avec useMutation()
Mutation de creation
<script setup lang="ts">
import { ref } from 'vue'
import { useMutation } from '@vue/apollo-composable'
import gql from 'graphql-tag'
const CREATE_LISTING = gql`
mutation CreateListing($input: CreateListingInput!) {
createListing(input: $input) {
id
title
description
price
status
category {
id
name
}
createdAt
}
}
`
// Formulaire reactif
const form = ref({
title: '',
description: '',
price: 0,
categoryId: '',
images: [] as string[]
})
const { mutate, loading, error, onDone, onError } = useMutation(CREATE_LISTING, {
// Mise a jour du cache apres la mutation
update(cache, { data: { createListing } }) {
// Ajout du nouveau listing dans le cache
cache.modify({
fields: {
listings(existingListings = { edges: [] }) {
const newListingRef = cache.writeFragment({
data: createListing,
fragment: gql`
fragment NewListing on Listing {
id
title
description
price
status
category {
id
name
}
createdAt
}
`
})
return {
...existingListings,
edges: [{ node: newListingRef }, ...existingListings.edges],
totalCount: existingListings.totalCount + 1
}
}
}
})
}
})
// Callbacks
onDone((result) => {
console.log('Listing cree:', result.data.createListing)
// Reset du formulaire
form.value = {
title: '',
description: '',
price: 0,
categoryId: '',
images: []
}
emit('created', result.data.createListing)
})
onError((err) => {
console.error('Erreur lors de la creation:', err)
})
const emit = defineEmits<{
created: [listing: any]
}>()
const submitForm = () => {
mutate({
input: {
title: form.value.title,
description: form.value.description,
price: parseFloat(form.value.price.toString()),
categoryId: form.value.categoryId,
images: form.value.images
}
})
}
</script>
<template>
<form @submit.prevent="submitForm" class="listing-form">
<div class="form-group">
<label for="title">Titre</label>
<input
id="title"
v-model="form.title"
type="text"
required
:disabled="loading"
/>
</div>
<div class="form-group">
<label for="description">Description</label>
<textarea
id="description"
v-model="form.description"
required
rows="4"
:disabled="loading"
></textarea>
</div>
<div class="form-group">
<label for="price">Prix (EUR)</label>
<input
id="price"
v-model.number="form.price"
type="number"
step="0.01"
min="0"
required
:disabled="loading"
/>
</div>
<div v-if="error" class="error-message">
{{ error.message }}
</div>
<button type="submit" :disabled="loading" class="submit-btn">
{{ loading ? 'Creation en cours...' : 'Creer l\'annonce' }}
</button>
</form>
</template>
Mutation de mise a jour
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useMutation } from '@vue/apollo-composable'
import gql from 'graphql-tag'
const UPDATE_LISTING = gql`
mutation UpdateListing($id: ID!, $input: UpdateListingInput!) {
updateListing(id: $id, input: $input) {
id
title
description
price
status
updatedAt
}
}
`
const props = defineProps<{
listing: {
id: string
title: string
description: string
price: number
status: string
}
}>()
const form = ref({ ...props.listing })
// Synchroniser le formulaire avec les props
watch(() => props.listing, (newListing) => {
form.value = { ...newListing }
}, { deep: true })
const { mutate, loading } = useMutation(UPDATE_LISTING)
const updateListing = async () => {
try {
await mutate({
id: props.listing.id,
input: {
title: form.value.title,
description: form.value.description,
price: form.value.price,
status: form.value.status
}
})
emit('updated')
} catch (err) {
console.error('Erreur de mise a jour:', err)
}
}
const emit = defineEmits<{
updated: []
}>()
</script>
<template>
<form @submit.prevent="updateListing">
<input v-model="form.title" :disabled="loading" />
<textarea v-model="form.description" :disabled="loading"></textarea>
<input v-model.number="form.price" type="number" :disabled="loading" />
<select v-model="form.status" :disabled="loading">
<option value="DRAFT">Brouillon</option>
<option value="PUBLISHED">Publie</option>
<option value="ARCHIVED">Archive</option>
</select>
<button type="submit" :disabled="loading">
{{ loading ? 'Mise a jour...' : 'Mettre a jour' }}
</button>
</form>
</template>
Mutation de suppression
<script setup lang="ts">
import { useMutation } from '@vue/apollo-composable'
import gql from 'graphql-tag'
const DELETE_LISTING = gql`
mutation DeleteListing($id: ID!) {
deleteListing(id: $id)
}
`
const props = defineProps<{
listingId: string
}>()
const { mutate, loading } = useMutation(DELETE_LISTING, {
variables: { id: props.listingId },
// Mise a jour optimiste du cache
update(cache) {
cache.evict({ id: `Listing:${props.listingId}` })
cache.gc() // Garbage collection
}
})
const emit = defineEmits<{
deleted: []
}>()
const confirmDelete = async () => {
if (!confirm('Etes-vous sur de vouloir supprimer cette annonce ?')) return
try {
await mutate()
emit('deleted')
} catch (err) {
console.error('Erreur de suppression:', err)
}
}
</script>
<template>
<button
@click="confirmDelete"
:disabled="loading"
class="delete-btn"
>
{{ loading ? 'Suppression...' : 'Supprimer' }}
</button>
</template>
Fragments GraphQL
Les fragments permettent de reutiliser des selections de champs :
// src/graphql/fragments.ts
import gql from 'graphql-tag'
export const LISTING_FRAGMENT = gql`
fragment ListingFields on Listing {
id
title
description
price
status
createdAt
updatedAt
}
`
export const USER_FRAGMENT = gql`
fragment UserFields on User {
id
username
email
avatar
role
}
`
export const LISTING_WITH_AUTHOR_FRAGMENT = gql`
fragment ListingWithAuthor on Listing {
...ListingFields
author {
...UserFields
}
category {
id
name
slug
}
}
${LISTING_FRAGMENT}
${USER_FRAGMENT}
`
Utilisation dans les queries :
<script setup lang="ts">
import { useQuery } from '@vue/apollo-composable'
import gql from 'graphql-tag'
import { LISTING_WITH_AUTHOR_FRAGMENT } from '@/graphql/fragments'
const GET_LISTINGS = gql`
query GetListings($first: Int!) {
listings(first: $first) {
edges {
node {
...ListingWithAuthor
}
}
totalCount
}
}
${LISTING_WITH_AUTHOR_FRAGMENT}
`
const { result } = useQuery(GET_LISTINGS, { first: 10 })
</script>
Optimistic UI
L’UI optimiste ameliore l’experience utilisateur en affichant immediatement le resultat attendu :
<script setup lang="ts">
import { useMutation } from '@vue/apollo-composable'
import gql from 'graphql-tag'
const LIKE_LISTING = gql`
mutation LikeListing($id: ID!) {
likeListing(id: $id) {
id
likesCount
isLikedByMe
}
}
`
const props = defineProps<{
listing: {
id: string
likesCount: number
isLikedByMe: boolean
}
}>()
const { mutate, loading } = useMutation(LIKE_LISTING)
const toggleLike = () => {
const newLikedState = !props.listing.isLikedByMe
const likeDelta = newLikedState ? 1 : -1
mutate(
{ id: props.listing.id },
{
// Reponse optimiste - s'affiche immediatement
optimisticResponse: {
__typename: 'Mutation',
likeListing: {
__typename: 'Listing',
id: props.listing.id,
likesCount: props.listing.likesCount + likeDelta,
isLikedByMe: newLikedState
}
}
}
)
}
</script>
<template>
<button
@click="toggleLike"
:disabled="loading"
:class="{ liked: listing.isLikedByMe }"
class="like-btn"
>
<span class="heart-icon">{{ listing.isLikedByMe ? '❤️' : '🤍' }}</span>
<span class="count">{{ listing.likesCount }}</span>
</button>
</template>
Gestion des Etats Loading et Error
Composant wrapper reutilisable
<!-- src/components/QueryState.vue -->
<script setup lang="ts">
import type { ApolloError } from '@apollo/client/core'
defineProps<{
loading: boolean
error: ApolloError | null
}>()
const emit = defineEmits<{
retry: []
}>()
</script>
<template>
<div v-if="loading" class="query-loading">
<slot name="loading">
<div class="spinner"></div>
<p>Chargement en cours...</p>
</slot>
</div>
<div v-else-if="error" class="query-error">
<slot name="error" :error="error">
<div class="error-icon">⚠️</div>
<h3>Une erreur est survenue</h3>
<p>{{ error.message }}</p>
<button @click="$emit('retry')">Reessayer</button>
</slot>
</div>
<template v-else>
<slot></slot>
</template>
</template>
<style scoped>
.query-loading {
display: flex;
flex-direction: column;
align-items: center;
padding: 2rem;
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid #f3f3f3;
border-top: 3px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.query-error {
text-align: center;
padding: 2rem;
background: #fee;
border-radius: 8px;
}
</style>
Utilisation :
<script setup lang="ts">
import { useQuery } from '@vue/apollo-composable'
import QueryState from '@/components/QueryState.vue'
const { result, loading, error, refetch } = useQuery(GET_LISTINGS)
</script>
<template>
<QueryState :loading="loading" :error="error" @retry="refetch">
<template #loading>
<div class="custom-skeleton">
<!-- Skeleton personnalise -->
</div>
</template>
<div v-if="result?.listings">
<!-- Contenu -->
</div>
</QueryState>
</template>
Comparatif GraphQL vs REST
| Aspect | REST | GraphQL |
|---|---|---|
| Endpoints | Multiples (/users, /posts, /comments) | Un seul (/graphql) |
| Donnees recues | Structure fixe definie par le serveur | Structure flexible definie par le client |
| Over-fetching | Frequent (toutes les donnees renvoyees) | Evite (seuls les champs demandes) |
| Under-fetching | Frequent (plusieurs requetes necessaires) | Evite (donnees liees en une requete) |
| Typage | Optionnel (OpenAPI/Swagger) | Natif et obligatoire |
| Versioning | Necessaire (/v1/, /v2/) | Deprecation de champs |
| Documentation | Externe (Swagger, Postman) | Auto-generee (introspection) |
| Cache | HTTP caching simple | Cache normalise complexe |
| Temps reel | WebSockets separes | Subscriptions natives |
| Courbe d’apprentissage | Faible | Moyenne |
| Outils DevTools | Postman, Insomnia | Apollo DevTools, GraphiQL |
| Upload fichiers | Support natif | Necessite configuration speciale |
Bonnes Pratiques
1. Organisez vos queries dans des fichiers separes
// src/graphql/queries/listings.ts
import gql from 'graphql-tag'
import { LISTING_FRAGMENT } from '../fragments'
export const GET_LISTINGS = gql`
query GetListings($filter: ListingsFilter, $first: Int) {
listings(filter: $filter, first: $first) {
edges {
node {
...ListingFields
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
${LISTING_FRAGMENT}
`
export const GET_LISTING = gql`
query GetListing($id: ID!) {
listing(id: $id) {
...ListingFields
author {
id
username
}
}
}
${LISTING_FRAGMENT}
`
2. Utilisez des composables personnalises
// src/composables/useListings.ts
import { computed } from 'vue'
import { useQuery, useMutation } from '@vue/apollo-composable'
import { GET_LISTINGS, CREATE_LISTING, DELETE_LISTING } from '@/graphql/queries/listings'
export function useListings(filter?: Record<string, any>) {
const { result, loading, error, fetchMore, refetch } = useQuery(
GET_LISTINGS,
{ filter, first: 10 }
)
const listings = computed(() =>
result.value?.listings?.edges?.map(e => e.node) || []
)
const hasNextPage = computed(() =>
result.value?.listings?.pageInfo?.hasNextPage || false
)
const loadMore = () => {
if (!hasNextPage.value) return
fetchMore({
variables: { after: result.value?.listings?.pageInfo?.endCursor }
})
}
return {
listings,
loading,
error,
hasNextPage,
loadMore,
refetch
}
}
export function useCreateListing() {
const { mutate, loading, error, onDone } = useMutation(CREATE_LISTING)
const createListing = (input: CreateListingInput) => {
return mutate({ input })
}
return { createListing, loading, error, onDone }
}
3. Gerez correctement le cache
// Politique de cache pour la pagination
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
listings: {
keyArgs: ['filter'],
merge(existing, incoming, { args }) {
if (!args?.after) return incoming
return {
...incoming,
edges: [...(existing?.edges || []), ...incoming.edges]
}
}
}
}
}
}
})
4. Implementez la gestion d’erreurs globale
// src/apollo/errorHandler.ts
import { onError } from '@apollo/client/link/error'
import { useNotifications } from '@/composables/useNotifications'
export const errorLink = onError(({ graphQLErrors, networkError }) => {
const { showError } = useNotifications()
if (graphQLErrors) {
graphQLErrors.forEach(({ message, extensions }) => {
switch (extensions?.code) {
case 'UNAUTHENTICATED':
// Redirection vers login
break
case 'FORBIDDEN':
showError('Acces refuse')
break
default:
showError(message)
}
})
}
if (networkError) {
showError('Erreur de connexion. Verifiez votre connexion internet.')
}
})
5. Utilisez les subscriptions pour le temps reel
<script setup lang="ts">
import { useSubscription } from '@vue/apollo-composable'
import gql from 'graphql-tag'
const LISTING_CREATED = gql`
subscription OnListingCreated {
listingCreated {
id
title
price
author {
username
}
}
}
`
const { result } = useSubscription(LISTING_CREATED)
watch(() => result.value, (newListing) => {
if (newListing?.listingCreated) {
notifications.show(`Nouvelle annonce: ${newListing.listingCreated.title}`)
}
})
</script>
6. Typez vos operations GraphQL
// Avec graphql-codegen
import type { GetListingsQuery, CreateListingMutation } from '@/generated/graphql'
const { result } = useQuery<GetListingsQuery>(GET_LISTINGS)
const { mutate } = useMutation<CreateListingMutation>(CREATE_LISTING)
Pieges Courants
1. Oublier de mettre a jour le cache
// Mauvais - Le cache n'est pas mis a jour
const { mutate } = useMutation(CREATE_LISTING)
// Bon - Mise a jour explicite du cache
const { mutate } = useMutation(CREATE_LISTING, {
update(cache, { data }) {
// Mettre a jour le cache
},
// Ou utiliser refetchQueries
refetchQueries: [{ query: GET_LISTINGS }]
})
2. N-plus-1 queries avec des relations
# Mauvais - Cause N+1 requetes
query {
listings {
id
author {
# Requete separee pour chaque listing
posts { id }
}
}
}
# Bon - Utilisez DataLoader cote serveur
# et limitez les champs demandes cote client
3. Ne pas gerer les etats de chargement
<!-- Mauvais -->
<template>
<div v-for="item in result.items" :key="item.id">
{{ item.name }}
</div>
</template>
<!-- Bon -->
<template>
<div v-if="loading">Chargement...</div>
<div v-else-if="error">Erreur: {{ error.message }}</div>
<div v-else-if="result?.items?.length">
<div v-for="item in result.items" :key="item.id">
{{ item.name }}
</div>
</div>
<div v-else>Aucun resultat</div>
</template>
4. Variables non reactives
// Mauvais - La variable n'est pas reactive
const id = props.id
const { result } = useQuery(GET_ITEM, { id })
// Bon - Utiliser computed pour la reactivite
const variables = computed(() => ({ id: props.id }))
const { result } = useQuery(GET_ITEM, variables)
5. Ignorer la politique de fetch
// Mauvais - Toujours depuis le reseau
const { result } = useQuery(GET_DATA, null, {
fetchPolicy: 'network-only' // Ignore le cache
})
// Bon - Utiliser cache-and-network pour UX optimale
const { result } = useQuery(GET_DATA, null, {
fetchPolicy: 'cache-and-network'
})
Conclusion
L’integration de GraphQL avec Vue Apollo transforme la maniere dont vos applications Vue.js communiquent avec les APIs. Les avantages sont nombreux :
Points cles a retenir :
-
Configuration robuste : Investissez du temps dans une configuration Apollo Client complete avec gestion des erreurs, authentification et cache optimise
-
useQuery et useMutation : Ces hooks composables s’integrent parfaitement avec la Composition API de Vue 3 pour des composants reactifs
-
Fragments : Reutilisez vos selections de champs pour maintenir la coherence et reduire la duplication
-
Optimistic UI : Ameliorez l’experience utilisateur en affichant les resultats attendus immediatement
-
Gestion du cache : Maitrisez InMemoryCache et ses typePolicies pour des performances optimales
-
Typage : Utilisez graphql-codegen pour generer des types TypeScript a partir de votre schema
GraphQL et Vue Apollo forment un duo puissant pour construire des applications modernes, reactives et performantes. La courbe d’apprentissage initiale est compensee par une productivite accrue et une meilleure experience developpeur a long terme.
In-Article Ad
Dev Mode
Mahmoud DEVO
Senior Full-Stack Developer
I'm a passionate full-stack developer with 10+ years of experience building scalable web applications. I write about Vue.js, Node.js, PostgreSQL, and modern DevOps practices.
Enjoyed this article?
Subscribe to get more tech content delivered to your inbox.
Related Articles
Vue.js et Apollo Client : Résoudre les problèmes de cache GraphQL
Apprenez à mettre à jour le cache Apollo Client après une mutation GraphQL dans Vue.js. Guide complet avec useQuery, useMutation et refetch.
Vue Apollo Composable : useQuery et useMutation pour vos operations GraphQL
Exploitez useQuery et useMutation de Vue Apollo Composable pour gerer vos requetes GraphQL. Guide pratique avec gestion du loading, erreurs, cache et optimistic updates.
Gestion de Formulaires avec Vuex : Mutations, Getters et Actions
Maîtrisez la gestion des formulaires avec Vuex. Créez des mutations pour chaque champ, des getters pour l'état et des actions pour les API.