Vue 3 + TypeScript : bonnes pratiques et architecture entreprise en 2025

Le mois dernier, l’équipe a tenu une réunion de choix technique : état global, arborescence, config TS — sans consensus. Ma première config Vue 3 + TypeScript ? J’ai retouché tsconfig.json une vingtaine de fois ; sur une autre machine, tout replantait.
Cet article résume ce qu’on a appris après de nombreux essais. Ce n’est pas la solution parfaite, mais trois projets moyens/grands tournent sans gros incident. Nouveau projet ou refonte d’architecture : ces pistes devraient vous faire gagner du temps.
Choix de stack Vue 3 en 2025
Vue 3 est là depuis longtemps ; en 2025, le choix est assez clair.
Notre stack : Vite + Vue 3 + TypeScript + Pinia + Vue Router 4. Beaucoup l’utilisent déjà ; voici pourquoi ces briques.
Vite : HMR quasi instantané, bien meilleur que Webpack au quotidien. Vue 3.6 est encore en alpha, mais Evan You a montré à Vue.js Nation 2025 des chiffres impressionnants — le Vapor Mode peut monter 100 000 composants en ~100 ms. Pas pour la prod aujourd’hui, mais la direction est bonne.
Pinia en priorité. En voyant l’API, ma première réaction : « enfin une gestion d’état comme il faut ». Comparaison :
// Vuex (verbeux)
const store = createStore({
state: () => ({ count: 0 }),
mutations: {
increment(state) { state.count++ }
},
actions: {
incrementAsync({ commit }) {
setTimeout(() => commit('increment'), 1000)
}
}
})
// Pinia (plus direct)
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const increment = () => count.value++
const incrementAsync = () => setTimeout(increment, 1000)
return { count, increment, incrementAsync }
})
Pinia fait environ 1,5 Ko. Sauf projet fortement lié à Vuex sans plan de migration, les nouveaux projets n’ont plus vraiment intérêt à Vuex.
Structure de répertoires
Trop de projets empilent tout dans components/ — trois mois plus tard, personne ne retrouve rien.
Sur un petit repo, tout va ; à l’échelle, sans convention c’est le chaos. Après plusieurs essais, on est restés sur ceci :
src/
├── api/ # Couche API
│ ├── modules/ # Par module métier
│ │ ├── user.ts
│ │ └── order.ts
│ └── index.ts
├── assets/ # Assets statiques
│ ├── images/
│ └── styles/
├── components/ # Composants globaux
│ ├── base/ # Button, Input, etc.
│ └── business/ # Composants métier partagés
├── composables/ # Fonctions composables
│ ├── useAuth.ts
│ └── useRequest.ts
├── layouts/ # Layouts de page
├── router/ # Routes
│ ├── modules/
│ └── index.ts
├── stores/ # Pinia
│ ├── modules/
│ └── index.ts
├── types/ # Types globaux
│ ├── api.d.ts
│ └── global.d.ts
├── utils/ # Utilitaires
├── views/ # Pages
│ ├── user/
│ └── order/
├── App.vue
└── main.ts
Points clés :
composables vs utils : composables = logique réactive (useAuth, useRequest) ; utils = fonctions pures (formatDate, debounce). Les mélanger complique la maintenance.
Découpage par domaine : sous-dossiers métier dans api, stores, views — tout ce qui concerne l’utilisateur vit sous user/.
types à part : types globaux dans types/ ; types locaux dans le composant. Ne pas tout jeter dans un seul dossier types/.
Bonnes pratiques TypeScript
Le « sport » des types peut décourager ; quelques patterns couvrent la majorité des cas.
tsconfig.json — à activer :
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"jsx": "preserve",
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["ESNext", "DOM"],
"skipLibCheck": true,
"noEmit": true,
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}
strict: true : beaucoup d’erreurs au début, rentable à long terme. moduleResolution: "bundler" va bien avec Vite.
Typage des composants — defineProps<Props>() peut surprendre au premier coup ; c’est géré par le compilateur Vue :
<script setup lang="ts">
// Props
interface Props {
title: string
count?: number
items: string[]
}
const props = defineProps<Props>()
// Props avec défauts
const propsWithDefaults = withDefaults(defineProps<Props>(), {
count: 0,
items: () => []
})
// Emits
interface Emits {
(e: 'update', value: string): void
(e: 'delete', id: number): void
}
const emit = defineEmits<Emits>()
// Ou plus concis (Vue 3.3+)
const emit2 = defineEmits<{
update: [value: string]
delete: [id: number]
}>()
</script>
Fichiers .vue non reconnus par l’IDE : ajouter env.d.ts ou shims-vue.d.ts dans src/ :
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
Pinia en pratique
Après les mutations Vuex, modifier le state directement dans Pinia semble « interdit » — en réalité, les mutations étaient surtout un héritage Flux ; Pinia s’en débarrasse.
Store en style Composition API :
// stores/modules/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { getUserInfo, login } from '@/api/modules/user'
export const useUserStore = defineStore('user', () => {
// state
const token = ref<string>('')
const userInfo = ref<UserInfo | null>(null)
// getters
const isLoggedIn = computed(() => !!token.value)
const userName = computed(() => userInfo.value?.name ?? 'Invité')
// actions
const setToken = (newToken: string) => {
token.value = newToken
localStorage.setItem('token', newToken)
}
const fetchUserInfo = async () => {
try {
const res = await getUserInfo()
userInfo.value = res.data
} catch (error) {
console.error('Échec récupération utilisateur', error)
}
}
const logout = () => {
token.value = ''
userInfo.value = null
localStorage.removeItem('token')
}
return {
token,
userInfo,
isLoggedIn,
userName,
setToken,
fetchUserInfo,
logout
}
})
Piège classique — la déstructuration casse la réactivité ; utiliser storeToRefs() :
import { storeToRefs } from 'pinia'
const userStore = useUserStore()
// Incorrect — perte de réactivité
const { userName, isLoggedIn } = userStore
// Correct
const { userName, isLoggedIn } = storeToRefs(userStore)
// Les actions se déstructurent directement (fonctions normales)
const { logout, fetchUserInfo } = userStore
Persistance : pinia-plugin-persistedstate :
// main.ts
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
// Dans le store
export const useUserStore = defineStore('user', () => {
// ...
}, {
persist: true // ou options détaillées
})
Vue Router 4 : routes typées
Les guards beforeEach s’accumulent vite — un projet avait ~200 lignes (droits, tracking, titre) ; on a découpé en plusieurs guards.
Types pour meta :
// router/index.ts
import type { RouteRecordRaw } from 'vue-router'
import { createRouter, createWebHistory } from 'vue-router'
declare module 'vue-router' {
interface RouteMeta {
title?: string
requiresAuth?: boolean
roles?: string[]
}
}
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'Home',
component: () => import('@/views/home/index.vue'),
meta: {
title: 'Accueil',
requiresAuth: false
}
},
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('@/views/dashboard/index.vue'),
meta: {
title: 'Tableau de bord',
requiresAuth: true,
roles: ['admin', 'editor']
}
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
Guard d’authentification :
// router/guards/auth.ts
import type { Router } from 'vue-router'
import { useUserStore } from '@/stores/modules/user'
export function setupAuthGuard(router: Router) {
router.beforeEach((to, from, next) => {
const userStore = useUserStore()
if (!to.meta.requiresAuth) {
next()
return
}
if (!userStore.isLoggedIn) {
next({ path: '/login', query: { redirect: to.fullPath } })
return
}
const { roles } = to.meta
if (roles && roles.length > 0) {
const hasRole = roles.some(role => userStore.userInfo?.roles?.includes(role))
if (!hasRole) {
next({ path: '/403' })
return
}
}
next()
})
}
Modulariser par domaine métier :
// router/modules/user.ts
export const userRoutes: RouteRecordRaw[] = [
{
path: '/user',
name: 'User',
component: () => import('@/layouts/BasicLayout.vue'),
children: [
{
path: 'profile',
name: 'UserProfile',
component: () => import('@/views/user/profile.vue')
}
]
}
]
Qualité de code et outillage
ESLint 9 en flat config : migration d’un après-midi depuis .eslintrc :
// eslint.config.js
import js from '@eslint/js'
import vue from 'eslint-plugin-vue'
import typescript from '@typescript-eslint/eslint-plugin'
import tsParser from '@typescript-eslint/parser'
import vueParser from 'vue-eslint-parser'
export default [
js.configs.recommended,
...vue.configs['flat/recommended'],
{
files: ['**/*.{ts,tsx,vue}'],
languageOptions: {
parser: vueParser,
parserOptions: {
parser: tsParser,
sourceType: 'module'
}
},
plugins: {
'@typescript-eslint': typescript
},
rules: {
'vue/multi-word-component-names': 'off',
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/no-explicit-any': 'warn'
}
}
]
Conflits Prettier/ESLint : eslint-config-prettier.
unplugin-auto-import importe Vue, Vue Router, Pinia automatiquement :
// vite.config.ts
import AutoImport from 'unplugin-auto-import/vite'
export default defineConfig({
plugins: [
AutoImport({
imports: ['vue', 'vue-router', 'pinia'],
dts: 'src/auto-imports.d.ts',
eslintrc: {
enabled: true
}
})
]
})
Ensuite ref(), computed() sans import manuel.
Commits : husky + lint-staged :
// package.json
{
"scripts": {
"prepare": "husky install",
"lint": "eslint . --fix",
"lint-staged": "lint-staged"
},
"lint-staged": {
"*.{js,ts,vue}": ["eslint --fix", "prettier --write"]
}
}
Pour conclure
Au début de Vue 3, la Composition API et les tutoriels hétérogènes faisaient beaucoup d’erreurs — on connaît.
Cette stack n’est pas universelle. Petit projet : provide/inject, arborescence légère. Projet entreprise, équipe ≥ 3 personnes : cette base a fait ses preuves.
Pas de solution miracle — consensus d’équipe, conventions, exécution. J’espère que cela vous évite quelques détours.
Comment structurez-vous vos projets Vue 3 ? Partagez vos retours en commentaires.
Mettre en place une architecture Vue 3 + TypeScript entreprise
De l'initialisation aux règles de code : Vite, Pinia, Vue Router, ESLint et bonnes pratiques
⏱️ Estimated time: 4 hr
- 1
Step 1: Initialisation et choix de stack
Créer le projet Vite :
• npm create vite@latest my-project -- --template vue-ts
• cd my-project
• npm install
Stack :
• Vite + Vue 3 + TypeScript + Pinia + Vue Router 4
• npm install pinia vue-router@4
Atouts :
• HMR quasi instantané vs Webpack
• Vapor Mode (Vue 3.6) : ~100 ms pour 100 000 composants — pas encore prod, mais prometteur - 2
Step 2: Concevoir l'arborescence
Découpage par domaine :
• src/api/modules/ (user.ts, order.ts…)
• src/components/base/ et business/
• src/composables/ (useAuth, useRequest — logique réactive)
• src/utils/ (formatDate, debounce — fonctions pures)
• src/stores/modules/
• src/views/ (user/, order/…)
• src/types/ (types globaux)
• Séparer composables et utils
• Sous-dossiers métier dans api, stores, views - 3
Step 3: Configurer TypeScript
tsconfig.json :
• strict: true
• moduleResolution: bundler (avec Vite)
• alias @/*
Composants Vue :
• defineProps<Props>()
• withDefaults(defineProps<Props>(), { count: 0, items: () => [] })
• defineEmits<Emits>()
Fichiers .vue :
• env.d.ts ou shims-vue.d.ts avec declare module '*.vue' - 4
Step 4: Configurer Pinia
Store en Composition API :
defineStore('user', () => {
const token = ref<string>('');
const userInfo = ref<UserInfo | null>(null);
const isLoggedIn = computed(() => !!token.value);
const fetchUserInfo = async () => { ... };
return { token, userInfo, isLoggedIn, fetchUserInfo }
})
Usage :
• Réactivité : storeToRefs(userStore)
• Actions : déstructuration directe
Persistance :
• pinia-plugin-persistedstate
• pinia.use(piniaPluginPersistedstate)
• persist: true dans le store - 5
Step 5: Vue Router 4 typé
Étendre RouteMeta :
declare module 'vue-router' {
interface RouteMeta {
title?: string;
requiresAuth?: boolean;
roles?: string[];
}
}
• routes: RouteRecordRaw[]
• setupAuthGuard : beforeEach, requiresAuth, roles
• Modules router/modules/*.ts fusionnés dans index.ts - 6
Step 6: ESLint 9 et conventions
Flat config (eslint.config.js) :
• js.configs.recommended, vue flat/recommended
• vue-eslint-parser + @typescript-eslint/parser
• eslint-config-prettier
• unplugin-auto-import (vue, vue-router, pinia)
• husky + lint-staged avant commit
FAQ
Pinia vs Vuex : pourquoi Pinia ?
Vuex : state, mutations, actions :
const store = createStore({
state: () => ({ count: 0 }),
mutations: { increment(state) { state.count++ } },
actions: { incrementAsync({ commit }) { setTimeout(() => commit('increment'), 1000) } }
})
Pinia (Composition API) :
export const useCounterStore = defineStore('counter', () => {
const count = ref(0);
const increment = () => count.value++;
const incrementAsync = () => setTimeout(increment, 1000);
return { count, increment, incrementAsync };
})
Avantages Pinia :
• Pas de mutations
• Meilleur TypeScript
• Plus léger, API plus claire
Sauf projet lié à Vuex sans migration, préférez Pinia.
Comment structurer un projet Vue 3 ? Différence composables / utils ?
• api/modules/, components/base|business/
• composables/ (logique réactive)
• utils/ (fonctions pures)
• stores/modules/, views/ par métier
• types/ global
composables vs utils :
• composables : useAuth, useRequest
• utils : formatDate, debounce
• Ne pas mélanger — maintenance difficile
api, stores, views : sous-dossiers métier (ex. user/) pour retrouver vite les fichiers.
Comment configurer TypeScript avec Vue 3 ?
• strict: true
• moduleResolution: bundler
• paths @/*
Composants :
• defineProps<Props>()
• withDefaults pour les défauts
• defineEmits<{ update: [value: string]; delete: [id: number] }>()
.vue :
env.d.ts ou shims-vue.d.ts avec declare module '*.vue'.
IDE qui ne trouve pas *.vue : souvent ce fichier manque.
Comment utiliser Pinia ? Pièges à éviter ?
• Déstructuration réactive : storeToRefs(userStore)
• Actions : déstructuration directe
Persistance : pinia-plugin-persistedstate, persist: true.
Modifier le state sans mutations peut surprendre après Vuex — c'est voulu par Pinia.
ESLint 9 flat config : comment migrer ?
• eslint.config.js
• js.configs.recommended, vue flat/recommended
• vue-eslint-parser, @typescript-eslint/parser
• eslint-config-prettier
• unplugin-auto-import dans vite.config.ts
• husky + lint-staged dans package.json
Vue Router 4 : routes typées et guards ?
• RouteRecordRaw[]
• setupAuthGuard : requiresAuth, redirect login, rôles → 403
• router/modules/*.ts + fusion index
Éviter un seul beforeEach géant (~200 lignes) : plusieurs guards spécialisés.
6 min de lecture · Publié le: 24 nov. 2025 · Mis à jour le: 27 juil. 2026
Framework frontend
Si vous arrivez depuis la recherche, le plus rapide est de passer à l’article précédent ou suivant de cette série.
Précédent
Formulaire React 19 en 30 lignes ? Actions en un clin d'œil, perf +40 %
Analyse approfondie des 6 fonctionnalités clés de React 19 : Actions, use(), Compiler, etc. Comparaisons code avant/après pour simplifier les formulaires, optimiser les performances et maîtriser les Server Components. Retour d'une semaine de pratique pour monter en compétence rapidement.
Partie 4 sur 6
Suivant
Optimisation des performances frontend : guide Core Web Vitals pour viser le score parfait
Guide systématique pour optimiser les trois métriques Core Web Vitals (LCP/INP/CLS) : images, code splitting, lazy loading et plus de 10 solutions concrètes. Passez de 60 à 90+ en 2 semaines, avec checklist complète et pièges à éviter.
Partie 6 sur 6



Commentaires
Connectez-vous avec GitHub pour laisser un commentaire