Project Setup
The standard way to create a Vue 3 project is create-vue, which scaffolds a Vite-powered application with optional TypeScript, Vue Router, Pinia, and testing. Unlike Create React App or Angular CLI, the Vue toolchain is fast, minimal, and configurable without ejecting.
Creating a Project with create-vue
npm create vue@latest
Project name: my-app
Add TypeScript? Yes
Add JSX Support? No
Add Vue Router? Yes
Add Pinia? Yes
Add Vitest? Yes
Add an End-to-End Testing Solution? Playwright
Add ESLint? Yes
Add Prettier? Yes
Add Vue DevTools extension? Yes
cd my-app
npm install
npm run dev
The dev server starts in under a second. Hot Module Replacement (HMR) updates the browser instantly when you save a file. No full-page reload, no lost component state.
Project Structure
After scaffolding, the project looks like this:
my-app/
src/
assets/ # Static assets (images, fonts, global CSS)
components/ # Reusable UI components
composables/ # Composition API functions (useXxx)
router/ # Vue Router configuration
index.ts
stores/ # Pinia stores
views/ # Page-level components (mapped to routes)
App.vue # Root component
main.ts # Application entry point
public/ # Static files served as-is
index.html # HTML entry point
vite.config.ts # Vite configuration
tsconfig.json # TypeScript configuration
package.json
Each directory has a clear purpose:
- components/: Reusable pieces of UI. A
BaseButton.vue, aUserAvatar.vue, aDataTable.vue. These are not tied to specific routes. - views/: Components that represent full pages.
HomeView.vue,UserProfileView.vue. These map to routes in the router configuration. - composables/: Reusable logic functions.
useAuth.ts,useFetch.ts,useLocalStorage.ts. Named with theuseprefix by convention. - stores/: Pinia state stores.
useUserStore.ts,useCartStore.ts. Each store manages a slice of global state. - router/: Route definitions and navigation guards.
The Entry Point
main.ts bootstraps the application:
// src/main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import './assets/main.css'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')
createApp creates the Vue application instance. app.use() installs plugins. app.mount() attaches the app to the DOM element with id="app" in index.html. The order matters: plugins must be installed before mounting.
Vue Router Configuration
The router maps URL paths to view components:
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '@/views/HomeView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView,
},
{
path: '/about',
name: 'about',
// Lazy loading: only loads when the route is visited
component: () => import('@/views/AboutView.vue'),
},
{
path: '/users/:id',
name: 'user-profile',
component: () => import('@/views/UserProfileView.vue'),
props: true,
},
],
})
export default router
createWebHistory uses the browser's History API for clean URLs without hash fragments. Lazy-loaded routes using dynamic import() split the bundle so users only download the code they need.
Pinia Store Setup
A basic Pinia store using setup syntax:
// src/stores/useUserStore.ts
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', () => {
const user = ref<{ id: number; name: string; email: string } | null>(null)
const token = ref<string | null>(null)
const isAuthenticated = computed(() => !!token.value)
const displayName = computed(() => user.value?.name ?? 'Guest')
async function login(email: string, password: string) {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
})
const data = await res.json()
user.value = data.user
token.value = data.token
}
function logout() {
user.value = null
token.value = null
}
return { user, token, isAuthenticated, displayName, login, logout }
})
TypeScript Configuration
The default tsconfig.json works for most projects. Key settings:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"jsx": "preserve",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}
The @/* path alias lets you write import Foo from '@/components/Foo.vue' instead of relative paths with ../../. This alias is configured in both tsconfig.json (for TypeScript) and vite.config.ts (for the bundler).
Vite Configuration
// vite.config.ts
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
})
The server.proxy configuration forwards API requests during development. Requests to /api/* go to your backend at port 8080 without CORS issues.
Auto-Imports with unplugin-auto-import
Tired of writing import { ref, computed, watch } from 'vue' in every file? unplugin-auto-import handles it:
npm install -D unplugin-auto-import
// vite.config.ts
import AutoImport from 'unplugin-auto-import/vite'
export default defineConfig({
plugins: [
vue(),
AutoImport({
imports: ['vue', 'vue-router', 'pinia'],
dts: 'src/auto-imports.d.ts',
}),
],
})
Now ref, computed, watch, useRouter, useRoute, and other common imports are available without explicit import statements:
<script setup lang="ts">
// No import needed: ref and computed are auto-imported
const count = ref(0)
const doubled = computed(() => count.value * 2)
</script>
The plugin generates a src/auto-imports.d.ts file so TypeScript knows about these globals. You should commit this file to your repository.
The Dev Server & HMR
Vite's dev server uses native ES modules in the browser. Instead of bundling your entire application on startup, it serves each module individually. The result is sub-second startup regardless of project size.
Hot Module Replacement (HMR) is Vue-aware. When you edit a component:
- Template changes update instantly without losing component state.
- Script changes re-execute the setup function, resetting local state but preserving Pinia store state.
- Style changes apply immediately with no flash.
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<!-- Edit this text and save: the browser updates instantly -->
<!-- count retains its value if you only changed the template -->
<button @click="count++">Count: {{ count }}</button>
</template>
<style scoped>
/* Edit styles here: they apply immediately, no reload */
button {
padding: 0.5rem 1rem;
background: #42b883;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
Common Pitfalls
- Skipping TypeScript: Vue 3's TypeScript support is excellent. Starting without it and adding it later is painful. Always select TypeScript during project creation.
- Forgetting the path alias in both configs: The
@alias needs to be configured in bothtsconfig.json(for type checking) andvite.config.ts(for module resolution). Missing either causes confusing errors. - Not using lazy loading for routes: Every route component imported statically adds to the initial bundle. Use dynamic
import()for all routes except the home page. - Ignoring the composables directory: New developers often put reusable logic inside components instead of extracting it to composables. Create the
composables/directory from day one and use it. - Over-configuring Vite: Vite's defaults are sensible. Most projects need only the Vue plugin, the path alias, and possibly a dev proxy. Adding plugins and configuration you do not yet need creates maintenance burden.
Key Takeaways
create-vuescaffolds a Vite-powered project with TypeScript, Vue Router, Pinia, and testing in under a minute.- The project structure separates components, views, composables, stores, and router configuration into distinct directories.
main.tsbootstraps the app by creating the Vue instance, installing plugins, and mounting to the DOM.- Vite provides sub-second startup and Vue-aware HMR that preserves component state during development.
unplugin-auto-importeliminates repetitive imports for Vue, Vue Router, and Pinia APIs.- Always start with TypeScript, lazy-loaded routes, and the
@path alias configured in both TypeScript and Vite configs.