---
title: "Remix 101 for Next.js Devs: Key Architectural Differences"
slug: "remix-for-nextjs-developers"
published: "2025-07-08"
updated: "2025-12-21"
categories:
  - "Remix"
llm-intent: "reference"
framework-versions:
  - "remix@2"
  - "next.js@15"
  - "react@18"
  - "node@18"
status: "stable"
llm-purpose: "A practical guide for Next.js devs learning Remix. Explore key differences in routing, data loading, and form handling—with real code examples."
llm-prereqs:
  - "General familiarity with the article topic"
llm-outputs:
  - "Completed outcome: A practical guide for Next.js devs learning Remix. Explore key differences in routing, data loading, and form handling—with real code examples."
---

**Summary Triples**
- (Remix route modules, colocate, server logic and UI in a single file under app/routes)
- (Data fetching in Remix, uses, exported loader functions that run on the server and return JSON or Response)
- (Access loader data, with, useLoaderData() hook inside route component)
- (Form submissions, are handled by, exported action functions that run server-side and return redirects or data)
- (Progressive-enhancement forms, use, <Form> from @remix-run/react to submit to actions with JS fallback)
- (Nested layouts, are implemented with, nested route files and <Outlet/> composition rather than a separate layout API)
- (Next.js data APIs, do not map 1:1 to Remix, getServerSideProps/getStaticProps are replaced by loaders + response caching/headers)
- (API endpoints, are created by, route modules that return Response objects (no separate /api folder required))
- (Client-side fetching, is supported via, useFetcher, useRevalidator, or standard fetch when you need client-only requests)
- (Redirects and headers, are handled with, redirect() helper and Response objects or by returning headers from loaders/actions)
- (Dynamic route params, are declared with, file names like $id.tsx (and optional index.tsx for directory defaults))
- (Routing philosophy, prioritizes, server-first, web-standard behavior and colocated route responsibilities over separated API routes)

### {GOAL}
A practical guide for Next.js devs learning Remix. Explore key differences in routing, data loading, and form handling—with real code examples.

### {PREREQS}
- General familiarity with the article topic

### {STEPS}
1. Follow the detailed walkthrough in the article content below.

<!-- llm:goal="A practical guide for Next.js devs learning Remix. Explore key differences in routing, data loading, and form handling—with real code examples." -->
<!-- llm:prereq="General familiarity with the article topic" -->
<!-- llm:output="Completed outcome: A practical guide for Next.js devs learning Remix. Explore key differences in routing, data loading, and form handling—with real code examples." -->

# Remix 101 for Next.js Devs: Key Architectural Differences
> A practical guide for Next.js devs learning Remix. Explore key differences in routing, data loading, and form handling—with real code examples.
Matija Žiberna · 2025-07-08

If you're like me, you've probably been living comfortably in the Next.js ecosystem for a while now. The App Router, Server Components, and the familiar file-based routing have become second nature. But recently, I found myself diving into the Shopify ecosystem for a new project, and guess what? They're all-in on Remix.

At first, I'll admit, I was a bit skeptical. "Another React framework? Really?" But after spending some quality time with Remix, I've discovered it's not just different—it's refreshingly different. The way it handles server-side logic, data fetching, and forms feels both familiar and completely new at the same time.

The transition wasn't without its learning curve, though. There were moments where I found myself reaching for patterns that simply don't exist in Remix, and other times where I was amazed by how elegantly Remix solved problems I didn't even know I had.

## What You'll Learn

In this article, we'll explore the key conceptual differences between Next.js and Remix that every Next.js developer needs to understand to work effectively with Remix. I've ranked these differences by their impact on your day-to-day development and how much mental adjustment they require.

We'll cover:
- **Server-side logic boundaries** - Where and how you write server code
- **Data fetching patterns** - Moving from various approaches to Remix's loader-centric model
- **Routing philosophy** - Single-file routes vs. Next.js's separation of concerns
- **Form handling** - Embracing progressive enhancement over pure client-side logic
- **Dynamic routing** - Similar concepts, different implementation
- **Client-side data fetching** - When and how to break out of the server-first pattern
- **Layout patterns** - Flexible outlet-based layouts vs. Next.js's structured approach
- **API endpoints** - Resource routes vs. traditional API routes
- **HTTP method handling** - Working within HTML form constraints

By the end, you'll have a solid mental model for thinking in Remix terms and understand why so many developers (and companies like Shopify) are gravitating toward this approach.

Let's dive in!

---

## 1. Server-Side Logic Location & Boundaries 
**Impact: HIGH | Learning Curve: MEDIUM | Philosophy: FUNDAMENTAL**

This is probably the biggest mindset shift you'll encounter. In Next.js, you have multiple ways to write server-side code, and the boundaries can feel a bit blurred. Remix takes a much more opinionated approach.

### Next.js 15 Approach:
- Server Components with "use server" directive
- API routes in separate files (`app/api/route.ts`)
- Server Actions can be co-located in components

```tsx
// app/posts/page.tsx
import { createPost } from './actions'

export default function PostsPage() {
  return (
    <form action={createPost}>
      <input name="title" />
      <button type="submit">Create</button>
    </form>
  )
}

// app/posts/actions.ts
'use server'
export async function createPost(formData: FormData) {
  // server logic here
}
```

### Remix Approach:
- Server logic ONLY in route files via `loader` and `action`
- No "use server" directive - the boundary is file-based
- All components are client-side by default

```tsx
// app/routes/posts.tsx
export const loader = async () => {
  const posts = await getPosts();
  return json({ posts });
};

export const action = async ({ request }) => {
  const formData = await request.formData();
  await createPost(formData);
  return redirect('/posts');
};

export default function Posts() {
  const { posts } = useLoaderData();
  return (
    <Form method="post">
      <input name="title" />
      <button type="submit">Create</button>
    </Form>
  );
}
```

The key insight here is that Remix makes the server/client boundary crystal clear: if it's in a `loader` or `action`, it runs on the server. Everything else is client-side.

---

## 2. Data Fetching Patterns
**Impact: HIGH | Learning Curve: HIGH | Philosophy: FUNDAMENTAL**

Next.js gives you multiple ways to fetch data, which can be both a blessing and a curse. Remix simplifies this with a single, consistent pattern.

### Next.js 15 Approach:
- Server Components for initial data
- Client-side fetching with hooks/libraries
- `getServerSideProps` for pages that need it

```tsx
// Server Component
export default async function Posts() {
  const posts = await fetch('/api/posts').then(r => r.json());
  return <div>{posts.map(post => <div key={post.id}>{post.title}</div>)}</div>;
}
```

### Remix Approach:
- `loader` for GET requests (always runs on server)
- `useLoaderData()` to consume in components
- Must be JSON serializable

```tsx
export const loader = async () => {
  const posts = await getPosts();
  return json({ posts }); // Must be serializable
};

export default function Posts() {
  const { posts } = useLoaderData<typeof loader>();
  return <div>{posts.map(post => <div key={post.id}>{post.title}</div>)}</div>;
}
```

The beauty of Remix's approach is its predictability. Every route that needs data has a `loader`, and every component gets that data via `useLoaderData()`. No guessing about where the data comes from or how it's fetched.

---

## 3. Routing Philosophy & File Structure
**Impact: HIGH | Learning Curve: MEDIUM | Philosophy: MODERATE**

Next.js separates concerns with different file types, while Remix embraces the single-file-per-route approach.

### Next.js 15 Structure:
```
app/
  layout.tsx      // Layout component
  page.tsx        // Page component  
  posts/
    page.tsx      // Posts page
    [id]/
      page.tsx    // Dynamic route
  api/
    posts/
      route.ts    // API endpoint
```

### Remix Structure:
```
app/routes/
  app.tsx                    // Layout route
  app._index.tsx            // Index route for /app
  app.posts.tsx             // /app/posts
  app.posts.$id.tsx         // /app/posts/:id
  api.posts.ts              // Resource route (no UI)
```

Remix's dot notation for nested routes takes some getting used to, but it's incredibly powerful once you grasp it. A single file can contain your layout, data loading, actions, and component—everything related to that route.

---

## 4. Form Handling & Mutations
**Impact: HIGH | Learning Curve: MEDIUM | Philosophy: MODERATE**

This is where Remix really shines. While Next.js has embraced Server Actions, Remix's approach to forms feels more native to the web platform.

### Next.js 15 Approach:
```tsx
'use server'
async function createPost(formData: FormData) {
  // server logic
}

// Usage
<form action={createPost}>
  <input name="title" />
  <button type="submit">Create</button>
</form>
```

### Remix Approach:
```tsx
// Multiple forms on same page hitting different routes
export default function ProductsPage() {
  const addFetcher = useFetcher();
  const removeFetcher = useFetcher();
  
  return (
    <div>
      {/* Posts to /products/add action */}
      <addFetcher.Form method="post" action="/products/add">
        <input name="name" />
        <button type="submit">Add</button>
      </addFetcher.Form>
      
      {/* Posts to /products/remove action */}
      <removeFetcher.Form method="post" action="/products/remove">
        <input name="intent" value="delete" type="hidden" />
        <input name="id" />
        <button type="submit">Remove</button>
      </removeFetcher.Form>
    </div>
  );
}
```

The `useFetcher` hook is incredibly powerful—it lets you have multiple forms on the same page, each with their own loading states and error handling, all while maintaining progressive enhancement.

---

## 5. Dynamic Route Parameters
**Impact: MEDIUM | Learning Curve: LOW | Philosophy: MODERATE**

Both frameworks handle dynamic routes well, but the syntax differs slightly.

### Next.js 15:
```tsx
// app/posts/[id]/page.tsx
export default function Post({ params }: { params: { id: string } }) {
  return <div>Post {params.id}</div>;
}
```

### Remix:
```tsx
// app/routes/posts.$id.tsx
export const loader = async ({ params }) => {
  const post = await getPost(params.id);
  return json({ post });
};

export default function Post() {
  const { post } = useLoaderData();
  return <div>{post.title}</div>;
}
```

The dollar sign syntax takes a moment to get used to, but it's quite intuitive once you're familiar with it.

---

## 6. Client-Side Data Fetching
**Impact: MEDIUM | Learning Curve: LOW | Philosophy: MODERATE**

Sometimes you need to fetch data on the client side. Both frameworks handle this, but Remix has some unique patterns.

### Next.js 15:
```tsx
'use client'
import { useState, useEffect } from 'react';

export default function Posts() {
  const [posts, setPosts] = useState([]);
  
  useEffect(() => {
    fetch('/api/posts').then(r => r.json()).then(setPosts);
  }, []);
  
  return <div>{/* render posts */}</div>;
}
```

### Remix:
```tsx
export default function Posts() {
  const fetcher = useFetcher();
  
  useEffect(() => {
    fetcher.load("/api/posts");  // Calls another route's loader
  }, []);
  
  return <div>{fetcher.data?.map(post => <div key={post.id}>{post.title}</div>)}</div>;
}
```

Remix's `useFetcher` can call any route's loader, making it easy to fetch data from other routes without duplicating logic.

---

## 7. Layout Patterns
**Impact: MEDIUM | Learning Curve: LOW | Philosophy: MINOR**

Layouts work differently in each framework, but both are flexible.

### Next.js 15:
```tsx
// app/layout.tsx
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <nav>Navigation</nav>
        {children}
      </body>
    </html>
  );
}
```

### Remix:
```tsx
// app/routes/app.tsx
import { Outlet } from "@remix-run/react";

export default function AppLayout() {
  return (
    <div>
      <nav>Navigation</nav>
      <Outlet />
    </div>
  );
}
```

Remix's outlet-based approach is more flexible—any route can become a layout just by including an `<Outlet />` component.

---

## 8. Resource Routes vs API Routes
**Impact: MEDIUM | Learning Curve: LOW | Philosophy: MINOR**

Both frameworks support API endpoints, but they handle them differently.

### Next.js 15:
```tsx
// app/api/posts/route.ts
export async function GET() {
  const posts = await getPosts();
  return Response.json(posts);
}

export async function POST(request: Request) {
  const data = await request.json();
  await createPost(data);
  return Response.json({ success: true });
}
```

### Remix:
```tsx
// app/routes/api.posts.ts
export const loader = async () => {
  const posts = await getPosts();
  return json(posts);
};

export const action = async ({ request }) => {
  const formData = await request.formData();
  await createPost(formData);
  return json({ success: true });
};
// No default export = no UI, just data
```

Remix's resource routes are just regular routes without a UI component. The `loader` handles GET requests, and `action` handles mutations.

---

## 9. HTTP Method Handling
**Impact: MEDIUM | Learning Curve: LOW | Philosophy: MINOR**

This is where the web platform's limitations become apparent in Remix.

### Next.js 15:
```tsx
// app/api/posts/[id]/route.ts
export async function DELETE(request: Request, { params }) {
  await deletePost(params.id);
  return Response.json({ success: true });
}

// Client call
fetch('/api/posts/123', { method: 'DELETE' });
```

### Remix:
```tsx
// app/routes/posts.$id.tsx
export const action = async ({ request }) => {
  const formData = await request.formData();
  const intent = formData.get("intent");
  
  if (intent === "delete") {
    await deletePost(formData.get("id"));
    return redirect("/posts");
  }
};

// Form usage
<Form method="post">
  <input name="intent" value="delete" type="hidden" />
  <input name="id" value={post.id} type="hidden" />
  <button type="submit">Delete</button>
</Form>
```

Since HTML forms only support GET and POST, Remix uses conventions like `intent` to differentiate between different actions.

---

## Key Takeaways for Next.js Developers

1. **Server boundaries are file-based**: No more `"use server"` directives. If it's in a `loader` or `action`, it runs on the server.

2. **Data flows through loaders**: Every route that needs data has a `loader`, and components consume that data via `useLoaderData()`.

3. **Embrace progressive enhancement**: Remix's `<Form>` component works without JavaScript, then enhances the experience when JS loads.

4. **One file per route**: A single route file can handle data loading, mutations, and UI—no need to separate concerns across multiple files.

5. **useFetcher is your friend**: For complex interactions, multiple forms, or cross-route communication, `useFetcher` is incredibly powerful.

The learning curve is moderate because the core React concepts remain the same, but the patterns are different enough to require some mental adjustment. Once you embrace Remix's philosophy of staying closer to web standards, you'll find it's a refreshing take on full-stack React development.

Give it a try on your next project—you might be surprised by how much you enjoy the clarity and simplicity of Remix's approach!

## LLM Response Snippet
```json
{
  "goal": "A practical guide for Next.js devs learning Remix. Explore key differences in routing, data loading, and form handling—with real code examples.",
  "responses": [
    {
      "question": "What does the article \"Remix 101 for Next.js Devs: Key Architectural Differences\" cover?",
      "answer": "A practical guide for Next.js devs learning Remix. Explore key differences in routing, data loading, and form handling—with real code examples."
    }
  ]
}
```