This episode dissects Remix routing: mapping route files to URLs, nested routing with Outlet for layout sharing, dynamic routes and catch-all routes, advanced route conventions, and navigation with Link and NavLink.

In episode 3 you already have a running project. Now we get into one of Remix's strongest pillars: routing. In other frameworks, routing is often a long list of configuration. In Remix, a route is a file — and the rules can be learned in a few minutes.
What sets Remix apart from most frameworks is nested routes. The URL /blog/halo-dunia doesn't just point to a single page, but a hierarchy: the blog layout wraps the post detail page. This structure means persistent UI parts are never re-rendered during navigation.
Episode 4 takes you from the simplest route to advanced patterns: dynamic routes, catch-all routes, index routes, and smooth navigation via Link and NavLink.
Every file in the app/routes folder is mapped directly to a URL. The file name determines the path:
app/routes/_index.tsx -> / index route
app/routes/blog.tsx -> /blog
app/routes/contact.tsx -> /contactThis rule is consistent and predictable. Each route file exports a default component that renders when the URL matches. There's no extra configuration step beyond creating the file.
A single route file contains several exports: a default component for the UI, loader and action for data, meta for the head, headers for the response, and ErrorBoundary. This is what's called a route module — one file, one URL, one complete lifecycle. You saw a glimpse of it in episode 2.
Nested routing works through the Outlet component. A parent route file exports a layout with <Outlet />, and child routes render inside it. For example: all pages under /blog use the same layout.
import { Outlet, Link } from "@remix-run/react";
export default function LayoutBlog() {
return (
<div>
<header>
<Link to="/blog">Blog List</Link>
</header>
<main>
<Outlet />
</main>
</div>
);
}The Outlet component renders the child route according to the URL hierarchy. When navigation happens, only the changing route parts re-render; the header and main are preserved.
Sometimes you want a layout without adding a URL segment. Remix uses the underscore for this: the folder app/routes/blog._layout.tsx creates a layout for routes under /blog without changing the URL. The underscore pattern is how Remix distinguishes conventions from the actual URL structure.
To capture a value from the URL, name the file with a dollar sign. The file posts.$id.tsx matches /posts/anything, and the value is available through params in the loader and useParams in the component.
import { useLoaderData, useParams } from "@remix-run/react";
export async function loader({ params }) {
return { id: params.id };
}
export default function DetailPost() {
const params = useParams();
const data = useLoaderData();
return <h1>Posting {params.id} -> data {data.id}</h1>;
}The loader receives params from the URL segments that start with a dollar sign. For deeper paths, you can use the posts.$category.$slug.tsx form for /posts/tekno/mengenal-remix.
The file $.tsx matches all URLs below its level — including multiple segments at once. This is useful for catching unknown routes or building a virtual file system. The full value is available as params["*"].
A file named _index.tsx inside a folder becomes the default page for that segment. For example: posts._index.tsx renders when the URL is /posts without extra segments, while posts.$id.tsx handles /posts/123. The index route is paired with its parent route, not with itself as the parent.
A route that doesn't export a default component is called a resource route — the full response is controlled in code. Examples include JSON endpoints, file downloads, or webhooks. This is how Remix exposes APIs without leaving the filesystem convention, and it will be used again in episode 13.
Use Link from @remix-run/react for navigation between application pages. Link uses Remix's internal flow so transitions are smoother and prefetch can be enabled. Don't use a plain a tag for internal navigation — use a only for external links.
import { NavLink } from "@remix-run/react";
export default function Nav() {
return (
<NavLink to="/blog" className={({ isActive }) =>
isActive ? "menu-aktif" : "menu-biasa"}>
Blog
</NavLink>
);
}NavLink automatically marks the active link via the isActive prop — perfect for navigation menus. For imperative navigation, there's useNavigate and navigate(route) inside handlers.
When internal navigation happens, Remix loads the new route's loaders, then renders with a transition. Because each route has its own loader, the data needed by the next page is already available before that page is displayed. It's this combination of nested routes and prefetch that makes Remix navigation feel instant.
Episode 4 makes you fluent at reading and writing routes: route files become URLs, nested routing with Outlet, dynamic and catch-all routes, index and resource route conventions, and navigation with Link and NavLink. A well-structured route map is now in your hands.
The key takeaways:
_index.tsx is the default page; $.tsx is the catch-all.In the next episode, episode 5, we'll discuss data loading and actions — how loaders fetch data on the server, form submission with actions, sending JSON responses, redirects, cookies, and deferred data with streaming. Routing and data are two sides of the same coin in Remix.