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 devComposition 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
defineAsyncComponentto lazy-load heavy components - Use
v-memoto skip re-rendering stable list items - Use
shallowRefandshallowReactivefor large data structures you control externally - Enable Vite's
build.rollupOptions.output.manualChunksto 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});