Integrer GraphQL et Vue Apollo : Requetes, mutations et gestion du cache

Configurez Vue Apollo avec GraphQL pour vos applications Vue.js. Executez des requetes et mutations avec une gestion optimale du cache.

Mahmoud DEVO
Mahmoud DEVO
December 28, 2025 9 min read
Integrer GraphQL et Vue Apollo : Requetes, mutations et gestion du cache

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

AspectRESTGraphQL
EndpointsMultiples (/users, /posts, /comments)Un seul (/graphql)
Donnees recuesStructure fixe definie par le serveurStructure flexible definie par le client
Over-fetchingFrequent (toutes les donnees renvoyees)Evite (seuls les champs demandes)
Under-fetchingFrequent (plusieurs requetes necessaires)Evite (donnees liees en une requete)
TypageOptionnel (OpenAPI/Swagger)Natif et obligatoire
VersioningNecessaire (/v1/, /v2/)Deprecation de champs
DocumentationExterne (Swagger, Postman)Auto-generee (introspection)
CacheHTTP caching simpleCache normalise complexe
Temps reelWebSockets separesSubscriptions natives
Courbe d’apprentissageFaibleMoyenne
Outils DevToolsPostman, InsomniaApollo DevTools, GraphiQL
Upload fichiersSupport natifNecessite 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 :

  1. Configuration robuste : Investissez du temps dans une configuration Apollo Client complete avec gestion des erreurs, authentification et cache optimise

  2. useQuery et useMutation : Ces hooks composables s’integrent parfaitement avec la Composition API de Vue 3 pour des composants reactifs

  3. Fragments : Reutilisez vos selections de champs pour maintenir la coherence et reduire la duplication

  4. Optimistic UI : Ameliorez l’experience utilisateur en affichant les resultats attendus immediatement

  5. Gestion du cache : Maitrisez InMemoryCache et ses typePolicies pour des performances optimales

  6. 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.

Advertisement

In-Article Ad

Dev Mode

Share this article

Mahmoud DEVO

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