Introduction
In 2026, Vue 3 leads the frontend world thanks to its Composition API, which revolutionizes code reusability and maintainability. Unlike the Options API, it lets you extract business logic into reusable composables, strict TypeScript typing, and seamless integration with tools like Pinia for state management and Vite for ultra-fast bundling. This expert tutorial guides you through building an advanced TODO app: guarded routing, local persistence, async fetching via JSONPlaceholder, and Suspense for loading states. You'll learn to scale a professional SPA while avoiding monolithic app pitfalls. By the end, you'll have a copy-paste-ready project optimized for production, SEO, and performance. Ideal for seniors striving for Vue excellence. (142 words)
Prerequisites
- Node.js 20+ and npm 10+
- Advanced Vue 3 knowledge (Options API, lifecycle hooks)
- Intermediate TypeScript (generics, interfaces)
- VS Code with Volar and TypeScript Vue extensions
- Git for version control
Initialize the Vite + Vue TS Project
npm create vue@latest todo-expert -- --template vue-ts
cd todo-expert
npm install
npm install vue-router@4 pinia @vueuse/core @tanstack/vue-query
npm install -D @types/node
npm run devThis command creates a Vite project with the Vue + TypeScript template, installs essential dependencies: Vue Router for routing, Pinia for persistent global state, VueUse for composable utilities, and TanStack Query for reactive data fetching. The --template vue-ts flag enables strict typing from the start. Run npm run dev to verify the app starts at http://localhost:5173.
Configure Vite and Plugins
Vite is Vue 3's default bundler, offering instant HMR and optimized builds. We'll extend its config to support aliases, API proxying, and strict TS resolutions—essential for a scalable expert app.
Complete vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
"@": resolve(__dirname, "src"),
},
},
server: {
proxy: {
'/api': {
target: 'https://jsonplaceholder.typicode.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^/api/, '/'),
},
},
},
})This config sets up the @ alias for imports from src/, enables the Vue plugin, and proxies /api calls to JSONPlaceholder to simulate a real backend without CORS issues. The rewrite cleans up paths. Restart the dev server after changes to apply them.
Initialize Router and Pinia in main.ts
import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'
import router from './router'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')The main.ts bootstraps the Vue app, integrates Pinia for global state, and Vue Router for SPA navigation. createPinia() enables default persistence via plugins. This entry point is the heart of every expert Vue 3 app.
Set Up Advanced Routing
Vue Router 4 supports guards, lazy-loading, and nested routes. We'll configure two views: Home for TODOs and About, with a guard to protect routes based on authentication (simulated).
router/index.ts with Guards
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: () => import('@/views/HomeView.vue'),
},
{
path: '/about',
name: 'about',
component: () => import('@/views/AboutView.vue'),
beforeEnter: (to, from) => {
const authStore = useAuthStore()
if (!authStore.isAuthenticated) return '/'
},
},
],
})
export default routerThis router uses createWebHistory for clean URLs, lazy-loads views to optimize the initial bundle, and implements a beforeEnter guard that checks auth via Pinia. Routes are implicitly typed by TS.
Pinia Store for Auth and Persistence
import { ref } from 'vue'
import { defineStore } from 'pinia'
import { useLocalStorage } from '@vueuse/core'
export const useAuthStore = defineStore('auth', () => {
const token = useLocalStorage('auth-token', '')
const isAuthenticated = ref(!!token.value)
function login(newToken: string) {
token.value = newToken
isAuthenticated.value = true
}
function logout() {
token.value = ''
isAuthenticated.value = false
}
return { token, isAuthenticated, login, logout }
}, {
persist: true,
})This store uses defineStore with Composition API and @vueuse/core for local persistence via useLocalStorage. The login/logout actions mutate reactive state. The persist: true option auto-saves to sessionStorage.
Create Reusable Composables
Composables are Vue 3's superpower: pure functions exporting reactive state and logic. We'll create one for fetching TODOs with TanStack Query, integrated with Suspense.
Composable useTodos with Vue Query
import { ref } from 'vue'
import { useQuery } from '@tanstack/vue-query'
interface Todo {
id: number
title: string
completed: boolean
}
export function useTodos() {
return useQuery({
queryKey: ['todos'],
queryFn: async (): Promise<Todo[]> => {
const res = await fetch('/api/todos?_limit=5')
if (!res.ok) throw new Error('Fetch failed')
return res.json()
},
})
}This composable wraps TanStack's useQuery for caching, auto-refetching, and states (loading/error). Typed with Todo interface, it fetches via the Vite proxy. Fully reusable, it boosts performance by avoiding unnecessary re-fetches.
HomeView Component with Suspense
<template>
<div>
<h1>Mes TODOs Experts</h1>
<Suspense>
<template #default>
<TodoList />
</template>
<template #fallback>
<div>Chargement des tâches...</div>
</template>
</Suspense>
<router-link to="/about">About (protégé)</router-link>
</div>
</template>
<script setup lang="ts">
import TodoList from '@/components/TodoList.vue'
</script>This component uses to handle async states in children like TodoList (which uses the composable). is the concise syntax for Composition API. The router link navigates to the guarded route.
Assemble App.vue with Global Layout
App.vue orchestrates the layout: Navbar with RouterLink, RouterView for pages, and provider for Vue Query.
Main App.vue Layout
<template>
<div id="app">
<nav>
<RouterLink to="/">Home</RouterLink>
<RouterLink to="/about">About</RouterLink>
</nav>
<RouterView />
</div>
</template>
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
</script>
<style scoped>
nav { display: flex; gap: 1rem; padding: 1rem; }
</style> dynamically renders route components. Scoped styles limit CSS to the component. Auto-imports via Volar. This layout is ready for global themes or transitions.
Best Practices
- Always type with TS: Interfaces for props, generics for composables.
- Extract to composables: Any reusable logic (fetching, form validation).
- Pinia + plugins: Persistence and devtools for state debugging.
- Suspense + Query: Handle loading/error at the component level.
- Lazy routes: Reduce initial bundle by 70%.
Common Errors to Avoid
- Ignoring
reactive()vsref():reffor primitives,reactivefor objects. - Forgetting
queryClientprovider: Wrap the app with. - No guards on sensitive routes: Always check auth before rendering.
- Builds without proxy: CORS blocks in prod; use a real backend or adapter.
Next Steps
Test with Vitest: npm install -D vitest @vue/test-utils. Explore Nuxt 3 for SSR. Resources: Vue 3 Docs, Pinia. Check our expert Vue trainings at Learni for live masterclasses.