Toolmingo
Guides17 min read

Vue.js Developer Roadmap 2025 (Step-by-Step Guide)

The complete Vue developer roadmap for 2025 — from JavaScript fundamentals through Vue 3, Pinia, Nuxt.js, testing, and landing your first Vue job.

Vue.js powers millions of websites — from small startups to enterprise giants like Alibaba, Xiaomi, and GitLab. It is consistently ranked as the most-loved JavaScript framework in developer surveys thanks to its gentle learning curve and powerful progressive architecture. This roadmap shows exactly what to learn, in what order, with realistic timelines for 2025.

At a glance

Phase Topics Timeline
1 JavaScript & web foundations Weeks 1–4
2 Vue 3 core: templates & reactivity Weeks 5–8
3 Component architecture Weeks 9–11
4 Vue Router & navigation Weeks 12–13
5 Pinia state management Weeks 14–15
6 API integration & async patterns Weeks 16–17
7 TypeScript with Vue 3 Weeks 18–20
8 Testing (Vitest + Vue Test Utils) Weeks 21–23
9 Nuxt.js & SSR Weeks 24–27
10 DevOps & deployment Weeks 28–29
11 Portfolio & job search Weeks 30–32

Phase 1 — JavaScript & web foundations

Vue is a JavaScript framework, so solid JS knowledge is non-negotiable before touching .vue files.

Core JavaScript to master first:

// ES6+ features you'll use daily in Vue
const user = { name: "Ana", age: 28 }
const { name, age } = user          // destructuring
const updated = { ...user, age: 29 } // spread (immutability pattern)

// Arrow functions and this context
const double = (n) => n * 2

// Array methods
const active = users.filter(u => u.active)
const names  = users.map(u => u.name)
const total  = items.reduce((sum, i) => sum + i.price, 0)

// Promises and async/await
async function fetchUser(id) {
  const res = await fetch(`/api/users/${id}`)
  if (!res.ok) throw new Error("Not found")
  return res.json()
}

// Optional chaining and nullish coalescing
const city = user?.address?.city ?? "Unknown"

Topics checklist before Vue:

Topic Why it matters
const/let, arrow functions Vue SFC syntax relies on them
Destructuring & spread Used constantly in defineProps, emits
Array methods (map/filter/reduce) Reactive list manipulation
Promises / async-await Every API call in Vue
ES modules (import/export) How Vue components import each other
this and closures Options API uses this heavily
DOM basics Understanding what Vue compiles to

Resources: MDN Web Docs, javascript.info (free), "You Don't Know JS" (free on GitHub).


Phase 2 — Vue 3 core: templates & reactivity

Start with the Composition API — it is the modern standard for Vue 3.

Single File Components (.vue)

Every Vue file is split into three blocks:

<script setup>
// logic (Composition API)
import { ref, computed } from 'vue'

const count = ref(0)
const doubled = computed(() => count.value * 2)

function increment() {
  count.value++
}
</script>

<template>
  <!-- HTML + Vue directives -->
  <button @click="increment">
    Count: {{ count }} (doubled: {{ doubled }})
  </button>
</template>

<style scoped>
/* CSS scoped to this component */
button { padding: 8px 16px; }
</style>

Reactivity: ref vs reactive

ref reactive
Use for Primitives & any value Objects & arrays
Access in script .value required Direct property access
Template access Auto-unwrapped (no .value) Direct
Destructuring Safe (keeps reactivity) Breaks reactivity — use toRefs
// ref — best default choice
const count = ref(0)
const user  = ref({ name: "Ana" })
count.value++
user.value.name = "Mia"

// reactive — use for complex objects you won't reassign
const state = reactive({ count: 0, name: "Ana" })
state.count++

// toRefs preserves reactivity when destructuring reactive objects
const { count: cnt, name } = toRefs(state)

Template directives cheat sheet

Directive Purpose Example
v-bind / : Bind prop/attr :href="url"
v-on / @ Listen to event @click="handler"
v-model Two-way binding v-model="email"
v-if / v-else-if / v-else Conditional render v-if="isLoggedIn"
v-show Toggle visibility (CSS) v-show="isOpen"
v-for List rendering v-for="item in items" :key="item.id"
v-once Render once, skip updates v-once
v-pre Skip compilation v-pre

v-if vs v-show:

  • v-if removes/inserts the DOM element — better when condition rarely changes.
  • v-show toggles display:none — better for frequent toggling (keeps DOM alive).

Computed properties and watchers

const firstName = ref("Ana")
const lastName  = ref("Marković")

// computed — cached, re-runs only when deps change
const fullName = computed(() => `${firstName.value} ${lastName.value}`)

// watch — run side effects when data changes
watch(firstName, (newVal, oldVal) => {
  console.log(`Changed from ${oldVal} to ${newVal}`)
})

// watchEffect — auto-tracks every reactive dep used inside
watchEffect(() => {
  document.title = `Hello, ${firstName.value}`
})

Phase 3 — Component architecture

Props and emits

<!-- Child.vue -->
<script setup>
const props = defineProps({
  title: { type: String, required: true },
  count: { type: Number, default: 0 }
})

const emit = defineEmits(['update', 'delete'])

function handleClick() {
  emit('update', props.count + 1)
}
</script>

<template>
  <div>
    <h2>{{ title }}</h2>
    <button @click="handleClick">+1</button>
    <button @click="emit('delete')">Delete</button>
  </div>
</template>
<!-- Parent.vue -->
<script setup>
import Child from './Child.vue'
import { ref } from 'vue'
const myCount = ref(0)
</script>

<template>
  <Child
    title="My Counter"
    :count="myCount"
    @update="myCount = $event"
    @delete="myCount = 0"
  />
</template>

Slots

<!-- Card.vue — provides named slots -->
<template>
  <div class="card">
    <header><slot name="header" /></header>
    <main><slot /></main>              <!-- default slot -->
    <footer><slot name="footer" /></footer>
  </div>
</template>

<!-- Usage -->
<Card>
  <template #header><h2>Title</h2></template>
  <p>Body content</p>
  <template #footer><button>OK</button></template>
</Card>

Composables (reusable logic)

Composables are the Vue 3 equivalent of React hooks — extract stateful logic into reusable functions:

// composables/useFetch.js
import { ref } from 'vue'

export function useFetch(url) {
  const data    = ref(null)
  const loading = ref(true)
  const error   = ref(null)

  fetch(url)
    .then(r => r.json())
    .then(d => { data.value = d })
    .catch(e => { error.value = e })
    .finally(() => { loading.value = false })

  return { data, loading, error }
}

// Component usage
const { data: users, loading } = useFetch('/api/users')

Component lifecycle

Hook When it runs
onBeforeMount Before first render
onMounted After DOM is ready — fetch data here
onBeforeUpdate Before reactive update re-renders
onUpdated After re-render
onBeforeUnmount Before component removal — clean up timers
onUnmounted After removal
import { onMounted, onUnmounted } from 'vue'

onMounted(() => {
  window.addEventListener('resize', handleResize)
})

onUnmounted(() => {
  window.removeEventListener('resize', handleResize) // always clean up!
})

Phase 4 — Vue Router

Vue Router is the official client-side routing solution.

npm install vue-router
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home    from '../views/Home.vue'
import About   from '../views/About.vue'
import Profile from '../views/Profile.vue'

const routes = [
  { path: '/',           component: Home },
  { path: '/about',      component: About },
  { path: '/user/:id',   component: Profile },  // dynamic segment
  { path: '/:pathMatch(.*)*', component: NotFound }, // 404
]

export default createRouter({
  history: createWebHistory(),
  routes,
})
<!-- Navigation -->
<RouterLink to="/">Home</RouterLink>
<RouterLink :to="{ name: 'profile', params: { id: 42 } }">Profile</RouterLink>

<!-- Route view renders matched component -->
<RouterView />

Navigation guards

// Route-level guard — protect authenticated pages
const routes = [
  {
    path: '/dashboard',
    component: Dashboard,
    meta: { requiresAuth: true }
  }
]

router.beforeEach((to, from) => {
  if (to.meta.requiresAuth && !isAuthenticated()) {
    return { path: '/login', query: { redirect: to.fullPath } }
  }
})

Programmatic navigation

import { useRouter, useRoute } from 'vue-router'

const router = useRouter()
const route  = useRoute()

// Read params
const userId = route.params.id
const search = route.query.q

// Navigate
router.push('/dashboard')
router.push({ name: 'profile', params: { id: 5 } })
router.replace('/login')
router.back()

Phase 5 — Pinia (state management)

Pinia is the official Vue state management library — it replaced Vuex in 2023.

npm install pinia
// stores/useAuthStore.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useAuthStore = defineStore('auth', () => {
  // State
  const user  = ref(null)
  const token = ref(localStorage.getItem('token'))

  // Getters (computed)
  const isLoggedIn = computed(() => !!token.value)
  const userName   = computed(() => user.value?.name ?? 'Guest')

  // Actions
  async function login(email, password) {
    const res = await fetch('/api/login', {
      method: 'POST',
      body: JSON.stringify({ email, password }),
      headers: { 'Content-Type': 'application/json' }
    })
    const data = await res.json()
    user.value  = data.user
    token.value = data.token
    localStorage.setItem('token', data.token)
  }

  function logout() {
    user.value  = null
    token.value = null
    localStorage.removeItem('token')
  }

  return { user, token, isLoggedIn, userName, login, logout }
})
<!-- Using the store in a component -->
<script setup>
import { useAuthStore } from '@/stores/useAuthStore'
const auth = useAuthStore()
</script>

<template>
  <div v-if="auth.isLoggedIn">
    Hello, {{ auth.userName }}
    <button @click="auth.logout">Logout</button>
  </div>
  <LoginForm v-else @submit="auth.login" />
</template>

Pinia vs Vuex vs local state

Local ref/reactive Pinia Vuex
Scope Single component Global / shared Global / shared
Boilerplate None Minimal High (mutations, actions, getters)
TypeScript Native Excellent Moderate
DevTools Partial Full Full
Vue 3 recommended ✅ for local ✅ for shared ❌ legacy (Vue 2 era)

Rule of thumb: use ref/reactive inside a single component; reach for Pinia only when two or more components need to share the same data.


Phase 6 — API integration & async patterns

useFetch composable with error handling

// composables/useApi.js
import { ref } from 'vue'

export function useApi() {
  const loading = ref(false)
  const error   = ref(null)

  async function request(url, options = {}) {
    loading.value = true
    error.value   = null
    try {
      const res = await fetch(url, {
        headers: { 'Content-Type': 'application/json' },
        ...options,
      })
      if (!res.ok) throw new Error(`HTTP ${res.status}`)
      return await res.json()
    } catch (e) {
      error.value = e.message
      throw e
    } finally {
      loading.value = false
    }
  }

  return { loading, error, request }
}

Async components & Suspense

<script setup>
import { defineAsyncComponent } from 'vue'

// Load HeavyChart only when needed
const HeavyChart = defineAsyncComponent(() =>
  import('./HeavyChart.vue')
)
</script>

<template>
  <!-- Suspense shows fallback while async component loads -->
  <Suspense>
    <HeavyChart :data="chartData" />
    <template #fallback>
      <div>Loading chart…</div>
    </template>
  </Suspense>
</template>

Provide / Inject (avoid prop drilling)

// Parent (e.g., App.vue)
import { provide, ref } from 'vue'
const theme = ref('dark')
provide('theme', theme)  // provide to all descendants

// Deep Child
import { inject } from 'vue'
const theme = inject('theme', 'light') // second arg = default

Phase 7 — TypeScript with Vue 3

Vue 3 was written in TypeScript and has first-class TS support.

# Create a TypeScript Vue project
npm create vue@latest my-app
# ✔ Add TypeScript? Yes
<script setup lang="ts">
interface User {
  id: number
  name: string
  email: string
}

// Typed props
const props = defineProps<{
  user: User
  loading?: boolean
}>()

// Typed emits
const emit = defineEmits<{
  update: [user: User]
  delete: [id: number]
}>()

// Typed ref
const count = ref<number>(0)
const users = ref<User[]>([])

// Typed computed
const activeUsers = computed<User[]>(() =>
  users.value.filter(u => u.id > 0)
)

// withDefaults for props with defaults
const propsWithDefaults = withDefaults(
  defineProps<{ count?: number; label?: string }>(),
  { count: 0, label: 'Click me' }
)
</script>

Phase 8 — Testing

Testing pyramid for Vue

Level Tool What to test
Unit Vitest Composables, stores, utilities
Component Vue Test Utils + Vitest Component render, events, props
E2E Playwright or Cypress Full user flows
npm install -D vitest @vue/test-utils jsdom @vitejs/plugin-vue
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  test: { environment: 'jsdom' }
})

Component testing

// tests/Counter.test.js
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import Counter from '@/components/Counter.vue'

describe('Counter', () => {
  it('renders initial count', () => {
    const wrapper = mount(Counter, { props: { initialCount: 5 } })
    expect(wrapper.text()).toContain('5')
  })

  it('increments on button click', async () => {
    const wrapper = mount(Counter)
    await wrapper.find('button').trigger('click')
    expect(wrapper.text()).toContain('1')
  })

  it('emits update event', async () => {
    const wrapper = mount(Counter)
    await wrapper.find('button').trigger('click')
    expect(wrapper.emitted('update')?.[0]).toEqual([1])
  })
})

Testing Pinia stores

import { setActivePinia, createPinia } from 'pinia'
import { useAuthStore } from '@/stores/useAuthStore'

beforeEach(() => {
  setActivePinia(createPinia())
})

it('login sets user and token', async () => {
  const store = useAuthStore()
  global.fetch = vi.fn().mockResolvedValue({
    ok: true,
    json: () => ({ user: { name: 'Ana' }, token: 'abc123' })
  })

  await store.login('ana@example.com', 'pass')
  expect(store.isLoggedIn).toBe(true)
  expect(store.userName).toBe('Ana')
})

Phase 9 — Nuxt.js

Nuxt.js is the full-stack meta-framework for Vue — it adds SSR, SSG, file-based routing, and auto-imports.

npx nuxi@latest init my-nuxt-app
cd my-nuxt-app && npm install

File-based routing

pages/
  index.vue          →  /
  about.vue          →  /about
  blog/
    index.vue        →  /blog
    [slug].vue       →  /blog/:slug
  user/
    [id]/
      profile.vue    →  /user/:id/profile

Rendering modes

Mode How Best for
SSR (Server-Side Rendering) Page generated on request Dynamic, personalised pages
SSG (Static Site Generation) Pre-rendered at build time Blogs, docs, marketing
SPA Client-only (no SSR) Admin dashboards
ISR (Incremental Static Regen) Revalidate cached pages News, e-commerce
<!-- pages/blog/[slug].vue -->
<script setup>
// useFetch is auto-imported in Nuxt
const { data: post } = await useFetch(`/api/posts/${useRoute().params.slug}`)
</script>

<template>
  <article v-if="post">
    <h1>{{ post.title }}</h1>
    <div v-html="post.content" />
  </article>
</template>

Nuxt auto-imports

Nuxt auto-imports Vue APIs, composables, and your own files — no manual imports needed:

<script setup>
// These are available WITHOUT any import statement in Nuxt:
const count = ref(0)           // Vue ref
const route = useRoute()       // Vue Router
const config = useRuntimeConfig()  // Nuxt runtime config

// Your own composables/useFoo.js are also auto-imported
const { data } = useFetch('/api/items')
</script>

Phase 10 — DevOps & deployment

Docker

# Multi-stage Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80

Deployment platforms

Platform Vue SPA Nuxt SSR Free tier Best for
Vercel Full-stack Nuxt
Netlify JAMstack / SSG
Cloudflare Pages Global edge
Railway Limited Node SSR servers
AWS S3 + CloudFront ✅ SPA only Pay-as-you-go Enterprise

GitHub Actions CI/CD

# .github/workflows/deploy.yml
name: Deploy Vue App

on:
  push:
    branches: [main]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - run: npm ci
      - run: npm run test
      - run: npm run build
      - uses: amondnet/vercel-action@v25
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.ORG_ID }}
          vercel-project-id: ${{ secrets.PROJECT_ID }}
          vercel-args: '--prod'

Full Vue technology map

Language
  JavaScript (ES2020+)
  TypeScript ──────────────── type safety

UI Layer
  Vue 3 SFC ─── <script setup> Composition API
    Template directives (v-if, v-for, v-model…)
    Reactivity (ref, reactive, computed, watch)
    Composables (reusable stateful logic)

Routing
  Vue Router ──────────────── client-side routing
    Dynamic routes, nested routes
    Navigation guards (auth, roles)

State Management
  Pinia ───────────────────── global state
  Local ref/reactive ──────── component-level state

API Layer
  Fetch / Axios
  TanStack Query (Vue Query) ─ server state cache
  Ofetch (Nuxt built-in)

Styling
  Tailwind CSS ────────────── utility-first
  CSS Modules ─────────────── scoped per component
  SCSS / PostCSS

Testing
  Vitest ──────────────────── unit + component
  Vue Test Utils ──────────── component mounting
  Playwright ──────────────── E2E

Build Tools
  Vite ────────────────────── dev server + bundler
  Vue CLI (legacy, Webpack)

Meta-framework
  Nuxt.js ─────────────────── SSR / SSG / full-stack
    Nitro server engine
    Auto-imports
    Modules ecosystem

Package Management
  pnpm (recommended) / npm / yarn

DevOps
  Docker → Vercel / Netlify / Cloudflare Pages
  GitHub Actions CI/CD

Realistic timeline (32 weeks)

Weeks Milestone What you can build
1–4 JS fundamentals solid Simple DOM projects
5–8 Vue 3 basics + reactivity Counter, to-do list, simple forms
9–11 Component architecture Reusable component library
12–13 Vue Router Multi-page SPA
14–15 Pinia SPA with shared state (shopping cart, auth)
16–17 API integration CRUD app with REST backend
18–20 TypeScript Refactor project to TS
21–23 Testing 80%+ test coverage on existing project
24–27 Nuxt.js Blog / portfolio with SSR/SSG
28–29 DevOps Dockerized, deployed to Vercel with CI/CD
30–32 Portfolio polish + job search 3 polished projects on GitHub

Portfolio project ideas

Project Vue concepts practiced Difficulty
Task Manager v-for, v-model, Pinia, Vue Router ⭐⭐
Movie Search App API calls, loading states, composables ⭐⭐
E-commerce Store Cart state (Pinia), filters, checkout flow ⭐⭐⭐
Real-time Chat WebSockets, reactive updates, auth ⭐⭐⭐
Blog with CMS Nuxt.js, SSG, Markdown, SEO meta tags ⭐⭐⭐
Dashboard with Charts Charts.js/ApexCharts, dynamic data, filters ⭐⭐⭐⭐

Vue developer roles and salary

Role Level US salary EU salary
Junior Vue Developer 0–2 yr $60–80k €30–45k
Mid-level Vue Developer 2–5 yr $85–115k €50–75k
Senior Vue Developer 5+ yr $120–160k €75–110k
Vue / Nuxt Full-Stack 3+ yr $100–140k €65–95k
Front-End Lead (Vue) 5+ yr $130–170k €85–120k
Vue Consultant / Freelance 3+ yr $80–150/hr €60–120/hr

Common mistakes table

Mistake Why it hurts Fix
Using Vuex instead of Pinia in new projects More boilerplate, worse TypeScript Start with Pinia
Forgetting :key on v-for Broken list updates, subtle bugs Always use :key="item.id"
Mutating props directly Breaks unidirectional data flow Emit events to parent
Putting all state in Pinia Over-engineered, harder to test Local state for single-component data
Missing onUnmounted cleanup Memory leaks (event listeners, timers) Always pair onMounted with cleanup
Using reactive and destructuring Loses reactivity silently Use ref or toRefs
Fetching data outside onMounted/composables SSR hydration mismatches in Nuxt Use useFetch/useAsyncData in Nuxt
Skipping TypeScript Harder to maintain as project grows Add lang="ts" from day one

Vue vs React vs Angular comparison

Vue 3 React 18 Angular 17
Learning curve Gentle Moderate Steep
Syntax style HTML-first (templates) JS-first (JSX) TypeScript-first
State management Pinia (official) Zustand / Redux (community) NgRx / Signals (built-in)
Full-stack meta-framework Nuxt.js Next.js Analog.js
TypeScript support Excellent Excellent Mandatory
Bundle size (hello world) ~13kB ~45kB ~75kB
Corporate adoption Alibaba, GitLab, Xiaomi Meta, Airbnb, Netflix Google, Microsoft, enterprise
Job market (2025) Strong (especially Asia/EU) Strongest Strong (enterprise)
Best for Progressive enhancement, teams new to frameworks Complex SPAs, large ecosystems Enterprise, opinionated structure

6 frequently asked questions

Q: Should I learn Vue 2 or Vue 3?
A: Vue 3 only. Vue 2 reached end-of-life in December 2023 and no longer receives security patches. The Composition API and <script setup> are the current standard.

Q: Pinia or Vuex?
A: Pinia for all new projects. The Vue core team officially recommends Pinia as Vuex's successor. Pinia has less boilerplate, better TypeScript support, and first-class DevTools integration.

Q: Vue or React — which should I learn first?
A: Vue has a gentler learning curve and cleaner template syntax, making it a better first framework for most beginners. That said, React has the largest job market in North America. If you are already comfortable with one, the other takes 1–2 weeks to pick up.

Q: Do I need Nuxt.js to build a production Vue app?
A: Not always. For SPAs (admin dashboards, internal tools), plain Vue + Vite is sufficient. Add Nuxt when you need SSR for SEO, SSG for a static blog, or a full-stack API layer. Start without Nuxt and add it when you hit its use cases.

Q: Is Vue still popular in 2025?
A: Yes. Vue is consistently in the top 3 JavaScript frameworks by usage, particularly dominant in China and Europe. It is the framework of choice at GitLab, Alibaba, Xiaomi, and hundreds of mid-size companies that value its approachability.

Q: What's the difference between Options API and Composition API?
A: Options API (the Vue 2 style) organises code by option type (data, methods, computed). Composition API (Vue 3 standard) organises code by feature using functions — this makes composables possible and keeps related code together. New projects should use Composition API with <script setup>.

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools