DeveloperBootstrap

JavaScript

Vue.js 3 Composition API Production Guide

Master Vue.js 3 Composition API with TypeScript, Pinia state management, custom composables, performance optimisation, and production deployment.

What you'll learn

  • Write components entirely with the Composition API and <script setup>
  • Build reusable composables for shared logic
  • Manage global state with Pinia
  • Configure Vue Router for SPA navigation
  • Optimise bundle size with async components
  • Test Vue components with Vitest and Vue Test Utils
  • Deploy a production Vue app with Vite

Project Setup

Bash
1# Create Vue 3 + TypeScript + Vite project
2pnpm create vue@latest my-vue-app
3# Select: TypeScript ✓ | Router ✓ | Pinia ✓ | Vitest ✓ | ESLint ✓
4
5cd my-vue-app && pnpm install && pnpm dev

Composition API Basics

MARKUP
1<!-- UserProfile.vue -->
2<script setup lang="ts">
3import { ref, computed, onMounted } from 'vue';
4
5interface User { id: string; name: string; email: string }
6
7const props = defineProps<{ userId: string }>();
8const emit  = defineEmits<{ saved: [user: User] }>();
9
10const user    = ref<User | null>(null);
11const isLoading = ref(false);
12const displayName = computed(() => user.value?.name ?? 'Unknown');
13
14async function fetchUser() {
15  isLoading.value = true;
16  try {
17    const res = await fetch(`${import.meta.env.VITE_API_URL}/users/${props.userId}`,
18      { credentials: 'include' });
19    user.value = await res.json() as User;
20  } finally {
21    isLoading.value = false;
22  }
23}
24
25onMounted(fetchUser);
26</script>
27
28<template>
29  <div v-if="isLoading">Loading...</div>
30  <div v-else-if="user">
31    <h1>{{ displayName }}</h1>
32    <p>{{ user.email }}</p>
33  </div>
34</template>

Composables

TypeScript
1// composables/useFetch.ts
2import { ref, type Ref } from 'vue';
3
4export function useFetch<T>(url: string) {
5  const data: Ref<T | null> = ref(null);
6  const error: Ref<Error | null> = ref(null);
7  const isLoading = ref(false);
8
9  async function execute() {
10    isLoading.value = true;
11    error.value = null;
12    try {
13      const res = await fetch(url, { credentials: 'include' });
14      if (!res.ok) throw new Error(`HTTP ${res.status}`);
15      data.value = (await res.json()) as T;
16    } catch (e) {
17      error.value = e instanceof Error ? e : new Error(String(e));
18    } finally {
19      isLoading.value = false;
20    }
21  }
22
23  return { data, error, isLoading, execute };
24}

Pinia State Management

TypeScript
1// stores/auth.ts
2import { defineStore } from 'pinia';
3import { ref, computed } from 'vue';
4
5interface User { id: string; name: string; email: string }
6
7export const useAuthStore = defineStore('auth', () => {
8  const user = ref<User | null>(null);
9  const isAuthenticated = computed(() => user.value !== null);
10
11  async function login(email: string, password: string) {
12    const res = await fetch(`${import.meta.env.VITE_API_URL}/auth/login`, {
13      method: 'POST',
14      credentials: 'include',
15      headers: { 'Content-Type': 'application/json' },
16      body: JSON.stringify({ email, password }),
17    });
18    if (!res.ok) throw new Error('Login failed');
19    user.value = (await res.json()) as User;
20  }
21
22  function logout() {
23    user.value = null;
24  }
25
26  return { user, isAuthenticated, login, logout };
27});

Vue Router

TypeScript
1// router/index.ts
2import { createRouter, createWebHistory } from 'vue-router';
3import { useAuthStore } from '@/stores/auth';
4
5const router = createRouter({
6  history: createWebHistory(),
7  routes: [
8    { path: '/', component: () => import('@/views/HomeView.vue') },
9    { path: '/dashboard', component: () => import('@/views/DashboardView.vue'),
10      meta: { requiresAuth: true } },
11    { path: '/login', component: () => import('@/views/LoginView.vue') },
12  ],
13});
14
15router.beforeEach((to) => {
16  const auth = useAuthStore();
17  if (to.meta['requiresAuth'] && !auth.isAuthenticated) {
18    return { path: '/login' };
19  }
20});
21
22export default router;

Performance

  • Use defineAsyncComponent to lazy-load heavy components
  • Use v-memo to skip re-rendering stable list items
  • Use shallowRef and shallowReactive for large data structures you control externally
  • Enable Vite's build.rollupOptions.output.manualChunks to split vendor code

Testing

TypeScript
1// UserProfile.test.ts
2import { mount } from '@vue/test-utils';
3import { describe, it, expect, vi } from 'vitest';
4import UserProfile from './UserProfile.vue';
5
6describe('UserProfile', () => {
7  it('renders the user name after fetch', async () => {
8    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
9      ok: true,
10      json: () => Promise.resolve({ id: '1', name: 'Alice', email: '[email protected]' }),
11    }));
12
13    const wrapper = mount(UserProfile, { props: { userId: '1' } });
14    await wrapper.vm.$nextTick();
15    await wrapper.vm.$nextTick(); // second tick for async fetch
16
17    expect(wrapper.text()).toContain('Alice');
18  });
19});