Skip to main content

File-Based Routing

Film Fanatic uses TanStack Router with file-based routing, providing:
  • Type-safe route parameters
  • Automatic route tree generation
  • Built-in data loading
  • SSR support
  • Viewport-based prefetching

Route Structure

src/routes/
├── __root.tsx                    # Root layout with providers
├── index.tsx                     # Homepage (/)
├── watchlist.tsx                 # Watchlist page (/watchlist)
├── search.tsx                    # Search page (/search)
├── disclaimer.tsx                # Disclaimer page (/disclaimer)
├── person.$id.tsx                # Person details (/person/:id)
├── keyword.$id.tsx               # Keyword page (/keyword/:id)
├── collection.$id.{-$slug}.tsx   # Collection page (/collection/:id/:slug)
├── list.$type.$slug.tsx          # List page (/list/:type/:slug)
├── movie/$id/{-$slug}/          # Movie routes
│   ├── index.tsx                # Movie details (/movie/:id/:slug)
│   ├── cast-crew.tsx            # Cast & crew (/movie/:id/:slug/cast-crew)
│   └── media.tsx                # Media (/movie/:id/:slug/media)
└── tv/$id/{-$slug}/             # TV show routes
    ├── index.tsx                # TV details (/tv/:id/:slug)
    ├── cast-crew.tsx            # Cast & crew (/tv/:id/:slug/cast-crew)
    ├── media.tsx                # Media (/tv/:id/:slug/media)
    ├── seasons.tsx              # All seasons (/tv/:id/:slug/seasons)
    └── season.$seasonNumber.tsx # Season details (/tv/:id/:slug/season/:number)

Root Route

The root route provides the shell for all pages:
import type { QueryClient } from "@tanstack/react-query";
import {
  createRootRouteWithContext,
  HeadContent,
  Scripts,
} from "@tanstack/react-router";
import { Footer } from "@/components/footer";
import { Navbar } from "@/components/navbar";
import { ThemeProvider } from "@/components/theme-provider";
import { UserSync } from "@/components/user-sync";
import { SITE_CONFIG } from "@/constants";
import { MetaImageTagsGenerator } from "@/lib/meta-image-tags";
import appCss from "@/styles.css?url";

interface RouterContext {
  queryClient: QueryClient;
}

export const Route = createRootRouteWithContext<RouterContext>()({
  head: () => ({
    meta: [
      { charSet: "utf-8" },
      { name: "viewport", content: "width=device-width, initial-scale=1" },
      ...MetaImageTagsGenerator({
        title: SITE_CONFIG.name,
        description: SITE_CONFIG.description,
        ogImage: SITE_CONFIG.defaultMetaImage,
        url: SITE_CONFIG.url,
      }),
      { name: "robots", content: "noindex, nofollow" },
      { property: "og:type", content: "website" },
      { name: "twitter:card", content: "summary_large_image" },
    ],
    links: [
      { rel: "stylesheet", href: appCss },
      { rel: "icon", href: "/logo.svg", sizes: "any", type: "image/x-icon" },
    ],
  }),
  shellComponent: RootDocument,
});

function RootDocument({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <head>
        <HeadContent />
      </head>
      <body className="min-h-screen leading-relaxed antialiased">
        <ThemeProvider>
          <UserSync />
          <Navbar />
          {children}
          <Footer />
          <Scripts />
        </ThemeProvider>
      </body>
    </html>
  );
}

Router Configuration

import { ClerkProvider, useAuth } from "@clerk/clerk-react";
import { createRouter } from "@tanstack/react-router";
import { setupRouterSsrQueryIntegration } from "@tanstack/react-router-ssr-query";
import { ConvexReactClient } from "convex/react";
import { ConvexProviderWithClerk } from "convex/react-clerk";
import { DefaultLoader } from "@/components/default-loader";
import { DefaultNotFoundComponent } from "@/components/default-not-found";
import * as TanstackQuery from "@/lib/query/root-provider";
import { routeTree } from "@/routeTree.gen";

const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL);

export const getRouter = () => {
  const rqContext = TanstackQuery.getContext();

  const router = createRouter({
    routeTree,
    context: { ...rqContext },
    defaultPreload: "viewport", // Prefetch routes when they enter viewport
    Wrap: (props: { children: React.ReactNode }) => {
      return (
        <ClerkProvider publishableKey={import.meta.env.VITE_CLERK_PUBLISHABLE_KEY}>
          <ConvexProviderWithClerk client={convex} useAuth={useAuth}>
            <TanstackQuery.Provider {...rqContext}>
              {props.children}
            </TanstackQuery.Provider>
          </ConvexProviderWithClerk>
        </ClerkProvider>
      );
    },
    scrollRestoration: true,
    caseSensitive: true,
    defaultStaleTime: 30 * 1000,
    defaultPendingComponent: () => <DefaultLoader />,
    defaultNotFoundComponent: () => <DefaultNotFoundComponent />,
    defaultErrorComponent: () => <DefaultNotFoundComponent />,
  });

  setupRouterSsrQueryIntegration({
    router,
    queryClient: rqContext.queryClient,
    handleRedirects: true,
    wrapQueryClient: true,
  });

  return router;
};

Page Routes

Homepage

import { createFileRoute } from "@tanstack/react-router";
import { Suspense } from "react";
import {
  PopularMovies,
  TrendingDayMovies,
  UpcomingMovies,
} from "@/components/homepage-media";
import { SearchBar, SearchBarSkeleton } from "@/components/ui/search-bar";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";

export const Route = createFileRoute("/")({
  component: HomePage,
});

function HomePage() {
  return (
    <section className="flex flex-col items-center justify-center">
      <div className="mx-auto max-w-screen-lg px-4 py-10 text-center">
        <h1 className="font-black text-2xl sm:text-5xl md:text-[4rem]">
          Welcome to<span className="px-2 text-blue-500">Film Fanatic</span>
        </h1>
        <p className="mb-3 text-xs text-accent-foreground">
          Millions of movies, TV shows, and people to discover.
        </p>
        <Suspense fallback={<SearchBarSkeleton />}>
          <SearchBar />
        </Suspense>
      </div>

      <div className="mx-auto flex w-full max-w-screen-xl px-5 py-5">
        <div className="flex w-full flex-col gap-10">
          <Tabs defaultValue="trending_day">
            <div className="flex items-center gap-5">
              <h2 className="font-medium text-xl md:text-2xl">Trending</h2>
              <TabsList>
                <TabsTrigger value="trending_day">Today</TabsTrigger>
                <TabsTrigger value="trending_week">This Week</TabsTrigger>
              </TabsList>
            </div>
            <TabsContent value="trending_day">
              <TrendingDayMovies />
            </TabsContent>
          </Tabs>

          <section>
            <h2 className="font-medium text-xl md:text-2xl">Upcoming Movies</h2>
            <UpcomingMovies />
          </section>
        </div>
      </div>
    </section>
  );
}
import { Link } from "@tanstack/react-router";

// Simple route
<Link to="/watchlist">My Watchlist</Link>

// Route with params (fully type-safe)
<Link
  to="/movie/$id/$slug"
  params={{ id: "123", slug: "inception" }}
>
  Inception
</Link>

// Route with search params
<Link
  to="/search"
  search={{ query: "matrix", page: 1 }}
>
  Search Results
</Link>

// Person route
<Link
  to="/person/$id"
  params={{ id: String(person.id) }}
>
  {person.name}
</Link>

Route Parameters

Dynamic Routes

  • $id - Required parameter (e.g., /movie/$id)
  • {-$slug} - Optional parameter (e.g., /movie/$id/{-$slug})
  • $type.$slug - Multiple parameters (e.g., /list/$type/$slug)

URL Structure Examples

/movie/550/fight-club              → movie details
/tv/1396/breaking-bad              → TV show details
/tv/1396/breaking-bad/seasons      → all seasons
/tv/1396/breaking-bad/season.1     → season 1
/person/287                        → person details
/collection/131295/the-matrix-collection → collection
/list/movies/popular               → movie list
/search?query=matrix               → search results

Route Features

Viewport Prefetching

Routes automatically prefetch when links enter the viewport:
const router = createRouter({
  defaultPreload: "viewport", // Prefetch on viewport entry
});

Scroll Restoration

Scroll position is automatically restored on navigation:
const router = createRouter({
  scrollRestoration: true,
});

Loading States

Default pending component shows during route transitions:
const router = createRouter({
  defaultPendingComponent: () => <DefaultLoader />,
});

Error Handling

Default error component handles route errors:
const router = createRouter({
  defaultErrorComponent: () => <DefaultNotFoundComponent />,
  defaultNotFoundComponent: () => <DefaultNotFoundComponent />,
});

Generated Route Tree

TanStack Router auto-generates type-safe route definitions:
// Auto-generated in src/routeTree.gen.ts
import { Route as rootRoute } from "./routes/__root";
import { Route as IndexRoute } from "./routes/index";
import { Route as MovieIdSlugRoute } from "./routes/movie/$id/{-$slug}/index";

const routeTree = rootRoute.addChildren([
  IndexRoute,
  MovieIdSlugRoute,
  // ... all other routes
]);

export { routeTree };
This provides full TypeScript autocomplete for all routes and parameters.

Build docs developers (and LLMs) love