search
Vue star Featured

Fix: Vue Router not working after page refresh

Learn how to fix Vue Router issues that occur after page refresh. This comprehensive guide covers history mode, server configuration, and best practices.

person By Gautam Sharma
calendar_today January 2, 2026
schedule 13 min read
Vue.js Vue Router History Mode SPA Frontend Development Routing

The ‘Vue Router not working after page refresh’ is a common Single Page Application (SPA) issue that occurs when using Vue Router’s history mode. This problem typically manifests when users refresh the page or directly access deep links, resulting in 404 errors or the application not loading properly.

This comprehensive guide explains what causes this issue, why it happens, and provides multiple solutions to fix it in your Vue.js projects with clean code examples and directory structure.


What is the Vue Router Refresh Issue?

The “Vue Router not working after page refresh” issue occurs when:

  • Navigating to a route and refreshing the page results in a 404 error
  • Directly accessing deep links (e.g., /about, /user/123) fails
  • The server returns a 404 error instead of serving the Vue application
  • Routing works in-app but breaks on page refresh or direct navigation

Common Error Manifestations:

  • 404 Not Found errors when refreshing
  • Server returns HTML instead of Vue app
  • Route not found after refresh
  • Blank page after direct URL access
  • “Cannot GET /path” errors

Understanding the Problem

Vue Router’s history mode uses the browser’s History API to create clean URLs without hash fragments. However, this creates a problem: when users refresh the page or access routes directly, the server receives the request and tries to find a matching server-side route, which doesn’t exist since Vue handles routing client-side.

Typical Vue.js Project Structure:

my-vue-app/
├── package.json
├── vite.config.js
├── src/
│   ├── main.js
│   ├── App.vue
│   ├── router/
│   │   └── index.js
│   ├── views/
│   │   ├── Home.vue
│   │   ├── About.vue
│   │   └── User.vue
│   ├── components/
│   │   └── Navigation.vue
│   └── assets/
├── public/
└── dist/

Solution 1: Configure Vue Router History Mode Properly

Ensure Vue Router is properly configured with history mode.

❌ Without Proper History Mode:

// src/router/index.js - ❌ Hash mode or missing configuration
import { createRouter, createWebHashHistory } from 'vue-router'
import Home from '../views/Home.vue'

const routes = [
  { path: '/', component: Home },
  { path: '/about', component: About }
]

// ❌ Using hash mode (URLs will have #)
const router = createRouter({
  history: createWebHashHistory(),
  routes
})

export default router

✅ With Proper History Mode:

router/index.js:

// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import About from '../views/About.vue'
import User from '../views/User.vue'

const routes = [
  { path: '/', component: Home },
  { path: '/about', component: About },
  { path: '/user/:id', component: User, props: true },
  // ✅ Add catch-all route for 404 handling
  { path: '/:pathMatch(.*)*', name: 'NotFound', component: () => import('../views/NotFound.vue') }
]

// ✅ Use history mode for clean URLs
const router = createRouter({
  history: createWebHistory(),
  routes
})

export default router

Solution 2: Configure Server for History Mode

Configure your server to serve the Vue application for all routes.

❌ Without Server Configuration:

// ❌ Server doesn't handle client-side routing
// When accessing /about, server looks for /about endpoint
// Results in 404 error

✅ With Server Configuration:

For Express.js:

// server.js
const express = require('express')
const path = require('path')
const app = express()

// ✅ Serve static files
app.use(express.static(path.join(__dirname, 'dist')))

// ✅ Handle all routes by serving index.html
app.get('*', (req, res) => {
  res.sendFile(path.join(__dirname, 'dist/index.html'))
})

const PORT = process.env.PORT || 3000
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`)
})

For Nginx:

# nginx.conf
server {
  listen 80;
  server_name example.com;
  root /path/to/vue/app/dist;
  index index.html;

  # ✅ Handle Vue Router history mode
  location / {
    try_files $uri $uri/ /index.html;
  }
}

For Apache (.htaccess):

# .htaccess
<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteBase /
  
  # ✅ Redirect all requests to index.html
  RewriteRule ^index\.html$ - [L]
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule . /index.html [L]
</IfModule>

Solution 3: Implement Client-Side 404 Handling

Add proper 404 handling in your Vue application.

views/NotFound.vue:

<template>
  <div class="not-found">
    <h1>404 - Page Not Found</h1>
    <p>The page you're looking for doesn't exist.</p>
    <router-link to="/">Go Home</router-link>
  </div>
</template>

<script>
export default {
  name: 'NotFound'
}
</script>

<style scoped>
.not-found {
  text-align: center;
  padding: 40px;
}
</style>

Updated router/index.js:

// src/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'), props: true },
  // ✅ Catch-all route for 404 handling
  { 
    path: '/:pathMatch(.*)*', 
    name: 'NotFound', 
    component: () => import('../views/NotFound.vue'),
    meta: { title: 'Page Not Found' }
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes,
  // ✅ Scroll behavior for better UX
  scrollBehavior(to, from, savedPosition) {
    if (savedPosition) {
      return savedPosition
    } else {
      return { top: 0 }
    }
  }
})

// ✅ Global navigation guard for additional protection
router.beforeEach((to, from, next) => {
  // Add any route protection logic here
  next()
})

export default router

Solution 4: Configure Build for Production

Ensure your build process creates the correct structure for history mode.

vite.config.js:

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  build: {
    // ✅ Ensure proper base path for production
    outDir: 'dist',
    assetsDir: 'assets',
    rollupOptions: {
      output: {
        // ✅ Configure for SPA deployment
        entryFileNames: `assets/[name].[hash].js`,
        chunkFileNames: `assets/[name].[hash].js`,
        assetFileNames: `assets/[name].[hash].[ext]`
      }
    }
  },
  // ✅ Base path configuration
  base: process.env.NODE_ENV === 'production' ? '/' : '/'
})

Solution 5: Use Development Server Proxy

Configure the development server to handle history mode properly.

vite.config.js (Development):

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  server: {
    // ✅ Configure development server for history mode
    historyApiFallback: {
      // ✅ Handle all routes by serving index.html
      index: '/index.html',
      // ✅ Handle specific API routes if needed
      rewrites: [
        { from: /^\/api\/.*$/, to: function(context) {
          return context.parsedUrl.pathname
        }}
      ]
    },
    port: 3000,
    open: true
  }
})

Solution 6: Implement Route Guards for Protection

Add route guards to handle authentication and other checks properly.

router/index.js:

import { createRouter, createWebHistory } from 'vue-router'

const routes = [
  { 
    path: '/', 
    name: 'Home', 
    component: () => import('../views/Home.vue'),
    meta: { requiresAuth: false }
  },
  { 
    path: '/about', 
    name: 'About', 
    component: () => import('../views/About.vue'),
    meta: { requiresAuth: false }
  },
  { 
    path: '/dashboard', 
    name: 'Dashboard', 
    component: () => import('../views/Dashboard.vue'),
    meta: { requiresAuth: true }
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

// ✅ Global navigation guard
router.beforeEach((to, from, next) => {
  // ✅ Check authentication if required
  if (to.meta.requiresAuth) {
    const isAuthenticated = checkAuth() // Your auth logic
    if (isAuthenticated) {
      next()
    } else {
      next('/login')
    }
  } else {
    next()
  }
})

// ✅ Helper function for auth check
function checkAuth() {
  // Implement your authentication logic
  return !!localStorage.getItem('token')
}

export default router

Working Code Examples

Complete Router Configuration:

// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import('../views/Home.vue'),
    meta: { title: 'Home' }
  },
  {
    path: '/about',
    name: 'About',
    component: () => import('../views/About.vue'),
    meta: { title: 'About Us' }
  },
  {
    path: '/user/:id',
    name: 'User',
    component: () => import('../views/User.vue'),
    props: true,
    meta: { title: 'User Profile' }
  },
  {
    path: '/products',
    name: 'Products',
    component: () => import('../views/Products.vue'),
    meta: { title: 'Products' }
  },
  {
    path: '/products/:category',
    name: 'ProductCategory',
    component: () => import('../views/ProductCategory.vue'),
    props: true,
    meta: { title: 'Product Category' }
  },
  {
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: () => import('../views/NotFound.vue'),
    meta: { title: 'Page Not Found' }
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes,
  scrollBehavior(to, from, savedPosition) {
    if (savedPosition) {
      return savedPosition
    } else {
      return { top: 0 }
    }
  }
})

// ✅ Update page title based on route
router.beforeEach((to, from, next) => {
  document.title = to.meta.title || 'My Vue App'
  next()
})

export default router

Main Application Component:

<!-- src/App.vue -->
<template>
  <div id="app">
    <nav class="navigation">
      <router-link to="/">Home</router-link>
      <router-link to="/about">About</router-link>
      <router-link to="/products">Products</router-link>
    </nav>
    
    <main class="main-content">
      <router-view />
    </main>
  </div>
</template>

<script>
export default {
  name: 'App'
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}

.navigation {
  margin-bottom: 20px;
}

.navigation a {
  margin: 0 10px;
  text-decoration: none;
  color: #42b983;
}

.navigation a.router-link-active {
  font-weight: bold;
  color: #42b983;
}

.main-content {
  padding: 20px;
}
</style>

Example View Component:

<!-- src/views/User.vue -->
<template>
  <div class="user-profile">
    <h1>User Profile</h1>
    <p>User ID: {{ id }}</p>
    <div v-if="user">
      <h2>{{ user.name }}</h2>
      <p>{{ user.email }}</p>
    </div>
    <div v-else-if="loading">Loading...</div>
    <div v-else>No user found</div>
  </div>
</template>

<script>
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'

export default {
  name: 'User',
  props: {
    id: {
      type: String,
      required: true
    }
  },
  setup(props) {
    const user = ref(null)
    const loading = ref(false)

    onMounted(async () => {
      loading.value = true
      try {
        // Simulate API call
        user.value = await fetchUser(props.id)
      } catch (error) {
        console.error('Error fetching user:', error)
      } finally {
        loading.value = false
      }
    })

    const fetchUser = async (id) => {
      // Simulate API call
      return new Promise(resolve => {
        setTimeout(() => {
          resolve({ id, name: `User ${id}`, email: `user${id}@example.com` })
        }, 500)
      })
    }

    return {
      user,
      loading
    }
  }
}
</script>

<style scoped>
.user-profile {
  padding: 20px;
}
</style>

Best Practices for Vue Router

1. Always Include Catch-All Route

// ✅ Always include a catch-all route for 404 handling
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFoundComponent }

2. Configure Server Properly

// ✅ Ensure server serves index.html for all routes in history mode
app.get('*', (req, res) => {
  res.sendFile(path.join(__dirname, 'dist/index.html'))
})

3. Use Dynamic Imports for Lazy Loading

// ✅ Use dynamic imports for code splitting
{ path: '/about', component: () => import('../views/About.vue') }

4. Implement Proper Error Handling

// ✅ Handle async operations properly
router.beforeEach(async (to, from, next) => {
  try {
    await someAsyncOperation()
    next()
  } catch (error) {
    next('/error')
  }
})

Debugging Steps

Step 1: Check Router Configuration

# Verify router is configured with history mode
grep -r "createWebHistory" src/

Step 2: Test Development Server

# Test in development with proper proxy configuration
npm run dev
# Try navigating to different routes and refreshing

Step 3: Test Production Build

# Build and serve production version
npm run build
npx serve dist
# Test route refresh functionality

Step 4: Check Server Configuration

# Verify server serves index.html for all routes
# Test with curl or browser navigation to non-existent routes

Common Mistakes to Avoid

1. Forgetting Catch-All Route

// ❌ Missing catch-all route
const routes = [
  { path: '/', component: Home },
  { path: '/about', component: About }
  // ❌ No catch-all route - will cause 404 on refresh
]

// ✅ Include catch-all route
const routes = [
  { path: '/', component: Home },
  { path: '/about', component: About },
  { path: '/:pathMatch(.*)*', component: NotFound }
]

2. Incorrect Server Configuration

// ❌ Server doesn't handle SPA routing
app.get('/about', (req, res) => {
  res.send('About page') // ❌ This won't work with history mode
})

// ✅ Server handles all routes
app.get('*', (req, res) => {
  res.sendFile(path.join(__dirname, 'dist/index.html'))
})

3. Using Hash Mode Unnecessarily

// ❌ Using hash mode when history mode is preferred
const router = createRouter({
  history: createWebHashHistory(), // ❌ Creates URLs with #
  routes
})

// ✅ Use history mode for clean URLs
const router = createRouter({
  history: createWebHistory(), // ✅ Creates clean URLs
  routes
})

Performance Considerations

1. Optimize Route Loading

// ✅ Use lazy loading for better performance
const routes = [
  { path: '/dashboard', component: () => import('../views/Dashboard.vue') }
]

2. Implement Code Splitting

// ✅ Group related components for better caching
const routes = [
  {
    path: '/admin',
    component: () => import('../views/Admin.vue'),
    children: [
      { path: 'users', component: () => import('../views/admin/Users.vue') },
      { path: 'settings', component: () => import('../views/admin/Settings.vue') }
    ]
  }
]

Security Considerations

1. Validate Route Parameters

// ✅ Validate route parameters
router.beforeEach((to, from, next) => {
  if (to.params.id && !isValidId(to.params.id)) {
    next('/not-found')
  } else {
    next()
  }
})

2. Protect Sensitive Routes

// ✅ Implement proper route protection
router.beforeEach((to, from, next) => {
  if (to.meta.requiresAuth && !isAuthenticated()) {
    next('/login')
  } else {
    next()
  }
})

Testing Vue Router

1. Unit Test Route Configuration

import { createRouter, createWebHistory } from 'vue-router'
import { mount } from '@vue/test-utils'
import App from '@/App.vue'

describe('Vue Router', () => {
  it('should handle history mode properly', () => {
    const router = createRouter({
      history: createWebHistory(),
      routes: [
        { path: '/', component: { template: '<div>Home</div>' } }
      ]
    })

    const wrapper = mount(App, {
      global: {
        plugins: [router]
      }
    })

    expect(wrapper.exists()).toBe(true)
  })
})

2. Test Route Navigation

it('should navigate to different routes', async () => {
  await router.push('/about')
  expect(router.currentRoute.value.path).toBe('/about')
})

Alternative Solutions

1. Use Hash Mode (If History Mode Isn’t Required)

// For simple applications where clean URLs aren't critical
const router = createRouter({
  history: createWebHashHistory(), // URLs will have #
  routes
})

2. Server-Side Rendering (SSR)

// For SEO-critical applications
// Consider using Nuxt.js for SSR

Migration Checklist

  • Verify Vue Router is configured with history mode
  • Add catch-all route for 404 handling
  • Configure server to serve index.html for all routes
  • Test route refresh functionality in development
  • Test production build with server configuration
  • Implement proper error handling and route guards
  • Update documentation for team members
  • Verify all routes work after refresh

Conclusion

The ‘Vue Router not working after page refresh’ issue is a common SPA routing problem that occurs when using history mode without proper server configuration. By following the solutions provided in this guide—whether through proper router configuration, server setup, catch-all routes, or development server proxying—you can ensure your Vue.js applications handle routing correctly both in-app and after page refresh.

The key is to understand that Vue Router’s history mode requires server-side configuration to serve the Vue application for all routes, as the server needs to handle client-side routing. With proper configuration, your Vue.js applications will work seamlessly with clean URLs and proper navigation, providing an excellent user experience.

Remember to always include catch-all routes, configure your server properly for SPA deployment, and test thoroughly to ensure all routes work correctly after refresh and direct navigation.

Gautam Sharma

About Gautam Sharma

Full-stack developer and tech blogger sharing coding tutorials and best practices

Related Articles

Vue

Fix: Cannot read property '$refs' of undefined in Vue.js

Learn how to fix the 'Cannot read property $refs of undefined' error in Vue.js applications. This comprehensive guide covers Vue instance access, lifecycle hooks, and best practices.

January 2, 2026
Vue

Fix: defineProps is not defined in Vue.js Error

Learn how to fix the 'defineProps is not defined' error in Vue.js applications. This comprehensive guide covers Composition API, TypeScript, and best practices.

January 2, 2026
Vue

Fix: Property or method is not defined on the instance in Vue.js

Learn how to fix the 'Property or method is not defined on the instance' error in Vue.js applications. This comprehensive guide covers data properties, methods, and best practices.

January 2, 2026