| |

Vue.js 55 🟢 Client-Side Routing with Vue Router (createRouter, createWebHistory, createWebHashHistory)

⏱️ Estimated read: 10 min | |

Vue Router is the official routing library for Vue, and its central function—createRouter()—is where every client-side navigation begins. The function accepts a configuration object with a routes array and a history option, and returns a router instance that you install on the Vue app . The history option is the most consequential decision in that configuration: it determines whether your URLs look like normal web addresses or carry a hash fragment, and it determines whether your server needs special configuration to serve the application.

Vue Router 4 introduced a breaking change from Vue Router 3: the old mode: 'history' string was replaced by a more flexible history option that accepts a history object created by createWebHistory(), createWebHashHistory(), or createMemoryHistory() . This change exists for tree-shaking—unused history implementations are excluded from the bundle—and for advanced use cases where a custom history implementation is needed . The base path is now passed as the first argument to the history factory rather than as a separate router option.

This chapter covers the three history modes, their URL behavior, their server requirements, and the practical consequences for SEO and deployment.

Key point: createWebHistory() produces clean URLs without a hash but requires server-side fallback configuration to avoid 404s on direct access; createWebHashHistory() produces URLs with a # that never reach the server, requiring zero server configuration but harming SEO; createMemoryHistory() is for SSR and Node environments and does not interact with the browser URL at all.


Why the history option matters

The URL appearance problem. With createWebHistory(), the URL looks normal: https://example.com/user/id. With createWebHashHistory(), it carries a hash: https://example.com/#/user/id. The hash is never sent to the server, which is why hash mode needs no server configuration. But the hash is also the reason hash mode does poorly on SEO—search engines may not index the fragment as a distinct page .

The server configuration problem. Because a Vue SPA is a single index.html file, a direct request to https://example.com/user/id hits the server asking for a file that does not exist. Without a fallback, the server returns 404. The fix is to configure the server to serve index.html for any path that does not match a static asset . This is the hidden cost of HTML5 mode: clean URLs, but server configuration required.

The SSR and Node problem. In server-side rendering, there is no browser URL to read. The createMemoryHistory() mode exists for this: it stores navigation state in memory, does not interact with window, and requires the application to push the initial navigation manually after installing the router . Using createWebHistory() on the server would crash because window is not defined.

The migration problem. Vue Router 3 used mode: 'history'. Vue Router 4 removed this in favor of the history option. Developers migrating from Vue 2 must change mode: 'history' to history: createWebHistory(), and mode: 'hash' to history: createWebHashHistory() . The base option also moved from the router options to the first argument of the history factory.


a. createRouter(): the router instance

createRouter() is the entry point. It takes an object with at least routes and history, and returns a Router instance that you install with app.use(router).

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

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', component: Home },
    { path: '/about', component: About }
  ]
})

export default router

The routes option is required in Vue Router 4. You can add routes later with router.addRoute(), but the router is designed to be created with an initial set of routes . Each route is an object with at least a path and a component, and optionally a name, redirect, children, and other properties.

The history option is required. It must be a history implementation created by one of the factory functions. Passing a string like 'history' or 'hash' will not work—that was the Vue Router 3 API .

Once created, the router is installed on the app:

const app = createApp(App)
app.use(router)
app.mount('#app')

The router then handles navigation, and the <router-view> component renders the matched route’s component.


b. createWebHistory(): clean URLs with server requirements

createWebHistory() is the recommended mode for most web applications. It uses the HTML5 History API (pushState, replaceState) to change the URL without reloading the page . The URL looks normal—no hash, no fragment—which is better for users, for sharing links, and for SEO.

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

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

The cost is server configuration. Because the SPA serves a single index.html, a direct request to a nested path like /user/id will fail unless the server is configured to fall back to index.html. The Vue Router documentation provides configurations for Apache, Nginx, native Node.js, Express, IIS, Caddy, Firebase, Netlify, and Vercel . The Nginx configuration is a single line:

location / {
  try_files $uri $uri/ /index.html;
}

The Express approach uses the connect-history-api-fallback middleware. The Netlify approach is a _redirects file with /* /index.html 200.

A subtlety: with the fallback in place, the server no longer reports 404 errors for non-existent paths, because every path serves index.html. To show a proper 404 page, you need a catch-all route in the Vue app: { path: '/:pathMatch(.*)', component: NotFound } .

If you deploy to a subfolder, pass the base path as the first argument: createWebHistory('/base-directory/') .


c. createWebHashHistory(): zero-configuration with SEO cost

createWebHashHistory() uses the URL hash to simulate navigation. The hash (#) and everything after it is never sent to the server, so the server only ever sees a request for index.html. This means zero server configuration is required—you can deploy to a static file host or even open the file locally and it will work.

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

const router = createRouter({
  history: createWebHashHistory(),
  routes: [...]
})

The URL looks like https://example.com/#/user/id. The hash is part of the client-side URL, and the browser’s hashchange event drives navigation.

The trade-off is SEO. Search engines may not treat the hash fragment as a distinct page, which means hash-mode SPAs are poorly indexed compared to history-mode SPAs. The Vue Router documentation is explicit: hash mode “does however have a bad impact in SEO. If that’s a concern for you, use the HTML5 history mode” .

Hash mode is appropriate for internal tools, prototypes, static hosting where server configuration is not possible, and environments where deep linking to clean URLs is not a requirement.


Complete Example Session

The following session demonstrates creating a router in both modes and the observable differences.

// ============================================
// PART 1: HTML5 MODE SETUP
// ============================================
import { createRouter, createWebHistory } from 'vue-router'
import Home from './views/Home.vue'
import About from './views/About.vue'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', component: Home },
    { path: '/about', component: About }
  ]
})

// Navigating to /about produces:
// https://example.com/about
// Server must serve index.html for this path

// ============================================
// PART 2: HASH MODE SETUP
// ============================================
import { createRouter, createWebHashHistory } from 'vue-router'

const hashRouter = createRouter({
  history: createWebHashHistory(),
  routes: [
    { path: '/', component: Home },
    { path: '/about', component: About }
  ]
})

// Navigating to /about produces:
// https://example.com/#/about
// Server only ever sees a request for index.html

// ============================================
// PART 3: BASE PATH FOR SUBFOLDER DEPLOYMENT
// ============================================
const subfolderRouter = createRouter({
  history: createWebHistory('/app/'),
  routes: [...]
})
// URLs become https://example.com/app/about

// ============================================
// PART 4: SSR WITH MEMORY HISTORY
// ============================================
import { createMemoryHistory } from 'vue-router'

const ssrRouter = createRouter({
  history: createMemoryHistory(),
  routes: [...]
})
// No window access; requires manual router.push(url) on server

// ============================================
// PART 5: CATCH-ALL 404 ROUTE
// ============================================
const routerWith404 = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', component: Home },
    { path: '/:pathMatch(.*)', component: NotFound }
  ]
})
// Required when using history mode, because server
// no longer returns 404 for unknown paths

// ============================================
// PART 6: Nginx Configuration for History Mode
// ============================================
// server {
//   listen 80;
//   location / {
//     try_files $uri $uri/ /index.html;
//   }
// }

The six parts cover HTML5 mode, hash mode, base path configuration, memory history for SSR, the catch-all 404 route, and the Nginx fallback configuration. What the session demonstrates is that the history mode determines both the URL shape and the deployment requirements—there is no mode that gives clean URLs without server configuration.


Quick Reference

History Modes

ModeFactoryURL ExampleServer ConfigSEO
HTML5createWebHistory()/user/idRequiredGood
HashcreateWebHashHistory()/#/user/idNonePoor
MemorycreateMemoryHistory()N/A (no URL)N/AN/A

Server Fallback Configurations

ServerConfiguration
Nginxtry_files $uri $uri/ /index.html;
ApacheFallbackResource /index.html
Expressconnect-history-api-fallback middleware
Netlify/* /index.html 200 in _redirects
Vercel{"source": "/(.*)", "destination": "/index.html"}

Vue Router 3 to 4 Migration

Vue Router 3Vue Router 4
mode: 'history'history: createWebHistory()
mode: 'hash'history: createWebHashHistory()
mode: 'abstract'history: createMemoryHistory()
base: '/app/'createWebHistory('/app/')

Required Options

OptionRequiredPurpose
routesYesRoute definitions
historyYesHistory implementation

Best Practices

✅ Do This:

createRouter({ history: createWebHistory(), routes })      // ✅ HTML5 mode
createWebHistory('/base/')                                 // ✅ base as argument
{ path: '/:pathMatch(.*)', component: NotFound }           // ✅ catch-all 404
try_files $uri $uri/ /index.html;                          // ✅ Nginx fallback

❌ Don’t Do This:

createRouter({ mode: 'history', routes })                  // ❌ Vue Router 3 API
createRouter({ history: 'history', routes })               // ❌ string not accepted
// no server fallback with createWebHistory                // ❌ 404 on refresh
// createWebHistory() in SSR                               // ❌ window undefined

Common Pitfalls

PitfallWhy It HappensFix
404 on page refreshNo server fallbackConfigure try_files or middleware
mode: 'history' ignoredVue Router 4 API changedUse history: createWebHistory()
Hash mode not indexedSEO crawlers ignore fragmentSwitch to HTML5 mode
Base path wrongDeployed to subfolderPass base to createWebHistory()
SSR crashwindow not definedUse createMemoryHistory()

Real-World Examples

1. HTML5 mode for production SPA

createRouter({ history: createWebHistory(), routes })

2. Hash mode for static hosting

createRouter({ history: createWebHashHistory(), routes })

3. Subfolder deployment

createWebHistory('/my-app/')

4. Nginx fallback

location / { try_files $uri $uri/ /index.html; }

5. Netlify redirect

/* /index.html 200

6. Express middleware

app.use(history())

7. SSR memory history

const history = isServer ? createMemoryHistory() : createWebHistory()

8. Catch-all 404

{ path: '/:pathMatch(.*)', component: NotFound }

9. Vercel config

[{ "source": "/(.*)", "destination": "/index.html" }]

10. Firebase hosting

{ "rewrites": [{ "source": "**", "destination": "/index.html" }] }

Visual

URL Appearance by Mode

┌──────────────────────────────────────────────┐
│  HTML5 MODE (createWebHistory)               │
│  https://example.com/user/id                 │
│  Normal URL, server must handle fallback     │
│                                              │
│  HASH MODE (createWebHashHistory)            │
│  https://example.com/#/user/id               │
│  Hash never reaches server, no config needed │
│                                              │
│  MEMORY MODE (createMemoryHistory)           │
│  No URL interaction                          │
│  For SSR and Node environments               │
└──────────────────────────────────────────────┘

Server Request Flow

┌──────────────────────────────────────────────┐
│  HTML5 MODE                                  │
│  User requests /user/id                      │
│         │                                    │
│         ▼                                    │
│  Server checks for file at /user/id          │
│         │                                    │
│    ┌────┴────┐                               │
│    │         │                               │
│  exists   not exists                         │
│    │         │                               │
│    ▼         ▼                               │
│  serve    serve index.html (fallback)        │
│  file     │                                  │
│           ▼                                  │
│     Vue Router handles /user/id              │
│                                              │
│  HASH MODE                                   │
│  User requests /#/user/id                    │
│         │                                    │
│         ▼                                    │
│  Server only sees /                          │
│         │                                    │
│         ▼                                    │
│  Serves index.html                           │
│         │                                    │
│         ▼                                    │
│  Browser parses #/user/id                    │
│  Vue Router handles it                       │
└──────────────────────────────────────────────┘

History Mode Decision

┌──────────────────────────────────────────────┐
│  Can you configure the server?               │
│  ├─ YES → Need SEO?                          │
│  │        ├─ YES → createWebHistory()        │
│  │        └─ NO → Either (preference)        │
│  └─ NO → createWebHashHistory()              │
│                                              │
│  Is this SSR?                                │
│  └─ YES → createMemoryHistory()              │
└──────────────────────────────────────────────┘

Vue Router 3 vs 4

┌──────────────────────────────────────────────┐
│  VUE ROUTER 3                                │
│  createRouter({ mode: 'history', routes })   │
│         │                                    │
│         ▼                                    │
│  VUE ROUTER 4                                │
│  createRouter({                              │
│    history: createWebHistory(),              │
│    routes                                    │
│  })                                          │
│                                              │
│  mode string → history factory               │
│  base option → factory argument              │
└──────────────────────────────────────────────┘

Summary

ItemValue
Router factorycreateRouter()
Required optionsroutes, history
HTML5 historycreateWebHistory()
Hash historycreateWebHashHistory()
Memory historycreateMemoryHistory()
HTML5 server configRequired (fallback to index.html)
Hash server configNone
HTML5 SEOGood
Hash SEOPoor
Base pathFirst argument to history factory
Vue Router 3 migrationmode → history option

Key takeaways:

  • createRouter() requires history and routes. The history option replaced Vue Router 3’s mode string with a factory function .
  • createWebHistory() gives clean URLs but needs server fallback. Without configuration, direct access to nested paths returns 404 .
  • createWebHashHistory() needs zero server configuration. The hash never reaches the server, but SEO suffers .
  • createMemoryHistory() is for SSR and Node. It does not touch the browser URL and requires manual initial navigation .
  • The base path moved to the history factory. createWebHistory('/base/') replaces Vue Router 3’s base option .
  • History mode requires a catch-all 404 route. Because the server fallback serves index.html for every path, the app must handle unknown routes itself .
  • The choice is a trade-off. Clean URLs require server configuration; zero configuration costs SEO and URL aesthetics.

Remember: Vue Router’s history option determines both the shape of your URLs and the requirements of your deployment. createWebHistory() is the recommended mode for production applications—it produces normal URLs and works with SEO, at the cost of server configuration. createWebHashHistory() is the zero-configuration option for static hosting and prototypes, but the hash is invisible to search engines. createMemoryHistory() is the server-side mode, disconnected from the browser. The migration from Vue Router 3 is mechanical: replace mode with history and the history factory, and move the base path to the factory’s first argument.


Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!