This episode dissects Remix's architecture: filesystem-based routing with nested routes, loader and action as server-side data handlers, the server rendering versus client hydration flow, and key project components like vite.config, app/routes, and the request lifecycle.

In episode 1 you understood Remix's philosophy. Now it's time to open the hood and see how Remix works behind the scenes. Episode 2 is an architecture map: from file to request, from server to browser.
The one core concept to hold onto: every incoming HTTP request flows through a predictable path. Remix looks at the request, matches the URL to a route in the filesystem, calls the loader to fetch data, renders the HTML on the server, then sends the result to the browser to be hydrated. Understanding this flow will make other concepts — caching, error boundaries, streaming — feel natural.
Why is this episode important? Because almost every Remix design decision can be explained through its architecture. If you know what happens when the browser requests a URL, you've already mastered half of this entire series.
Remix maps files in the app/routes folder directly to URLs. The file _index.tsx becomes the page /, about.tsx becomes /about, and folders create hierarchy. This convention lets you read your application's structure straight from the folder structure, with no centralized routing configuration.
app/routes/_index.tsx -> / home page
app/routes/about.tsx -> /about
app/routes/posts._index.tsx -> /posts
app/routes/posts.$id.tsx -> /posts/123 id is a paramYou don't write manual route config like in classic React Router. Files in app/routes become URLs directly without extra configuration.
Remix's distinguishing feature is nested routes: each route file can export a default component, and parent routes render via the Outlet component from child routes. The result is nested layouts that follow the URL structure. The parts shared across several pages only need to be written once in the parent route.
import { Outlet, NavLink } from "@remix-run/react";
export default function LayoutBlog() {
return (
<div>
<nav>
<NavLink to="/posts">All Posts</NavLink>
</nav>
<Outlet />
</div>
);
}The Outlet above is where child routes render. When navigation happens, only the route parts that change re-render, not the whole page — that's the source of Remix's speed.
The loader function is where data is read on the server. It's called every time a route is requested, and its result is sent to the component via useLoaderData. Because it runs on the server, a loader can safely access databases, secrets, and external APIs.
import { useLoaderData } from "@remix-run/react";
export async function loader({ params }) {
const post = await getPost(params.id);
return post;
}
export default function DetailPost() {
const post = useLoaderData();
return <article>{post.judul}</article>;
}The loader receives context like params, request, and context, then returns data. This is the data the component uses to render HTML on the server.
If the loader reads, then action writes. Actions are called when a form is submitted via POST, PUT, PATCH, or DELETE. After the action finishes, Remix refreshes the route's loaders so the displayed data is always up to date. This two-function pattern replaces an entire separate API layer in an SPA application.
When the browser requests a URL, Remix matches the route, calls all the relevant loaders, renders the complete HTML on the server, and sends it along with serialized data. The browser displays the page immediately, then runs JavaScript to hydrate — attaching React's event handlers and state to the existing HTML.
URL comes in -> match route -> run loaders -> render HTML on the server
-> send HTML + data -> browser renders -> hydrate with ReactThis flow runs for both the first navigation and subsequent ones, except subsequent navigations run through an internal fetch and are progressively handled by the client.
Remix doesn't provide its own server. It uses adapters to run on various platforms: @remix-run/node for Node.js, @remix-run/cloudflare-pages for Cloudflare, @remix-run/vercel for Vercel, and so on. The build produces static assets and a server bundle that get deployed to the target platform. Adapters will be covered in depth in episode 20.
A modern Remix project (v3, Vite-based) has the following structure:
nama-aplikasi/
app/
routes/ <- route files, one file per URL
entry.client.tsx <- browser entry point
entry.server.tsx <- server entry point
root.tsx <- root layout, home of the top-level Outlet
public/ <- static assets
vite.config.ts <- build configuration (replaces remix.config.js)
tsconfig.jsonIn Remix v2, the main configuration lived in remix.config.js; since v3 the configuration moved to vite.config.ts. This is an important change when you read older tutorials — tutorials that mention remix.config.js were usually written for pre-v3 versions.
Every route can export special functions: meta for tags in <head>, headers for HTTP response headers, plus ErrorBoundary and CatchBoundary to handle errors. This combination of exports is what's called a route module — one file containing the UI, data, meta, and error behavior of a single URL.
Episode 2 shows Remix's architecture as a whole: filesystem routing with nested layouts, loader and action as a server-side read-write pair, the flow of server rendering followed by client hydration, and a modern Vite-based project structure. Every upcoming concept will use this map as its point of reference.
The key takeaways:
In the next episode, episode 3, we'll get hands-on: starting a Remix project with npx create-remix@latest, understanding the generated folder structure, running the dev server with hot reload, and configuring TypeScript, ESLint, and Prettier. Get your terminal ready, because we're building your first project.