Remix Samples

Remix full-stack web framework examples including routes, loaders, actions, and modern web development patterns

💻 Basic Remix Application typescript

🟢 simple ⭐⭐

Essential Remix app structure with routing, data loading, and form handling

⏱️ 25 min 🏷️ remix, full-stack, react, typescript
Prerequisites: React basics, TypeScript, Node.js
// Remix Basic Application Examples
// Remix is a full-stack React framework focused on web fundamentals

// 1. Root Route (app/root.tsx)
import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration } from "@remix-run/react";
import type { LinksFunction } from "@remix-run/node";

import globalStylesheetUrl from "./styles/global.css";

export const links: LinksFunction = () => [
  { rel: "stylesheet", href: globalStylesheetUrl },
];

export default function App() {
  return (
    <html lang="en">
      <head>
        <Meta />
        <Links />
      </head>
      <body>
        <Outlet />
        <ScrollRestoration />
        <Scripts />
        <LiveReload />
      </body>
    </html>
  );
}

// 2. Index Route (app/routes/_index.tsx)
import { Link } from "@remix-run/react";

export default function Index() {
  return (
    <div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.4" }}>
      <h1>Welcome to Remix</h1>
      <ul>
        <li>
          <Link to="/posts">View Posts</Link>
        </li>
        <li>
          <Link to="/posts/new">Create New Post</Link>
        </li>
      </ul>
    </div>
  );
}

// 3. Dynamic Route with Loader (app/routes/posts.$postId.tsx)
import { json, LoaderFunctionArgs } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";

// Mock database
const posts = [
  { id: "1", title: "First Post", body: "This is the first post content" },
  { id: "2", title: "Second Post", body: "This is the second post content" },
];

export async function loader({ params }: LoaderFunctionArgs) {
  const post = posts.find(p => p.id === params.postId);
  if (!post) {
    throw new Response("Not Found", { status: 404 });
  }
  return json({ post });
}

export default function PostRoute() {
  const { post } = useLoaderData<typeof loader>();

  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
    </div>
  );
}

// 4. Form with Action (app/routes/posts.new.tsx)
import { ActionFunctionArgs, redirect } from "@remix-run/node";
import { Form } from "@remix-run/react";

export async function action({ request }: ActionFunctionArgs) {
  const formData = await request.formData();
  const title = formData.get("title");
  const body = formData.get("body");

  if (!title || !body) {
    return json({ error: "Title and body are required" }, { status: 400 });
  }

  // In a real app, you would save to database here
  const newPost = {
    id: Date.now().toString(),
    title: title.toString(),
    body: body.toString(),
  };

  return redirect(`/posts/${newPost.id}`);
}

export default function NewPost() {
  return (
    <div>
      <h1>Create New Post</h1>
      <Form method="post">
        <div>
          <label htmlFor="title">Title: </label>
          <input id="title" name="title" type="text" required />
        </div>
        <div>
          <label htmlFor="body">Body: </label>
          <textarea id="body" name="body" required />
        </div>
        <button type="submit">Create Post</button>
      </Form>
    </div>
  );
}

// 5. API Route (app/routes/api/posts.ts)
import { json, LoaderFunctionArgs } from "@remix-run/node";

export async function loader({ request }: LoaderFunctionArgs) {
  const posts = [
    { id: "1", title: "First Post", body: "This is the first post" },
    { id: "2", title: "Second Post", body: "This is the second post" },
  ];

  const url = new URL(request.url);
  const format = url.searchParams.get("format");

  if (format === "json") {
    return json({ posts });
  }

  return json({ posts });
}

export async function action({ request }: ActionFunctionArgs) {
  const formData = await request.formData();
  const post = {
    id: Date.now().toString(),
    title: formData.get("title"),
    body: formData.get("body"),
  };

  return json({ post }, { status: 201 });
}

// 6. Error Boundary (app/routes/posts.$postId.tsx)
import { isRouteErrorResponse, useRouteError } from "@remix-run/react";

export function ErrorBoundary() {
  const error = useRouteError();

  if (isRouteErrorResponse(error)) {
    return (
      <div>
        <h1>
          {error.status} {error.statusText}
        </h1>
        <p>{error.data?.message}</p>
      </div>
    );
  } else if (error instanceof Error) {
    return (
      <div>
        <h1>Error</h1>
        <p>{error.message}</p>
        <p>The stack trace is:</p>
        <pre>{error.stack}</pre>
      </div>
    );
  } else {
    return <h1>Unknown Error</h1>;
  }
}

// 7. Layout Route (app/routes/posts.tsx)
import { Outlet } from "@remix-run/react";
import { Link } from "@remix-run/react";

export default function PostsLayout() {
  return (
    <div>
      <nav>
        <ul>
          <li><Link to="/posts">All Posts</Link></li>
          <li><Link to="/posts/new">New Post</Link></li>
        </ul>
      </nav>
      <main>
        <Outlet />
      </main>
    </div>
  );
}

// 8. Resource Route (app/routes/resources/download.ts)
import { LoaderFunctionArgs } from "@remix-run/node";

export async function loader({ request }: LoaderFunctionArgs) {
  // Generate a CSV file
  const csvContent = "name,email\nJohn Doe,[email protected]\nJane Smith,[email protected]";

  return new Response(csvContent, {
    headers: {
      "Content-Type": "text/csv",
      "Content-Disposition": 'attachment; filename="users.csv"',
    },
  });
}

// 9. Using Sessions (app/utils/sessions.ts)
import { createCookieSessionStorage, redirect } from "@remix-run/node";

const { getSession, commitSession, destroySession } = createCookieSessionStorage({
  cookie: {
    name: "__session",
    secrets: ["your-secret-key"],
    sameSite: "lax",
    path: "/",
    maxAge: 60 * 60 * 24 * 30, // 30 days
    httpOnly: true,
  },
});

export async function requireAuth(request: Request) {
  const session = await getSession(request.headers.get("Cookie"));
  const userId = session.get("userId");

  if (!userId) {
    throw redirect("/login");
  }

  return userId;
}

export { getSession, commitSession, destroySession };

// 10. Database Integration (app/models/post.server.ts)
import { prisma } from "./db.server";

export async function getPosts() {
  return prisma.post.findMany({
    orderBy: { createdAt: "desc" },
  });
}

export async function getPost(id: string) {
  return prisma.post.findUnique({
    where: { id },
  });
}

export async function createPost(data: { title: string; body: string }) {
  return prisma.post.create({
    data,
  });
}

export async function updatePost(id: string, data: { title?: string; body?: string }) {
  return prisma.post.update({
    where: { id },
    data,
  });
}

export async function deletePost(id: string) {
  return prisma.post.delete({
    where: { id },
  });
}

💻 Advanced Remix Patterns typescript

🟡 intermediate ⭐⭐⭐⭐

Complex Remix patterns including authentication, caching, and performance optimization

⏱️ 45 min 🏷️ remix, advanced, patterns, full-stack
Prerequisites: Remix basics, Node.js, TypeScript, Authentication concepts
// Advanced Remix Patterns

// 1. Authentication with Loader and Action
import { json, LoaderFunctionArgs, ActionFunctionArgs, redirect } from "@remix-run/node";
import { Form, useLoaderData } from "@remix-run/react";
import { getSession, commitSession } from "~/utils/sessions";

// Protected Route Loader
export async function loader({ request }: LoaderFunctionArgs) {
  const session = await getSession(request.headers.get("Cookie"));
  const userId = session.get("userId");

  if (!userId) {
    throw redirect("/login");
  }

  const user = await getUserById(userId);
  if (!user) {
    throw redirect("/login");
  }

  return json({ user });
}

// Login Action
export async function action({ request }: ActionFunctionArgs) {
  const formData = await request.formData();
  const email = formData.get("email");
  const password = formData.get("password");

  const user = await authenticateUser(email.toString(), password.toString());

  if (!user) {
    return json({ error: "Invalid credentials" }, { status: 400 });
  }

  const session = await getSession();
  session.set("userId", user.id);

  return redirect("/dashboard", {
    headers: {
      "Set-Cookie": await commitSession(session),
    },
  });
}

export default function Dashboard() {
  const { user } = useLoaderData<typeof loader>();

  return (
    <div>
      <h1>Welcome, {user.name}!</h1>
      <Form method="post" action="/logout">
        <button type="submit">Logout</button>
      </Form>
    </div>
  );
}

// 2. Caching with Headers
export async function loader({ request }: LoaderFunctionArgs) {
  const data = await getExpensiveData();

  return json(data, {
    headers: {
      "Cache-Control": "public, max-age=3600, s-maxage=3600",
      "Vary": "Cookie",
    },
  });
}

// 3. File Uploads
import { unstable_createFileUploadHandler } from "@remix-run/node";
import { unstable_createMemoryUploadHandler } from "@remix-run/node";
import { unstable_parseMultipartFormData } from "@remix-run/node";

const uploadHandler = unstable_createFileUploadHandler({
  directory: "./public/uploads",
  maxPartSize: 5_000_000, // 5MB
});

export async function action({ request }: ActionFunctionArgs) {
  const formData = await unstable_parseMultipartFormData(
    request,
    uploadHandler
  );

  const file = formData.get("file") as File;
  if (!file) {
    return json({ error: "No file uploaded" }, { status: 400 });
  }

  return json({
    message: "File uploaded successfully",
    filename: file.name,
    size: file.size
  });
}

// 4. Real-time Updates with Server-Sent Events
export async function loader({ request }: LoaderFunctionArgs) {
  return new Response(
    new ReadableStream({
      start(controller) {
        const interval = setInterval(async () => {
          const data = await getLatestData();
          controller.enqueue(`data: ${JSON.stringify(data)}\n\n`);
        }, 1000);

        request.signal.addEventListener("abort", () => {
          clearInterval(interval);
          controller.close();
        });
      },
    }),
    {
      headers: {
        "Content-Type": "text/event-stream",
        "Cache-Control": "no-cache",
        "Connection": "keep-alive",
      },
    }
  );
}

// 5. Internationalization (i18n)
export async function loader({ request }: LoaderFunctionArgs) {
  const url = new URL(request.url);
  const locale = url.searchParams.get("locale") || "en";

  const messages = await import(`~/locales/${locale}.json`);

  return json({
    locale,
    messages: messages.default
  });
}

// 6. GraphQL Integration
import { GraphQLClient, gql } from "graphql-request";

const graphQLClient = new GraphQLClient(process.env.GRAPHQL_ENDPOINT!);

const GET_POSTS_QUERY = gql`
  query GetPosts($limit: Int!) {
    posts(limit: $limit) {
      id
      title
      content
      author {
        name
      }
    }
  }
`;

export async function loader() {
  const data = await graphQLClient.request(GET_POSTS_QUERY, { limit: 10 });
  return json(data);
}

// 7. Optimistic Updates
import { useFetcher } from "@remix-run/react";

function LikeButton({ post, liked }: { post: Post; liked: boolean }) {
  const fetcher = useFetcher();

  const isLiked = fetcher.formData ?
    fetcher.formData.get("liked") === "true" :
    liked;

  return (
    <fetcher.Form method="post" action="/api/like">
      <input type="hidden" name="postId" value={post.id} />
      <input type="hidden" name="liked" value={(!isLiked).toString()} />
      <button type="submit">
        {isLiked ? "❤️" : "🤍"} {post.likes}
      </button>
    </fetcher.Form>
  );
}

// 8. Progressive Enhancement
function SearchForm({ query }: { query?: string }) {
  return (
    <Form method="get" action="/search" className="search-form">
      <input
        type="search"
        name="q"
        defaultValue={query}
        placeholder="Search posts..."
        required
      />
      <button type="submit">Search</button>
    </Form>
  );
}

// 9. Custom Server (server.js)
import { createRequestHandler } from "@remix-run/express";
import express from "express";

const app = express();
app.use(express.static("public", { maxAge: "1h" }));

app.all(
  "*",
  createRequestHandler({
    build: require("@remix-run/dev/server-build"),
  })
);

const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`Express server listening on port ${port}`);
});

// 10. Testing with Vitest
import { test, expect } from "vitest";
import { loader } from "~/routes/posts._index";
import { createRequest } from "@remix-run/node";

test("posts loader returns posts", async () => {
  const request = createRequest("http://localhost:3000/posts");
  const response = await loader({ request, params: {}, context: {} });
  const data = await response.json();

  expect(data.posts).toBeInstanceOf(Array);
  expect(data.posts[0]).toHaveProperty("title");
});

// 11. Deployment Configuration (vercel.json)
{
  "version": 2,
  "builds": [
    {
      "src": "package.json",
      "use": "@vercel/static-build",
      "config": {
        "distDir": "public"
      }
    },
    {
      "src": "server.js",
      "use": "@vercel/node"
    }
  ],
  "routes": [
    {
      "src": "/(.*)",
      "dest": "server.js"
    }
  ]
}

// 12. Environment Variables Management (.env.example)
DATABASE_URL="postgresql://user:password@localhost:5432/myapp"
SESSION_SECRET="your-session-secret"
REDIS_URL="redis://localhost:6379"
UPLOAD_DIR="./public/uploads"
NODE_ENV="development"