Vue.js is a progressive JavaScript framework for building user interfaces and single-page applications. This guide covers the full development lifecycle: Vue’s reactive architecture, the Composition API and <script setup> syntax, Pinia for state management, Vue Router for navigation, Vitest and Vue Test Utils for testing, and production deployment with Vite. Whether you are migrating from Vue 2 or starting fresh on Vue 3, the sections below give you a practical, end-to-end blueprint.
- Architecture: Reactive data system with a virtual DOM and component tree.
- Composition API: Modular, reusable logic via
setup()and<script setup>. - State: Pinia replaces Vuex as the official store with full TypeScript support.
- Routing: Vue Router 4 with lazy loading, guards, and dynamic segments.
- Testing: Vitest for unit tests, Vue Test Utils for component mounting.
- Deployment: Vite build produces optimized static assets for any CDN or host.
01 Vue.js Overview
Vue.js was created by Evan You in 2014 as a lightweight alternative to heavier frameworks. It is officially described as progressive, meaning you can adopt it incrementally, drop a single script tag into an existing page for interactivity, or build a full single-page application (SPA) with a modern toolchain. Vue 3, released in September 2020, rewrote the core in TypeScript and introduced the Composition API as the recommended way to author components.
Why Developers Choose Vue
Vue occupies a pragmatic middle ground between the minimalism of libraries like Alpine.js and the full ecosystem of React or Angular. Its single-file components (.vue files) collocate template, logic, and styles, which keeps related code together and improves maintainability. The reactivity system automatically tracks dependencies, so the UI updates whenever underlying data changes, no manual diffing required.
| Aspect | Vue 2 | Vue 3 |
|---|---|---|
| Reactivity | Object.defineProperty |
Proxy-based |
| API Style | Options API only | Options + Composition API |
| TypeScript | Partial support | First-class, written in TS |
| State Management | Vuex | Pinia (official) |
| Build Tool | Vue CLI (webpack) | Vite (default) |
| Tree Shaking | Limited | Full ES module support |
@vue/compat build helps large Vue 2 codebases transition incrementally.
02 Vue Architecture
Vue applications are organized as a tree of components. At runtime, the framework maintains a virtual DOM, a lightweight JavaScript representation of the actual DOM. When reactive state changes, Vue computes a minimal set of DOM mutations and applies them in a batched, efficient update cycle.
Core Architectural Layers
Reactivity System
Uses ES6 Proxies to intercept reads and writes. When a component reads reactive state during render, a dependency is recorded. On mutation, only the affected components re-render.
Virtual DOM
Each component compiles to a render function that returns virtual nodes (VNodes). The diffing algorithm compares old and new VNode trees and patches the real DOM with minimal operations.
Component Model
Components are self-contained units with props, events, slots, and lifecycle hooks. They compose into a tree, enabling reuse, encapsulation, and predictable data flow.
Compiler
The template compiler runs at build time (via Vite) to convert HTML-like templates into optimized JavaScript render functions, removing the runtime compilation cost.
Project Structure
A standard Vite-powered Vue project follows this layout:
my-vue-app/
├── index.html
├── package.json
├── vite.config.js
├── src/
│ ├── main.js # App entry, creates and mounts root
│ ├── App.vue # Root component
│ ├── components/ # Reusable UI components
│ ├── views/ # Route-level page components
│ ├── stores/ # Pinia store definitions
│ ├── router/ # Vue Router configuration
│ └── assets/ # Images, fonts, styles
└── public/ # Static files served as-is
components/ for reusable, presentational pieces and views/ for full pages tied to routes. This separation makes navigation logic easier to reason about as the app grows.
03 Components and the Composition API
The Composition API is Vue 3’s recommended authoring style. Instead of grouping code by option type (data, methods, computed), you group logic by feature. This makes complex components easier to read and allows you to extract reusable logic into composable functions.
Single-File Component with <script setup>
The <script setup> compiler macro is the idiomatic way to write components. It automatically exposes top-level bindings to the template and requires less boilerplate than an explicit setup() function.
<script setup>
import { ref, computed, onMounted } from 'vue'
// Reactive state
const count = ref(0)
const items = ref([])
// Computed value, re-computes when count changes
const double = computed(() => count.value * 2)
// Method
function increment() {
count.value++
}
// Lifecycle hook
onMounted(() => {
console.log('Component mounted, count =', count.value)
})
</script>
<template>
<div class="counter">
<p>Count: {{ count }} (doubled: {{ double }})</p>
<button @click="increment">Increment</button>
</div>
</template>
<style scoped>
.counter { padding: 1rem; border: 1px solid #ddd; }
</style>
Core Reactivity Primitives
| Primitive | Purpose | Example |
|---|---|---|
ref() |
Single reactive value (any type) | const name = ref('Vue') |
reactive() |
Reactive object (deep) | const state = reactive({ user: null }) |
computed() |
Derived, cached value | const full = computed(() => first + last) |
watch() |
Side effect on change | watch(count, (n) => save(n)) |
watchEffect() |
Auto-tracked side effect | watchEffect(() => log(count.value)) |
Composables: Reusable Logic
Composables are functions that encapsulate reactive state and methods. They are Vue’s replacement for Vue 2 mixins and avoid the naming collisions and unclear source problems mixins had.
// composables/useFetch.js
import { ref, watchEffect } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(true)
watchEffect(async () => {
loading.value = true
error.value = null
try {
const res = await fetch(url.value)
data.value = await res.json()
} catch (e) {
error.value = e.message
} finally {
loading.value = false
}
})
return { data, error, loading }
}
ref requires .value access in JavaScript but is auto-unwrapped in templates. Forgetting .value inside <script> is the most common beginner mistake, it silently assigns to a plain variable instead of updating reactivity.
04 State Management with Pinia
Pinia is the official state management library for Vue 3, succeeding Vuex. It is built on the Composition API, has full TypeScript inference, and removes the mutations layer that Vuex required. Stores are simpler to write and the devtools integration is excellent.
Defining a Store
// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
history: []
}),
getters: {
double: (state) => state.count * 2,
isPositive: (state) => state.count > 0
},
actions: {
increment() {
this.count++
this.history.push(Date.now())
},
reset() {
this.count = 0
this.history = []
}
}
})
Pinia also supports a setup-style store syntax that mirrors composables:
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const double = computed(() => count.value * 2)
function increment() { count.value++ }
return { count, double, increment }
})
Using a Store in a Component
<script setup>
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'
const store = useCounterStore()
const { count, double } = storeToRefs(store) // reactive refs
</script>
<template>
<p>{{ count }}, doubled: {{ double }}</p>
<button @click="store.increment">+1</button>
</template>
const { count } = store), it breaks reactivity. Always use storeToRefs() for state and getters, and call actions directly on the store instance.
| Feature | Vuex (Vue 2 era) | Pinia (Vue 3) |
|---|---|---|
| Mutations | Required for state changes | Removed, actions mutate directly |
| Modules | Namespaced nested modules | Flat, independent stores |
| TypeScript | Verbose, manual typing | Full automatic inference |
| Size | ~7 KB | ~1.5 KB |
| Devtools | Time-travel supported | Time-travel + improved UI |
05 Routing with Vue Router
Vue Router 4 is the official routing library for Vue 3. It maps URLs to components, supports nested routes, dynamic parameters, query strings, and navigation guards for access control.
Basic Router Setup
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{ path: '/', name: 'home', component: () => import('@/views/Home.vue') },
{ path: '/about', name: 'about', component: () => import('@/views/About.vue') },
{ path: '/user/:id', name: 'user', component: () => import('@/views/User.vue') },
{ path: '/:pathMatch(.*)*', name: 'not-found', component: () => import('@/views/NotFound.vue') }
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
scrollBehavior(to, from, savedPosition) {
return savedPosition || { top: 0 }
}
})
export default router
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
createApp(App)
.use(createPinia())
.use(router)
.mount('#app')
Navigation Guards
Guards let you run logic before a route is confirmed. Common uses include authentication checks, analytics logging, and data preloading.
router.beforeEach((to, from) => {
const auth = useAuthStore()
if (to.meta.requiresAuth && !auth.isAuthenticated) {
return { name: 'login', query: { redirect: to.fullPath } }
}
})
| Guard Type | Scope | Typical Use |
|---|---|---|
beforeEach |
Global | Auth check, analytics |
beforeResolve |
Global | After component guards, before confirm |
afterEach |
Global | Page title, scroll restoration |
beforeEnter |
Per-route | Route-specific preconditions |
beforeRouteEnter |
In-component | Data fetch before mount |
import() (as shown above). Vite splits each route into a separate chunk, reducing the initial bundle size and improving time-to-interactive.
06 Testing Vue Applications
Vue testing centers on two tools: Vitest, a fast unit test runner built on Vite’s transform pipeline, and Vue Test Utils, a library for mounting and interacting with components in isolation.
Component Test Example
// Counter.spec.js
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Counter from './Counter.vue'
describe('Counter', () => {
it('renders initial count', () => {
const wrapper = mount(Counter)
expect(wrapper.text()).toContain('Count: 0')
})
it('increments on button click', async () => {
const wrapper = mount(Counter)
await wrapper.find('button').trigger('click')
expect(wrapper.text()).toContain('Count: 1')
})
})
npm i -D vitest @vue/test-utils jsdomtest: { environment: 'jsdom' } to vite.config.js.*.spec.js files next to components.npx vitest, watch mode by default.npx vitest run --coverage for a report.Testing Pyramid for Vue
| Layer | Tool | What to Test |
|---|---|---|
| Unit | Vitest | Composables, utils, pure functions |
| Component | Vue Test Utils | Props, events, slots, rendering |
| E2E | Playwright / Cypress | User flows, routing, integration |
07 Deployment
Vue applications built with Vite compile into a set of static HTML, JavaScript, and CSS files. Because the output is static, you can host it on virtually any platform, a CDN, object storage, or a traditional web server.
Build and Deploy Steps
npm run build produces an optimized dist/ directory.npm run preview serves the build locally to verify.dist/ to your host (Netlify, Vercel, S3, GitHub Pages, or your own server).index.html so client-side routing works on refresh.Cache-Control for hashed assets, short for index.html.Hosting Platform Comparison
Vercel
Zero-config deployment via Git integration. Automatic HTTPS, preview branches, and edge network. Ideal for SPAs and full-stack apps.
Netlify
Continuous deployment, form handling, and serverless functions. Drag-and-drop dist/ for quick manual deploys.
S3 + CloudFront
Cost-effective for high-traffic sites. Upload to S3, serve via CloudFront with invalidation on each deploy.
GitHub Pages
Free hosting for project sites. Use createWebHistory with the correct base path for subdirectory routing.
SPA Fallback Configuration
For Nginx, add this to your server block so deep links resolve correctly:
location / {
try_files $uri $uri/ /index.html;
}
index.html aggressively, it references hashed asset filenames that change on every build. A stale index.html will load old chunks that no longer exist, breaking the app. Use no-cache or a short max-age for HTML, and immutable, max-age=31536000 for /assets/*.
08 Action: Start Building
You now have the full picture of a modern Vue 3 application, from reactivity fundamentals through to production deployment. The fastest way to internalize these concepts is to build something. Here is a concrete path to get a working app running in under an hour.
npm create vue@latest and select TypeScript, Router, Pinia, and Vitest./post/:id route and a navigation guard.Recommended Learning Resources
| Resource | Type | Best For |
|---|---|---|
| vuejs.org Guide | Docs | Official, up-to-date reference |
| Vue Mastery | Video courses | Structured, beginner-to-advanced |
| Awesome Vue (GitHub) | Curated list | Discovering libraries and plugins |
| Vue Discord / Forum | Community | Getting help and staying current |
Dev Station works with teams across the United States and the United Kingdom. Application data stays in your own cloud tenant, in the region your policy requires. Where a client needs SOC 2, HIPAA or UK GDPR evidence, we build the technical controls those frameworks ask for and work alongside the assessor who issues the certificate. Our engineers work from Vietnam with overlap into US Eastern, US Pacific and UK GMT hours, and we invoice in USD or GBP.
Want an AI assistant to summarize or cite this guide?
Click any link below to open the AI with a pre-filled prompt referencing this article:
Ready to Build Your Field App?
Contact Dev Station Technology to discuss your project requirements and receive a development roadmap within 48 hours.
Get a Quote →

