Skip to content

title: Vue 3 with Apollo Client for FraiseQL description: Complete guide for querying FraiseQL servers from Vue 3 applications using Apollo Client. keywords: ["debugging", "implementation", "best-practices", "deployment", "tutorial"] tags: ["documentation", "reference"]


Vue 3 with Apollo Client for FraiseQL

Status: ✅ Production Ready Audience: Vue developers Reading Time: 25-30 minutes Last Updated: 2026-02-05

Complete guide for querying FraiseQL servers from Vue 3 applications using Apollo Client.


Installation & Setup

Prerequisites

  • Vue 3.0+
  • Node.js 16+
  • FraiseQL server running

Install Dependencies

npm install @vue/apollo-composable @apollo/client graphql subscriptions-transport-ws

Create Apollo Client

// apollo.config.ts
import { ApolloClient, InMemoryCache, HttpLink, from } from '@apollo/client';
import { onError } from '@apollo/client/link/error';
import { WebSocketLink } from '@apollo/client/link/ws';
import { getMainDefinition } from '@apollo/client/utilities';

const errorLink = onError(({ graphQLErrors, networkError }) => {
  if (graphQLErrors) {
    graphQLErrors.forEach(({ message, locations, path }) => {
      console.error(`[GraphQL error]: ${message}`, { locations, path });
    });
  }
  if (networkError) console.error(`[Network error]: ${networkError}`);
});

const httpLink = new HttpLink({
  uri: 'http://localhost:8000/graphql',
  credentials: 'include',
});

const wsLink = new WebSocketLink({
  uri: 'ws://localhost:8000/graphql',
  options: {
    reconnect: true,
    connectionParams: () => ({
      authorization: localStorage.getItem('token') || '',
    }),
  },
});

const splitLink = from([
  errorLink,
  new (typeof window !== 'undefined' ? require('@apollo/client').split : null)?.(
    ({ query }) => {
      const definition = getMainDefinition(query);
      return (
        definition.kind === 'OperationDefinition' &&
        definition.operation === 'subscription'
      );
    },
    wsLink,
    httpLink
  ) || httpLink,
]);

export const apolloClient = new ApolloClient({
  link: splitLink,
  cache: new InMemoryCache(),
  defaultOptions: {
    watchQuery: {
      fetchPolicy: 'cache-and-network',
    },
  },
});

Register with Vue App

// main.ts
import { createApp } from 'vue';
import { DefaultApolloClient } from '@vue/apollo-composable';
import { apolloClient } from './apollo.config';
import App from './App.vue';

const app = createApp(App);
app.provide(DefaultApolloClient, apolloClient);
app.mount('#app');

Queries

Basic Query with useQuery

<template>
  <div v-if="loading" class="loading">Loading users...</div>
  <div v-else-if="error" class="error">{{ error.message }}</div>
  <ul v-else>
    <li v-for="user in result?.users" :key="user.id">
      {{ user.name }} ({{ user.email }})
    </li>
  </ul>
</template>

<script setup lang="ts">
import { useQuery, gql } from '@vue/apollo-composable';
import { computed } from 'vue';

const GET_USERS = gql`
  query GetUsers {
    users {
      id
      name
      email
    }
  }
`;

const { result, loading, error } = useQuery(GET_USERS);
</script>

Query with Variables

<template>
  <div>
    <div v-if="loading">Loading user...</div>
    <div v-else-if="error">{{ error.message }}</div>
    <div v-else>
      <h1>{{ user?.name }}</h1>
      <p>Email: {{ user?.email }}</p>
      <h2>Posts:</h2>
      <ul>
        <li v-for="post in user?.posts" :key="post.id">
          {{ post.title }}
        </li>
      </ul>
    </div>
  </div>
</template>

<script setup lang="ts">
import { useQuery, gql } from '@vue/apollo-composable';
import { computed } from 'vue';

interface Props {
  userId: string;
}

const props = defineProps<Props>();

const GET_USER = gql`
  query GetUserById($id: ID!) {
    user(id: $id) {
      id
      name
      email
      posts {
        id
        title
      }
    }
  }
`;

const { result, loading, error } = useQuery(
  GET_USER,
  () => ({
    id: props.userId,
  }),
  () => ({
    enabled: !!props.userId,
  })
);

const user = computed(() => result.value?.user);
</script>

Reactive Query Variables

<template>
  <div>
    <input v-model="searchTerm" placeholder="Search users..." />
    <ul>
      <li v-for="user in users" :key="user.id">
        {{ user.name }}
      </li>
    </ul>
  </div>
</template>

<script setup lang="ts">
import { useQuery, gql } from '@vue/apollo-composable';
import { ref, computed } from 'vue';

const SEARCH_USERS = gql`
  query SearchUsers($term: String!) {
    searchUsers(term: $term) {
      id
      name
      email
    }
  }
`;

const searchTerm = ref('');

const { result, loading } = useQuery(
  SEARCH_USERS,
  () => ({ term: searchTerm.value }),
  () => ({
    skip: searchTerm.value.length < 2,
  })
);

const users = computed(() => result.value?.searchUsers || []);
</script>

Pagination with fetchMore

<template>
  <div>
    <div class="posts">
      <div v-for="post in posts" :key="post.id" class="post">
        <h3>{{ post.title }}</h3>
        <p>{{ post.content }}</p>
      </div>
    </div>
    <button @click="loadMore" :disabled="loading || !hasMore">
      {{ loading ? 'Loading...' : 'Load More' }}
    </button>
  </div>
</template>

<script setup lang="ts">
import { useQuery, gql } from '@vue/apollo-composable';
import { ref, computed } from 'vue';

const GET_POSTS = gql`
  query GetPosts($limit: Int!, $offset: Int!) {
    posts(limit: $limit, offset: $offset) {
      id
      title
      content
    }
    postsCount
  }
`;

const pageSize = 10;
const currentPage = ref(0);

const { result, loading, fetchMore } = useQuery(
  GET_POSTS,
  () => ({
    limit: pageSize,
    offset: 0,
  })
);

const posts = computed(() => result.value?.posts || []);
const totalCount = computed(() => result.value?.postsCount || 0);
const hasMore = computed(() => posts.value.length < totalCount.value);

const loadMore = async () => {
  currentPage.value++;
  await fetchMore({
    variables: {
      offset: currentPage.value * pageSize,
    },
    updateQuery: (prev, { fetchMoreResult }) => {
      if (!fetchMoreResult) return prev;
      return {
        posts: [...(prev?.posts || []), ...(fetchMoreResult.posts || [])],
        postsCount: fetchMoreResult.postsCount,
      };
    },
  });
};
</script>

Mutations

Basic Mutation with useMutation

<template>
  <form @submit.prevent="createPost">
    <input
      v-model="title"
      type="text"
      placeholder="Post title"
      required
    />
    <textarea
      v-model="content"
      placeholder="Post content"
      required
    />
    <button type="submit" :disabled="loading">
      {{ loading ? 'Creating...' : 'Create Post' }}
    </button>
    <div v-if="error" class="error">{{ error.message }}</div>
  </form>
</template>

<script setup lang="ts">
import { useMutation, gql } from '@vue/apollo-composable';
import { ref } from 'vue';

const CREATE_POST = gql`
  mutation CreatePost($title: String!, $content: String!) {
    createPost(title: $title, content: $content) {
      id
      title
      content
      createdAt
    }
  }
`;

const title = ref('');
const content = ref('');

const { mutate: createPostMutation, loading, error } = useMutation(CREATE_POST);

const createPost = async () => {
  try {
    const result = await createPostMutation({
      title: title.value,
      content: content.value,
    });
    console.log('Post created:', result?.data?.createPost);
    title.value = '';
    content.value = '';
  } catch (err) {
    console.error('Error:', err);
  }
};
</script>

Update Cache After Mutation

<template>
  <button @click="updateName" :disabled="loading">
    Update Name
  </button>
</template>

<script setup lang="ts">
import { useMutation, useQuery, gql } from '@vue/apollo-composable';
import { ref } from 'vue';

const UPDATE_USER = gql`
  mutation UpdateUser($id: ID!, $name: String!) {
    updateUser(id: $id, name: $name) {
      id
      name
    }
  }
`;

const GET_USER = gql`
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      name
    }
  }
`;

interface Props {
  userId: string;
}

const props = defineProps<Props>();
const newName = ref('');

const { mutate: updateUserMutation, loading } = useMutation(UPDATE_USER, () => ({
  update: (cache, { data }) => {
    cache.writeQuery({
      query: GET_USER,
      variables: { id: props.userId },
      data: {
        user: data?.updateUser,
      },
    });
  },
}));

const updateName = async () => {
  await updateUserMutation({
    id: props.userId,
    name: newName.value,
  });
};
</script>

Subscriptions

Subscribe to Real-Time Events

<template>
  <div class="feed">
    <div v-if="connecting" class="status">Connecting...</div>
    <div v-else-if="error" class="error">{{ error.message }}</div>
    <div v-else>
      <h2>New Posts</h2>
      <div v-if="latestPost" class="post">
        <h3>{{ latestPost.title }}</h3>
        <p>by {{ latestPost.author.name }}</p>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { useSubscription, gql } from '@vue/apollo-composable';
import { ref, computed } from 'vue';

const ON_POST_CREATED = gql`
  subscription OnPostCreated {
    postCreated {
      id
      title
      author {
        name
      }
    }
  }
`;

const { result, loading: connecting, error } = useSubscription(ON_POST_CREATED);

const latestPost = computed(() => result.value?.postCreated);
</script>

Manage Multiple Subscriptions

<template>
  <div>
    <h2>Messages ({{ messages.length }})</h2>
    <div class="messages">
      <div v-for="msg in messages" :key="msg.id" class="message">
        <strong>{{ msg.user.name }}:</strong> {{ msg.text }}
      </div>
    </div>

    <h2>Users Online ({{ onlineUsers.length }})</h2>
    <ul>
      <li v-for="user in onlineUsers" :key="user.id">
        {{ user.name }}
      </li>
    </ul>
  </div>
</template>

<script setup lang="ts">
import { useSubscription, gql } from '@vue/apollo-composable';
import { ref, computed } from 'vue';

const ON_MESSAGE = gql`
  subscription OnMessage {
    messageReceived {
      id
      text
      user { name }
    }
  }
`;

const ON_USER_ONLINE = gql`
  subscription OnUserOnline {
    userOnline { id name }
  }
`;

const messages = ref<any[]>([]);
const onlineUsers = ref<any[]>([]);

const { result: msgResult } = useSubscription(ON_MESSAGE);
const { result: userResult } = useSubscription(ON_USER_ONLINE);

// Watch for new messages
const watchMessages = () => {
  if (msgResult.value?.messageReceived) {
    messages.value.push(msgResult.value.messageReceived);
  }
};

// Watch for user status
const watchUsers = () => {
  if (userResult.value?.userOnline) {
    onlineUsers.value = [userResult.value.userOnline];
  }
};
</script>

Error Handling

Global Error Handling

// apollo.config.ts
const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
  if (graphQLErrors) {
    for (const err of graphQLErrors) {
      if (err.extensions?.code === 'UNAUTHENTICATED') {
        localStorage.removeItem('token');
        window.location.href = '/login';
      }

      if (err.extensions?.code === 'FORBIDDEN') {
        console.error('Access denied');
      }

      if (err.extensions?.code === 'VALIDATION_ERROR') {
        console.error('Validation failed:', err.message);
      }
    }
  }

  if (networkError) {
    if ('statusCode' in networkError && networkError.statusCode === 401) {
      window.location.href = '/login';
    }
  }
});

Component-Level Error Handling

<template>
  <div>
    <div v-if="error" class="error-container">
      <p>{{ errorMessage }}</p>
      <button @click="retry">Try Again</button>
    </div>
    <div v-else-if="loading">Loading...</div>
    <UserList v-else :users="users" />
  </div>
</template>

<script setup lang="ts">
import { useQuery, gql } from '@vue/apollo-composable';
import { computed } from 'vue';

const GET_USERS = gql`query GetUsers { users { id name } }`;

const { result, loading, error, refetch } = useQuery(GET_USERS);

const users = computed(() => result.value?.users || []);

const errorMessage = computed(() => {
  if (!error.value) return '';
  if (error.value.message.includes('network')) {
    return 'Network error. Check your connection.';
  }
  return error.value.message;
});

const retry = () => refetch();
</script>

Caching Strategies

Cache Management

<template>
  <div>
    <button @click="clearCache">Clear Cache</button>
    <button @click="refetchQuery">Refetch Data</button>
  </div>
</template>

<script setup lang="ts">
import { useQuery, useApolloClient, gql } from '@vue/apollo-composable';

const GET_USERS = gql`query GetUsers { users { id name } }`;

const { refetch } = useQuery(GET_USERS);
const { client } = useApolloClient();

const clearCache = () => {
  client.cache.reset();
};

const refetchQuery = () => {
  refetch();
};
</script>

Fetch Policies

<script setup lang="ts">
import { useQuery, gql } from '@vue/apollo-composable';

const GET_DATA = gql`query GetData { data { id } }`;

// Different fetch policies for different data types

// Static user profile - use cache
useQuery(GET_DATA, null, { fetchPolicy: 'cache-first' });

// Real-time notifications - always fetch
useQuery(GET_DATA, null, {
  fetchPolicy: 'network-only',
  pollInterval: 5000, // Refetch every 5 seconds
});

// Activity feed - cache + refresh
useQuery(GET_DATA, null, { fetchPolicy: 'cache-and-network' });
</script>

Performance Optimization

Lazy Query Loading

<template>
  <input
    v-model="searchTerm"
    @change="performSearch"
    placeholder="Search..."
  />
  <ul v-if="results">
    <li v-for="result in results" :key="result.id">
      {{ result.name }}
    </li>
  </ul>
</template>

<script setup lang="ts">
import { useLazyQuery, gql } from '@vue/apollo-composable';
import { ref, computed } from 'vue';

const SEARCH = gql`
  query Search($term: String!) {
    search(term: $term) { id name }
  }
`;

const searchTerm = ref('');
const { result, load } = useLazyQuery(SEARCH);
const results = computed(() => result.value?.search);

const performSearch = async () => {
  if (searchTerm.value.length > 2) {
    await load(null, { term: searchTerm.value });
  }
};
</script>

Conditional Query Loading

<script setup lang="ts">
import { useQuery, gql } from '@vue/apollo-composable';
import { ref, computed } from 'vue';

const GET_PROFILE = gql`query GetProfile { profile { id name } }`;
const GET_SETTINGS = gql`query GetSettings { settings { theme } }`;

const showSettings = ref(false);

// Profile loads immediately
const { result: profileResult } = useQuery(GET_PROFILE);

// Settings only load when needed
const { result: settingsResult } = useQuery(
  GET_SETTINGS,
  null,
  { skip: computed(() => !showSettings.value) }
);
</script>

Testing

Test Query Component

// UserList.spec.ts
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import { createMockClient } from '@vue/apollo-composable/testing';
import { gql } from '@apollo/client';
import UserList from './UserList.vue';

const mockClient = createMockClient();

describe('UserList', () => {
  it('renders user list', async () => {
    const wrapper = mount(UserList, {
      global: {
        plugins: [mockClient],
      },
    });

    await wrapper.vm.$nextTick();
    expect(wrapper.find('.users').exists()).toBe(true);
  });
});

Test Mutation

import { describe, it, expect, vi } from 'vitest';
import { useMutation, gql } from '@vue/apollo-composable';

const UPDATE_USER = gql`
  mutation UpdateUser($id: ID!, $name: String!) {
    updateUser(id: $id, name: $name) { id name }
  }
`;

describe('Mutation', () => {
  it('updates user', async () => {
    const { mutate } = useMutation(UPDATE_USER);

    const result = await mutate({
      id: '1',
      name: 'Updated Name',
    });

    expect(result?.data?.updateUser?.name).toBe('Updated Name');
  });
});

Server-Side Rendering (Nuxt)

Setup with Nuxt 3

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxtjs/apollo'],
  apollo: {
    clients: {
      default: {
        httpEndpoint: 'http://localhost:8000/graphql',
        wsEndpoint: 'ws://localhost:8000/graphql',
      },
    },
  },
});

Use Queries in Nuxt

<template>
  <div>
    <h1>{{ user?.name }}</h1>
  </div>
</template>

<script setup lang="ts">
import { useQuery, gql } from '@vue/apollo-composable';

const GET_USER = gql`
  query GetUser($id: ID!) {
    user(id: $id) { id name }
  }
`;

const { result } = useQuery(
  GET_USER,
  () => ({ id: useRoute().params.id }),
  { prefetch: true } // Prefetch on server
);

const user = computed(() => result.value?.user);
</script>

See Also

Related Guides:

Vue & Apollo Documentation: