Skip to content
All essays
WebMarch 27, 202614 min

Qwik City Routing and Layouts: Building Nested Applications

Master Qwik City routing system, nested layouts, route parameters, navigation, and building complex multi-page applications.

Ü
Ümit Uz
Mobile & Full Stack Developer

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 routes

Route Parameters

Handle dynamic route parameters.

Basic Route Parameters

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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

typescript
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

typescript
// 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

typescript
// 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>
  );
});

Implement navigation between routes.

typescript
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

typescript
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>;
});
typescript
export const navigateWithData = $(async (nav: any) => {
  await nav('/success', {
    state: { message: 'Operation completed!' },
  });
});

Prefetching

Optimize navigation with route prefetching.

typescript
<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

typescript
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

typescript
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

typescript
import { redirect } from '@builder.io/qwik-city';

export const useOldRoute = loader$(({ url }) => {
  if (url.pathname === '/old-route') {
    throw redirect(301, '/new-route');
  }
});

Conditional Redirects

typescript
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

typescript
// 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

typescript
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

typescript
// 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

typescript
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

typescript
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

typescript
// 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

typescript
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:

  1. 1File-based routing is intuitive and automatic
  2. 2Nested layouts enable consistent UI across routes
  3. 3Route guards protect authenticated and authorized areas
  4. 4Prefetching optimizes navigation performance
  5. 5Type-safe route parameters
  6. 6Flexible error handling
  7. 7Built-in internationalization support

Master these routing patterns to build scalable, performant applications with Qwik City.

Enjoyed this essay? Share it.

Related essays

Next essay
Visual Testing: Catching UI Regressions