Appearance
Routing & Navigation ​
File-based routing with Nuxt 3 and type-safe navigation.
Route File Structure ​
Single Page Routes ​
If a route has no child routes, use a single file:
pages/
employees.vue # /employees (no children)
settings.vue # /settings (no children)Routes with Children ​
Only create a folder with index.vue when:
- The route has multiple child routes, OR
- The page has many
_componentsthat would clutter the parent folder
pages/
employees/ # Has children
index.vue # /employees
add.vue # /employees/add
[id].vue # /employees/123
settings/ # Has many components
index.vue # /settings
_components/ # Component folder
SettingsForm.vue
SettingsList.vueFile Structure Rule
- No children →
employees.vue - Has children OR many components →
employees/index.vue
See Folder Structure → for component organization patterns.
Standard Resource Routes ​
| Route | File | Purpose |
|---|---|---|
/employees | employees/index.vue | List all |
/employees/add | employees/add.vue | Create new |
/employees/123 | employees/[id].vue | View single |
/employees/123/edit | employees/[id]/edit.vue | Edit single |
Separate Routes for Add/Edit
Even though add and edit forms are similar, always use separate routes:
/employees/add- Create new/employees/123/edit- Edit existing
They can wrap the same component internally, but different routes allow:
- âś… Better URL semantics
- âś… Proper back button behavior
- âś… Bookmarkable states
- âś… Analytics tracking
Dynamic Routes ​
Use [param] for dynamic segments:
vue
<!-- pages/employees/[id]/index.vue -->
<script setup>
const route = useRoute()
const employeeId = route.params.id // "123"
</script>Accessing Route Params ​
vue
<script setup>
const route = useRoute()
// Single param
const id = route.params.id
// Multiple params
const { orgId, employeeId } = route.params
// Query params
const { status, department } = route.query
</script>navigateTo - Always Use Route Names ​
Always navigate using route names, not path strings:
typescript
// âś… Correct - using route name
const handleNext = () => {
return navigateTo({ name: 'employees' })
}
// âś… With params
return navigateTo({
name: 'employees-id',
params: { id: 123 }
})
// âś… With query
return navigateTo({
name: 'employees',
query: { status: 'active' }
})
// ❌ Wrong - using path strings
return navigateTo('/employees')
return navigateTo(`/employees/${id}`)Why Route Names?
The key benefit: Dynamic segments you don't specify stay the same from the current route.
Example: You're on /hr/123/employees and want to go to settings:
typescript
// âś… With route name - orgId stays 123 automatically
return navigateTo({ name: 'hr-orgId-settings' })
// Goes to: /hr/123/settings
// ❌ With path - you need to know and pass the current orgId
const route = useRoute()
return navigateTo(`/hr/${route.params.orgId}/settings`)You don't need to know or pass params that aren't changing!
Other benefits:
- âś… Refactoring - Changing URLs won't break navigation
- âś… Autocomplete - IDE suggests available routes
- âś… Consistency - Standard pattern across the app
Required Return/Await Pattern ​
Always return or await navigateTo - Nuxt requires this:
typescript
// âś… Correct - with return
const handleNext = () => {
return navigateTo({ name: 'employees' })
}
// âś… Correct - with await
const handleNext = async () => {
await navigateTo({ name: 'employees' })
console.log('Navigation complete')
}
// ❌ Wrong - missing return/await
const handleNext = () => {
navigateTo({ name: 'employees' }) // Missing return!
}Why This Matters
Nuxt uses the return value to manage navigation lifecycle. Without return/await, navigation can break or behave unexpectedly.
If a Function Returns navigateTo ​
If a helper function returns navigateTo, you must also return that call:
typescript
// Helper that returns navigateTo
const goToEmployee = (id: number) => {
return navigateTo({ name: 'employees-id', params: { id } })
}
// âś… Correct - return the result
const handleClick = () => {
return goToEmployee(123)
}
// ❌ Wrong - missing return
const handleClick = () => {
goToEmployee(123) // Missing return!
}Route Names ​
Nuxt generates route names from file paths:
| File Path | Route Name |
|---|---|
employees/index.vue | employees |
employees/add.vue | employees-add |
employees/[id].vue | employees-id |
employees/[id]/edit.vue | employees-id-edit |
Finding Route Names
Check your IDE autocomplete when typing name: or refer to the file structure to determine the route name.
Query Parameters ​
Always use object syntax with route names:
typescript
// âś… Good - object syntax with route name
return navigateTo({
name: 'employees',
query: {
status: 'active',
department: 'engineering'
}
})
// URL: /employees?status=active&department=engineering
// ❌ Avoid - path strings
return navigateTo(`/employees?status=${status}&department=${dept}`)Route Dialogs ​
Route dialogs are dialogs controlled by the URL - they have their own route.
Why Use Route Dialogs? ​
- âś… Shareable URLs - Send someone a direct link to the dialog
- âś… Browser back button - Naturally closes the dialog
- âś… Bookmarkable - Users can bookmark the dialog state
- âś… Parent stays visible - Background page remains rendered
Structure ​
pages/
employees/
index.vue # Parent page (list)
index/ # Child routes
index.vue # Default (can be empty/hidden)
add.vue # Dialog: /employees/add
[id]/
edit.vue # Dialog: /employees/123/edit
delete.vue # Dialog: /employees/123/deleteParent Page Setup ​
The parent must render <NuxtPage /> for child routes:
vue
<!-- pages/employees/index.vue -->
<script setup>
const { data: employees } = useOrgApi('/api/organizations/employees', {
lazy: true
})
</script>
<template>
<div>
<h1>Employees</h1>
<!-- Button to open dialog - use route name -->
<RouteDialogTrigger :to="{ name: 'employees-add' }">
Add Employee
</RouteDialogTrigger>
<!-- Employee list -->
<EmployeeTable :employees="employees" />
<!-- Render child routes (dialogs) -->
<NuxtPage />
</div>
</template>Dialog Route ​
vue
<!-- pages/employees/index/add.vue -->
<script setup>
const handleSubmit = async () => {
await $apiFetch('/api/organizations/employees', {
method: 'post',
body: formData.value
})
// Navigate back to parent (closes dialog) - use route name
return navigateTo({ name: 'employees' })
}
</script>
<template>
<RouteDialog :close-route="{ name: 'employees' }">
<UiDialogContent>
<UiDialogHeader>
<UiDialogTitle>Add Employee</UiDialogTitle>
</UiDialogHeader>
<form @submit.prevent="handleSubmit">
<!-- Form fields -->
<UiButton type="submit">Save</UiButton>
</form>
</UiDialogContent>
</RouteDialog>
</template>RouteDialogTrigger ​
Opens a dialog by navigating to its route:
vue
<template>
<!-- Default button - always use route name -->
<RouteDialogTrigger :to="{ name: 'employees-add' }">
Add Employee
</RouteDialogTrigger>
<!-- With button props -->
<RouteDialogTrigger
:to="{ name: 'employees-id-delete', params: { id: 123 } }"
variant="destructive"
size="sm"
>
Delete
</RouteDialogTrigger>
<!-- Custom element (as-child) -->
<RouteDialogTrigger
:to="{ name: 'employees-id-edit', params: { id: 123 } }"
as-child
>
<UiButton variant="outline">
<Icon name="lucide:edit" />
Edit
</UiButton>
</RouteDialogTrigger>
</template>Always Use Route Names
Never use path strings like to="/employees/add" - always use route name syntax:
vue
<!-- ❌ Wrong -->
<RouteDialogTrigger to="/employees/add">
<!-- âś… Correct -->
<RouteDialogTrigger :to="{ name: 'employees-add' }">Accessibility
RouteDialogTrigger automatically manages focus restoration - when the dialog closes, focus returns to the trigger button.
Default Child Route ​
To prevent empty state, add a default child:
vue
<!-- pages/employees/index/index.vue -->
<template>
<div class="hidden" />
</template>This ensures the parent page shows when no dialog is open.
Type-Safe Navigation ​
Routes use the nuxt-typed-router package:
typescript
// Use route names for navigation
navigateTo({ name: 'employees' })
navigateTo({ name: 'employees-id', params: { id: 123 } })
navigateTo({ name: 'employees-add' })Autocomplete
Your IDE may provide autocomplete for route names when you type name:
Common Patterns ​
Pattern 1: Navigate After Action ​
typescript
const handleDelete = async (id: number) => {
await $apiFetch(`/api/organizations/employees/${id}`, {
method: 'delete'
})
toast.success('Employee deleted')
// Navigate back to list using route name
return navigateTo({ name: 'employees' })
}Pattern 2: Navigate with Query ​
typescript
const handleFilter = (status: string) => {
return navigateTo({
name: 'employees',
query: { status }
})
}Pattern 3: Navigate with Params ​
typescript
const handleViewEmployee = (id: number) => {
return navigateTo({
name: 'employees-id',
params: { id }
})
}Pattern 4: Conditional Navigation ​
typescript
const handleNext = async () => {
if (!isValid.value) {
toast.error('Please fix errors')
return // Don't navigate
}
return navigateTo({ name: 'next-step' })
}Pattern 5: Replace History ​
typescript
// Replace current entry (don't add to history)
return navigateTo({ name: 'employees', replace: true })Best Practices ​
Do's âś…
- Always use route names, not path strings
- Always return/await
navigateTo - Use separate routes for add/edit
- Use route dialogs for overlay content
- Only create folder/index.vue when needed
Don'ts ❌
- Don't use path strings - use route names
- Don't forget to return
navigateTo - Don't reuse routes for different purposes
- Don't create unnecessary folder/index.vue structures
Related: Folder Structure → | Error Handling → | API Calls →