Dynamic Routes
Dynamic Routes
Level 2 — Directory Structure & Routing A file-naming convention in the
pages/directory that uses square brackets (e.g.,[id].vue) to capture variable segments of a URL.
1. Prerequisites
pages/Directory — Where these dynamic files are created.useRoute&useRouterHooks — The composables used to access parameter values.
2. Term Category
Routing / Navigation (Parameterized & Catch-All Routes): Dynamic Routes generate URL parameter matching ([id].vue, [...slug].vue) from filename conventions in the pages/ directory.
3. Explanation
Environment Context
- Server & Client
(1) Design Motivation — "Why did we design this?"
If you are building an e-commerce store, you don't create a separate physical file for every single product (e.g., iphone.vue, macbook.vue, airpods.vue). That would require millions of files. Instead, you create a single "Product Template" file that can dynamically render any product based on the URL (e.g., /products/iphone, /products/macbook).
Nuxt needs a way to know that a specific part of the file path is a variable, not a literal string.
(2) Core Concept
In Nuxt, if you wrap a file or folder name in square brackets [], it becomes a Dynamic Route. The string inside the brackets becomes the name of the parameter you can access in your code.
Structure:
pages/
└── products/
└── [id].vue # Matches /products/1, /products/apple, etc.
(3) Accessing the Parameter
Inside [id].vue, you can access the exact value the user typed in the URL by using the auto-imported useRoute() composable.
<!-- pages/products/[id].vue -->
<script setup lang="ts">
const route = useRoute();
// If the URL is /products/apple, route.params.id will be 'apple'
const productId = route.params.id;
// Fetch data dynamically based on the URL parameter
const { data: product } = await useFetch(`/api/products/${productId}`);
</script>
<template>
<div v-if="product">
<h1>{{ product.name }}</h1>
</div>
</template>
(4) Catch-All Routes
If you want a route to match everything that comes after it (e.g., /docs/getting-started/installation/step-1), you use a catch-all route by putting ... inside the brackets: [...slug].vue. This will capture the entire path as an array.
4. Common Mistakes & Pitfalls
Mistake 1: Relying on route params for secure operations without validation
The mistake: Directly passing route.params.id to a database query without validating what the user actually typed.
Why it's wrong: The user can type anything into the URL bar. If they type /products/DROP-TABLE, your code will crash or execute a malicious query if you aren't careful.
Golden Rule: Always validate dynamic route parameters before using them, especially in database queries or API requests. You can even validate routes directly in Nuxt using definePageMeta.
Example Validation:
<script setup lang="ts">
definePageMeta({
validate: async (route) => {
// Only allow numbers for the ID
return /^\d+$/.test(route.params.id as string);
}
})
</script>
Mistake 2: Using Single Brackets [id] When Catch-All Brackets [...slug] Are Required
The mistake: Naming route folder pages/docs/[slug].vue and expecting it to match multi-segment paths /docs/api/v1.
Why it's wrong: Standard dynamic segments [slug].vue match ONLY a single path segment (/docs/api). Use catch-all [...slug].vue for multi-segment paths.
Incorrect:
// pages/docs/[slug].vue ❌ Fails to match multi-segment /docs/a/b/c!
Fix:
// pages/docs/[...slug].vue Matches /docs/a/b/c as array ['a', 'b', 'c']
Mistake 3: Accessing Un-Ref'd route.params.id inside Reactive Watchers
The mistake: Destructuring const { id } = useRoute().params at top level and expecting it to update on same-page route navigation.
Why it's wrong: Destructuring route.params severs Vue reactivity. When navigating between /users/1 and /users/2, destructured id will retain the old value '1'. Use route.params.id directly or toRefs(useRoute()).
Incorrect:
<script setup>
const { id } = useRoute().params; // ❌ Destructuring severs reactivity on route changes!
</script>
Fix:
<script setup>
const route = useRoute();
const userId = computed(() => route.params.id); // Reactive computed property updates on navigation
</script>
5. Practice Exercises
Exercise 1: Extracting Named Route Parameters from Filenames
Scenario:
Create a dynamic route pages/posts/[slug].vue and render parameter slug using useRoute().
Requirements:
- Create
pages/posts/[slug].vue. - Extract
route.params.slug.
Answer
Implementation
<!-- pages/posts/[slug].vue -->
<script setup lang="ts">
const route = useRoute();
const slug = computed(() => route.params.slug as string);
</script>
<template>
<article>
<h1>Article Slug: {{ slug }}</h1>
</article>
</template>
Technical Explanation
- Bracket filename notation
[slug].vuegenerates a named dynamic URL route parameterslug. - Matches paths like
/posts/hello-worldor/posts/nuxt-3-guide. useRoute().params.slugaccesses current path parameter values reactively.
Exercise 2: Implementing Catch-All Routes for CMS Pages
Scenario:
Create a catch-all route pages/[...slug].vue to render dynamic multi-segment CMS pages (/docs/guide/getting-started).
Requirements:
- Create
pages/[...slug].vue. - Join
route.params.slugarray.
Answer
Implementation
<!-- pages/[...slug].vue -->
<script setup lang="ts">
const route = useRoute();
// route.params.slug is an array of path segments e.g. ['docs', 'guide', 'getting-started']
const fullPath = computed(() => {
const segments = route.params.slug;
return Array.isArray(segments) ? segments.join("/") : segments;
});
</script>
<template>
<main>
<h1>CMS Page Path: /{{ fullPath }}</h1>
</main>
</template>
Technical Explanation
- Ellipsis syntax
[...slug].vuegenerates a catch-all route matching single or multi-segment URL paths. route.params.slugparses path segments as a string array (['docs', 'guide']).- Ideal for dynamic documentation portals and CMS content hierarchies.
Exercise 3: Optional Catch-All Routes for Default Fallbacks
Scenario:
Create an optional catch-all route pages/[[...slug]].vue matching / as well as nested sub-paths.
Requirements:
- Code
[[...slug]].vueoptional catch-all structure.
Answer
Implementation
<!-- pages/[[...slug]].vue -->
<script setup lang="ts">
const route = useRoute();
const isHome = computed(() => !route.params.slug || route.params.slug.length === 0);
</script>
<template>
<div>
<h1 v-if="isHome">Root Homepage Content</h1>
<h1 v-else>Nested Path: {{ route.params.slug }}</h1>
</div>
</template>
Technical Explanation
- Double bracket ellipsis
[[...slug]].vuemakes the catch-all parameter optional. - Matches
/(whereslugis undefined) and/any/nested/path. - Flexible route structure for single-file CMS routing.
6. Related Terms
useFetch— The standard way to fetch data based on the dynamic route parameter.- File-based Routing — Related concept: File-based Routing.
pages/Directory — Related concept:pages/Directory.useRoute&useRouterHooks — Related concept:useRoute&useRouterHooks.
7. Key Takeaways
- Wrap filenames or foldernames in
[]to make them dynamic (e.g.,[id].vue). - Access the captured value using
useRoute().params.id. - Use
[...slug].vueto catch multi-segment URLs. - Always validate route parameters to prevent unexpected behavior.