Skip to main content

Dev Station Technology

Vue js app development

Mastering Vue JS App Development: A Step-by-Step Guide

TL;DR

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.

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.

2014
Initial release
~34 KB
Min+gzip runtime
Vue 3.x
Current major version
MIT
Open-source license

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
Migration note: Vue 2 reached end of life on December 31, 2023. New projects should use Vue 3. The official @vue/compat build helps large Vue 2 codebases transition incrementally.

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
Convention: Keep 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.

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 }
}
Pitfall: 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.

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>
Pitfall: Do not destructure store state directly (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

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
Performance: Lazy-load route components with dynamic import() (as shown above). Vite splits each route into a separate chunk, reducing the initial bundle size and improving time-to-interactive.

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')
  })
})
1

Install: npm i -D vitest @vue/test-utils jsdom
2

Configure: Add test: { environment: 'jsdom' } to vite.config.js.
3

Write: Co-locate *.spec.js files next to components.
4

Run: npx vitest, watch mode by default.
5

Coverage: 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
Best practice: Test behavior, not implementation details. Assert on rendered output and emitted events rather than internal state. This keeps tests resilient to refactors.

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

1

Build: npm run build produces an optimized dist/ directory.
2

Preview: npm run preview serves the build locally to verify.
3

Upload: Copy dist/ to your host (Netlify, Vercel, S3, GitHub Pages, or your own server).
4

Configure SPA fallback: All unknown routes must serve index.html so client-side routing works on refresh.
5

Set headers: Add long-lived 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;
}
Cache caveat: Never cache 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/*.

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.

1

Scaffold: Run npm create vue@latest and select TypeScript, Router, Pinia, and Vitest.
2

Build a feature: Create a component that fetches data from a public API (e.g., JSONPlaceholder) and renders a list.
3

Add a store: Move the fetched data into a Pinia store and consume it from two different components.
4

Add routes: Create a detail page with a dynamic /post/:id route and a navigation guard.
5

Write tests: Cover the store actions and at least one component with Vitest.
6

Deploy: Push to a Git repo and connect it to Vercel or Netlify for automatic builds.

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
Key takeaway: Vue’s progressive philosophy means you can start small and scale up. Begin with a single component, add Pinia when state grows complex, introduce Vue Router for multi-page navigation, and layer in testing as your confidence grows. The ecosystem is designed so each piece is optional until you need it.

Dev Station works with teams across the United States and the United Kingdom. Application data is held to SOC 2 or HIPAA where a US client requires it, and to GDPR with ISO 27001 for UK and EU records. Our engineers work from Vietnam with overlap into US Eastern, US Pacific and UK GMT hours, and we invoice in USD or GBP.

Ask an AI about this

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 →

Related articles

Let's Talk