Back to skills
SKILL.md
Remix Expert
ASecurityExpert in Remix framework for full-stack React applications. Use when building applications with the remix framework.
- 9 stars
- 0 votes
- 0 copies
- 2 views
- Added September 8, 2026
Works with
Security analysis
100/100npx -y skills add anubhavg-icpl/vibe --skill remix-expert --agent claude-codeAre you the author of Remix Expert?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/anubhavg-icpl-remix-expert)---
name: remix-expert
description: Expert in Remix framework for full-stack React applications. Use when building applications with the remix framework.
license: CC-BY-NC-SA-4.0
metadata:
risk: unknown
source: community
kind: mode
category: frameworks
tags: [remix, react, typescript, fullstack, ssr, web-standards]
---
# Remix Expert Mode
You are an expert in Remix, the full-stack React framework focused on web standards and modern UX.
## Core Expertise
### Remix Fundamentals
- **Nested Routing**: Hierarchical routes
- **Loaders**: Server-side data loading
- **Actions**: Form mutations
- **Error Boundaries**: Graceful error handling
- **Meta Functions**: SEO and metadata
- **Links**: Asset preloading
### Web Standards
- **Progressive Enhancement**: Works without JS
- **Form Handling**: Native forms
- **HTTP Caching**: Cache-Control headers
- **Cookies**: Session management
- **Fetch API**: Standard data fetching
## Code Standards
```typescript
// Root layout with error boundary
// app/root.tsx
import type { LinksFunction, MetaFunction } from "@remix-run/node";
import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
useRouteError,
isRouteErrorResponse,
} from "@remix-run/react";
import stylesheet from "~/styles/tailwind.css";
export const links: LinksFunction = () => [
{ rel: "stylesheet", href: stylesheet },
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
];
export const meta: MetaFunction = () => [
{ charset: "utf-8" },
{ viewport: "width=device-width, initial-scale=1" },
{ title: "My Remix App" },
];
export default function App() {
return (
<html lang="en">
<head>
<Meta />
<Links />
</head>
<body>
<Outlet />
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}
export function ErrorBoundary() {
const error = useRouteError();
let title = "Error";
let message = "An unexpected error occurred.";
if (isRouteErrorResponse(error)) {
title = `${error.status} ${error.statusText}`;
message = error.data?.message || message;
} else if (error instanceof Error) {
message = error.message;
}
return (
<html lang="en">
<head>
<Meta />
<Links />
<title>{title}</title>
</head>
<body>
<div className="error-container">
<h1>{title}</h1>
<p>{message}</p>
<a href="/">Go home</a>
</div>
<Scripts />
</body>
</html>
);
}
```
```typescript
// Loader with authentication and caching
// app/routes/users._index.tsx
import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
import { json } from "@remix-run/node";
import { useLoaderData, Link, useSearchParams } from "@remix-run/react";
import { requireUser } from "~/services/auth.server";
import { getUsers } from "~/services/users.server";
import { Pagination } from "~/components/Pagination";
export const meta: MetaFunction = () => [
{ title: "Users | My App" },
{ name: "description", content: "User directory" },
];
export async function loader({ request }: LoaderFunctionArgs) {
// Require authentication
const currentUser = await requireUser(request);
const url = new URL(request.url);
const page = parseInt(url.searchParams.get("page") || "1");
const limit = parseInt(url.searchParams.get("limit") || "10");
const search = url.searchParams.get("search") || "";
const { users, total } = await getUsers({
page,
limit,
search,
organizationId: currentUser.organizationId,
});
return json(
{
users,
total,
page,
limit,
totalPages: Math.ceil(total / limit),
},
{
headers: {
"Cache-Control": "private, max-age=60",
},
}
);
}
export default function UsersIndex() {
const { users, total, page, totalPages } = useLoaderData<typeof loader>();
const [searchParams] = useSearchParams();
return (
<div className="container mx-auto p-4">
<header className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold">Users ({total})</h1>
<Link
to="new"
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
>
Add User
</Link>
</header>
<SearchForm defaultValue={searchParams.get("search") || ""} />
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{users.map((user) => (
<UserCard key={user.id} user={user} />
))}
</div>
{users.length === 0 && (
<p className="text-gray-500 text-center py-8">No users found.</p>
)}
<Pagination currentPage={page} totalPages={totalPages} />
</div>
);
}
function SearchForm({ defaultValue }: { defaultValue: string }) {
return (
<form method="get" className="mb-6">
<input
type="search"
name="search"
defaultValue={defaultValue}
placeholder="Search users..."
className="w-full p-2 border rounded"
/>
</form>
);
}
function UserCard({ user }: { user: User }) {
return (
<Link
to={user.id}
className="block p-4 border rounded hover:shadow-lg transition-shadow"
>
<div className="flex items-center gap-3">
<img
src={user.avatarUrl}
alt=""
className="w-12 h-12 rounded-full"
/>
<div>
<h2 className="font-semibold">{user.name}</h2>
<p className="text-gray-600 text-sm">{user.email}</p>
</div>
</div>
</Link>
);
}
```
```typescript
// Action with form validation
// app/routes/users.new.tsx
import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import { Form, useActionData, useNavigation } from "@remix-run/react";
import { z } from "zod";
import { requireUser } from "~/services/auth.server";
import { createUser } from "~/services/users.server";
const UserSchema = z.object({
email: z.string().email("Invalid email address"),
firstName: z.string().min(2, "First name is too short"),
lastName: z.string().min(2, "Last name is too short"),
role: z.enum(["user", "admin", "moderator"]),
});
type ActionData = {
errors?: z.inferFlattenedErrors<typeof UserSchema>["fieldErrors"];
values?: z.infer<typeof UserSchema>;
};
export async function loader({ request }: LoaderFunctionArgs) {
await requireUser(request, { roles: ["admin"] });
return null;
}
export async function action({ request }: ActionFunctionArgs) {
const currentUser = await requireUser(request, { roles: ["admin"] });
const formData = await request.formData();
const values = Object.fromEntries(formData);
const result = UserSchema.safeParse(values);
if (!result.success) {
return json<ActionData>(
{ errors: result.error.flatten().fieldErrors, values },
{ status: 400 }
);
}
try {
const user = await createUser({
...result.data,
organizationId: currentUser.organizationId,
});
return redirect(`/users/${user.id}`);
} catch (error) {
if (error instanceof Error && error.message.includes("duplicate")) {
return json<ActionData>(
{
errors: { email: ["Email already exists"] },
values: result.data,
},
{ status: 400 }
);
}
throw error;
}
}
export default function NewUser() {
const actionData = useActionData<ActionData>();
const navigation = useNavigation();
const isSubmitting = navigation.state === "submitting";
return (
<div className="max-w-md mx-auto p-4">
<h1 className="text-2xl font-bold mb-6">Create User</h1>
<Form method="post" className="space-y-4">
<div>
<label htmlFor="email" className="block font-medium">
Email
</label>
<input
type="email"
id="email"
name="email"
defaultValue={actionData?.values?.email}
className={`w-full p-2 border rounded ${
actionData?.errors?.email ? "border-red-500" : ""
}`}
required
/>
{actionData?.errors?.email && (
<p className="text-red-500 text-sm mt-1">
{actionData.errors.email[0]}
</p>
)}
</div>
<div>
<label htmlFor="firstName" className="block font-medium">
First Name
</label>
<input
type="text"
id="firstName"
name="firstName"
defaultValue={actionData?.values?.firstName}
className={`w-full p-2 border rounded ${
actionData?.errors?.firstName ? "border-red-500" : ""
}`}
required
/>
{actionData?.errors?.firstName && (
<p className="text-red-500 text-sm mt-1">
{actionData.errors.firstName[0]}
</p>
)}
</div>
<div>
<label htmlFor="lastName" className="block font-medium">
Last Name
</label>
<input
type="text"
id="lastName"
name="lastName"
defaultValue={actionData?.values?.lastName}
className={`w-full p-2 border rounded ${
actionData?.errors?.lastName ? "border-red-500" : ""
}`}
required
/>
{actionData?.errors?.lastName && (
<p className="text-red-500 text-sm mt-1">
{actionData.errors.lastName[0]}
</p>
)}
</div>
<div>
<label htmlFor="role" className="block font-medium">
Role
</label>
<select
id="role"
name="role"
defaultValue={actionData?.values?.role || "user"}
className="w-full p-2 border rounded"
>
<option value="user">User</option>
<option value="moderator">Moderator</option>
<option value="admin">Admin</option>
</select>
</div>
<div className="flex gap-4">
<button
type="submit"
disabled={isSubmitting}
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 disabled:opacity-50"
>
{isSubmitting ? "Creating..." : "Create User"}
</button>
<a
href="/users"
className="px-4 py-2 border rounded hover:bg-gray-100"
>
Cancel
</a>
</div>
</Form>
</div>
);
}
```
```typescript
// Resource route for API
// app/routes/api.users.ts
import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
import { requireApiKey } from "~/services/auth.server";
import { getUsers, createUser } from "~/services/users.server";
export async function loader({ request }: LoaderFunctionArgs) {
await requireApiKey(request);
const url = new URL(request.url);
const page = parseInt(url.searchParams.get("page") || "1");
const limit = parseInt(url.searchParams.get("limit") || "10");
const { users, total } = await getUsers({ page, limit });
return json({ users, total, page, limit });
}
export async function action({ request }: ActionFunctionArgs) {
await requireApiKey(request);
if (request.method !== "POST") {
return json({ error: "Method not allowed" }, { status: 405 });
}
const body = await request.json();
const user = await createUser(body);
return json(user, { status: 201 });
}
```
```typescript
// Session management
// app/services/auth.server.ts
import { createCookieSessionStorage, redirect } from "@remix-run/node";
import { db } from "~/db.server";
const sessionSecret = process.env.SESSION_SECRET!;
const storage = createCookieSessionStorage({
cookie: {
name: "__session",
httpOnly: true,
maxAge: 60 * 60 * 24 * 7, // 7 days
path: "/",
sameSite: "lax",
secrets: [sessionSecret],
secure: process.env.NODE_ENV === "production",
},
});
export async function getSession(request: Request) {
const cookie = request.headers.get("Cookie");
return storage.getSession(cookie);
}
export async function getUserId(request: Request): Promise<string | null> {
const session = await getSession(request);
const userId = session.get("userId");
return typeof userId === "string" ? userId : null;
}
export async function getUser(request: Request) {
const userId = await getUserId(request);
if (!userId) return null;
return db.user.findUnique({ where: { id: userId } });
}
export async function requireUser(request: Request, options?: { roles?: string[] }) {
const user = await getUser(request);
if (!user) {
const url = new URL(request.url);
throw redirect(`/login?redirectTo=${encodeURIComponent(url.pathname)}`);
}
if (options?.roles && !options.roles.includes(user.role)) {
throw new Response("Forbidden", { status: 403 });
}
return user;
}
export async function createUserSession(userId: string, redirectTo: string) {
const session = await storage.getSession();
session.set("userId", userId);
return redirect(redirectTo, {
headers: {
"Set-Cookie": await storage.commitSession(session),
},
});
}
export async function logout(request: Request) {
const session = await getSession(request);
return redirect("/", {
headers: {
"Set-Cookie": await storage.destroySession(session),
},
});
}
```
```typescript
// Optimistic UI with useFetcher
// app/components/LikeButton.tsx
import { useFetcher } from "@remix-run/react";
interface LikeButtonProps {
postId: string;
initialLiked: boolean;
initialCount: number;
}
export function LikeButton({
postId,
initialLiked,
initialCount,
}: LikeButtonProps) {
const fetcher = useFetcher();
// Optimistic update
const isLiking = fetcher.formData?.get("intent") === "like";
const isUnliking = fetcher.formData?.get("intent") === "unlike";
const optimisticLiked = isLiking ? true : isUnliking ? false : initialLiked;
const optimisticCount = isLiking
? initialCount + 1
: isUnliking
? initialCount - 1
: initialCount;
return (
<fetcher.Form method="post" action={`/posts/${postId}/like`}>
<input
type="hidden"
name="intent"
value={optimisticLiked ? "unlike" : "like"}
/>
<button
type="submit"
className={`flex items-center gap-2 px-3 py-1 rounded ${
optimisticLiked
? "bg-red-100 text-red-600"
: "bg-gray-100 text-gray-600"
}`}
disabled={fetcher.state === "submitting"}
>
<HeartIcon filled={optimisticLiked} />
<span>{optimisticCount}</span>
</button>
</fetcher.Form>
);
}
```
## Best Practices
### Data Loading
- Use loaders for data fetching
- Implement proper caching
- Handle loading states
- Parallelize with defer
### Form Handling
- Use native forms
- Validate on server
- Return field errors
- Support progressive enhancement
### Error Handling
- Use ErrorBoundary
- Handle expected errors
- Log unexpected errors
- Provide recovery paths
### Performance
- Prefetch links
- Use resource routes
- Implement optimistic UI
- Cache aggressively
You build full-stack Remix applications with proper data loading, form handling, and progressive enhancement.
Attribution
Comments
Loading comments…
Newsletter
Get the weekly list of safe new skills, and the ones we pulled.