Qwik City provides a sophisticated routing system that supports nested layouts, route parameters, and intuitive navigation patterns. This comprehensive guide covers everything you need to build complex multi-page applications with Qwik City.
Understanding Qwik City Routing
Qwik City uses a file-based routing system where routes are automatically generated based on the directory structure in src/routes.
File-Based Routing
src/routes/
├── index.tsx # /
├── about.tsx # /about
├── blog/
│ ├── index.tsx # /blog
│ ├── [slug]/index.tsx # /blog/:slug
│ └── archive.tsx # /blog/archive
└── dashboard/
├── index.tsx # /dashboard
├── settings.tsx # /dashboard/settings
└── layout.tsx # Layout for dashboard routesRoute Parameters
Handle dynamic route parameters.
Basic Route Parameters
// src/routes/products/[id]/index.tsx
import { loader$, component$ } from '@builder.io/qwik';
import { useLocation } from '@builder.io/qwik-city';
export const useProduct = loader$(async ({ params }) => {
const product = await db.product.findUnique({
where: { id: params.id },
});
return product;
});
export default component$(() => {
const product = useProduct();
const location = useLocation();
return (
<div>
<h1>Product: {product.value?.name}</h1>
<p>ID: {location.params.id}</p>
</div>
);
});Multiple Route Parameters
// src/routes/categories/[categoryId]/products/[productId]/index.tsx
export const useProduct = loader$(async ({ params }) => {
const { categoryId, productId } = params;
const product = await db.product.findFirst({
where: {
id: productId,
categoryId,
},
});
return product;
});Optional Route Parameters
// src/routes/[[lang]]/index.tsx
export const useLocalizer = loader$(async ({ params }) => {
const lang = params.lang || 'en';
const messages = await getMessages(lang);
return { lang, messages };
});Route Guards
Protect routes with authentication and authorization.
Authentication Guard
// src/routes/dashboard/layout.tsx
import { loader$, component$ } from '@builder.io/qwik';
import { useLocation } from '@builder.io/qwik-city';
export const useAuthGuard = loader$(async ({ cookie, fail, url }) => {
const sessionToken = cookie.get('session')?.value;
if (!sessionToken) {
const redirectUrl = new URL('/login', url.origin);
redirectUrl.searchParams.set('redirect', url.pathname);
throw redirect(302, redirectUrl.toString());
}
const session = await db.session.findUnique({
where: { token: sessionToken },
});
if (!session || session.expiresAt < new Date()) {
throw fail(401, 'Unauthorized');
}
return session.user;
});
export default component$(() => {
useAuthGuard();
return (
<div>
<Slot />
</div>
);
});Role-Based Access Control
export const useAdminGuard = loader$(async ({ cookie, fail }) => {
const sessionToken = cookie.get('session')?.value;
const session = await db.session.findUnique({
where: { token: sessionToken },
include: { user: true },
});
if (!session || session.user.role !== 'admin') {
throw fail(403, 'Forbidden');
}
return session.user;
});Layouts
Create nested layouts for consistent UI across routes.
Route Layouts
// src/routes/dashboard/layout.tsx
import { component$, Slot } from '@builder.io/qwik';
import { useLocation } from '@builder.io/qwik-city';
export default component$(() => {
const location = useLocation();
return (
<div class="dashboard-layout">
<nav>
<a href="/dashboard">Overview</a>
<a href="/dashboard/settings">Settings</a>
<a href="/dashboard/analytics">Analytics</a>
</nav>
<main>
<Slot />
</main>
<footer>
© 2026 Dashboard - {location.pathname}
</footer>
</div>
);
});Nested Layouts
// src/routes/dashboard/admin/layout.tsx
export default component$(() => {
return (
<div class="admin-layout">
<aside>
<a href="/dashboard/admin/users">Users</a>
<a href="/dashboard/admin/settings">Settings</a>
</aside>
<div class="content">
<Slot />
</div>
</div>
);
});Navigation
Implement navigation between routes.
Link Component
import { Link } from '@builder.io/qwik-city';
export default component$(() => {
return (
<nav>
<Link href="/">Home</Link>
<Link href="/about">About</Link>
<Link href="/contact">Contact</Link>
</nav>
);
});Programmatic Navigation
import { useNavigate } from '@builder.io/qwik-city';
export default component$(() => {
const nav = useNavigate();
const handleClick = $(async () => {
await nav('/dashboard');
});
return <button onClick$={handleClick}>Go to Dashboard</button>;
});Navigation with State
export const navigateWithData = $(async (nav: any) => {
await nav('/success', {
state: { message: 'Operation completed!' },
});
});Prefetching
Optimize navigation with route prefetching.
Link Prefetching
<Link href="/dashboard" prefetch="true">
Dashboard
</Link>
// Prefetch on hover
<Link href="/dashboard" prefetch="hover">
Dashboard
</Link>
// Prefetch on viewport
<Link href="/dashboard" prefetch="viewport">
Dashboard
</Link>Manual Prefetching
import { usePrefetch } from '@builder.io/qwik-city';
export default component$(() => {
const prefetch = usePrefetch();
const prefetchDashboard = $(() => {
prefetch('/dashboard');
});
return (
<button onMouseEnter$={prefetchDashboard}>Dashboard</button>
);
});Route Metadata
Manage metadata for SEO and social sharing.
Page Metadata
import { routeLoader$ } from '@builder.io/qwik-city';
export const useMetadata = routeLoader$(({ url, params }) => {
return {
title: 'Product - My Store',
meta: [
{
name: 'description',
content: 'View our amazing products',
},
{
property: 'og:title',
content: 'Product Page',
},
{
property: 'og:description',
content: 'View our amazing products',
},
{
property: 'og:image',
content: '/images/product-og.jpg',
},
{
name: 'twitter:card',
content: 'summary_large_image',
},
],
};
});
export default component$(() => {
useMetadata();
return <div>Product Page</div>;
});Redirects
Implement route redirects.
Permanent Redirects
import { redirect } from '@builder.io/qwik-city';
export const useOldRoute = loader$(({ url }) => {
if (url.pathname === '/old-route') {
throw redirect(301, '/new-route');
}
});Conditional Redirects
export const useRedirectBasedOnRole = loader$(async ({ cookie }) => {
const sessionToken = cookie.get('session')?.value;
const session = await db.session.findUnique({
where: { token: sessionToken },
include: { user: true },
});
if (session?.user.role === 'admin') {
throw redirect(302, '/admin/dashboard');
} else if (session?.user.role === 'user') {
throw redirect(302, '/user/dashboard');
}
});Error Pages
Create custom error pages.
404 Page
// src/routes/[...404]/index.tsx
export default component$(() => {
return (
<div class="error-page">
<h1>404 - Page Not Found</h1>
<p>The page you're looking for doesn't exist.</p>
<Link href="/">Go Home</Link>
</div>
);
});Error Boundary
import { component$, ErrorBoundary } from '@builder.io/qwik';
export default component$(() => {
return (
<ErrorBoundary
fallback={(error) => (
<div class="error">
<h2>Something went wrong</h2>
<p>{error.message}</p>
</div>
)}
>
<ChildComponent />
</ErrorBoundary>
);
});Route Groups
Organize routes without affecting URL structure.
Route Groups
src/routes/
├── (marketing)/
│ ├── index.tsx # /
│ ├── about.tsx # /about
│ └── features.tsx # /features
├── (app)/
│ ├── dashboard/
│ │ └── index.tsx # /dashboard
│ └── settings/
│ └── index.tsx # /settings
└── index.tsx # /Internationalization
Build internationalized applications with routing.
Locale-Based Routing
// src/routes/[locale]/index.tsx
export const useLocale = loader$(({ params, cookie }) => {
const locale = params.locale || 'en';
const supportedLocales = ['en', 'es', 'fr'];
if (!supportedLocales.includes(locale)) {
throw redirect(302, '/en');
}
cookie.set('locale', locale, { path: '/' });
return locale;
});
export default component$(() => {
const locale = useLocale();
const t = useTranslations(locale);
return (
<div>
<h1>{t('welcome')}</h1>
</div>
);
});Best Practices
1. Use Type-Safe Route Parameters
interface RouteParams {
id: string;
category: string;
}
export const useProduct = loader$(async ({ params }: { params: RouteParams }) => {
const product = await db.product.findUnique({
where: { id: params.id },
});
return product;
});2. Implement Proper Error Handling
export const useProduct = loader$(async ({ params, fail }) => {
try {
const product = await db.product.findUnique({
where: { id: params.id },
});
if (!product) {
throw fail(404, 'Product not found');
}
return product;
} catch (error) {
throw fail(500, 'Internal server error');
}
});3. Optimize Route Splitting
// Lazy load heavy components
import { component$, lazy } from '@builder.io/qwik';
const HeavyComponent = lazy$(async () =>
import('~/components/HeavyComponent')
);
export default component$(() => {
return (
<div>
<HeavyComponent />
</div>
);
});4. Use Semantic HTML
export default component$(() => {
return (
<nav aria-label="Main navigation">
<ul>
<li><Link href="/">Home</Link></li>
<li><Link href="/about">About</Link></li>
<li><Link href="/contact">Contact</Link></li>
</ul>
</nav>
);
});Conclusion
Qwik City's routing system provides a powerful, flexible way to build complex multi-page applications with excellent performance. The combination of file-based routing, nested layouts, route parameters, and intelligent prefetching makes it easy to create sophisticated applications that load instantly.
Key takeaways:
- 1File-based routing is intuitive and automatic
- 2Nested layouts enable consistent UI across routes
- 3Route guards protect authenticated and authorized areas
- 4Prefetching optimizes navigation performance
- 5Type-safe route parameters
- 6Flexible error handling
- 7Built-in internationalization support
Master these routing patterns to build scalable, performant applications with Qwik City.