Remix Full-Stack Application
fullstack
TypeScript
scaffolding
playful
Build a full-stack app with Remix using loaders, actions, and progressive enhancement.
By ethan_w
12/8/2025
Prompt
Remix Full-Stack Application
Build a full-stack web application for [Application] using Remix with server-side rendering, progressive enhancement, and type-safe data loading.
Requirements
1. Route Structure
Create routes for:
- [Route 1] - [Purpose] (e.g., home/landing)
- [Route 2] - [Purpose] (e.g., data list with loader)
- [Route 3] - [Purpose] (e.g., form with action)
- [Route 4] - [Purpose] (e.g., dynamic detail page)
2. Data Loading (Loaders)
Implement loaders for:
- Fetching data on server before render
- Type-safe data with TypeScript
- Error handling (404, 500)
- Authentication checks
- Database queries
3. Data Mutations (Actions)
Create actions for:
- Form submissions
- Data creation/updates/deletion
- Validation
- Redirects after success
- Error handling
4. User Experience
Implement:
- Progressive enhancement (works without JS)
- Optimistic UI updates
- Loading states
- Error boundaries
- Pending states with useNavigation
5. Performance
Optimize with:
- Nested routes for partial updates
- Resource routes for APIs
- Prefetching with <Link prefetch>
- Caching headers
Implementation Pattern
// app/routes/[resource].$id.tsx - Route with loader
import { json, type LoaderFunctionArgs, type ActionFunctionArgs } from "@remix-run/node"
import { useLoaderData, useActionData, Form, useNavigation } from "@remix-run/react"
// Loader - runs on server, fetches data
export async function loader({ params, request }: LoaderFunctionArgs) {
const { id } = params
// Check authentication
const userId = await getUserId(request)
if (!userId) {
throw new Response("Unauthorized", { status: 401 })
}
// Fetch data
const [item] = await db.[resource].findUnique({
where: { id }
})
// Handle not found
if (![item]) {
throw new Response("Not Found", { status: 404 })
}
return json({ [item] })
}
// Action - handles form submissions
export async function action({ request, params }: ActionFunctionArgs) {
const formData = await request.formData()
const intent = formData.get("intent")
if (intent === "update") {
const [field1] = formData.get("[field1]")
const [field2] = formData.get("[field2]")
// Validate
const errors = {}
if (!validateEmail([field1])) {
errors.[field1] = "Invalid email"
}
if (errors.length) {
return json({ errors }, { status: 400 })
}
// Update database
await db.[resource].update({
where: { id: params.id },
data: { [field1], [field2] }
})
return redirect(`/[resource]/${params.id}`)
}
if (intent === "delete") {
await db.[resource].delete({
where: { id: params.id }
})
return redirect("/[resource]")
}
}
// Component
export default function [ResourcePage]() {
const { [item] } = useLoaderData<typeof loader>()
const actionData = useActionData<typeof action>()
const navigation = useNavigation()
const isSubmitting = navigation.state === "submitting"
return (
<div>
<h1>{[item].[title]}</h1>
<Form method="post">
<input
name="[field1]"
defaultValue={[item].[field1]}
aria-invalid={actionData?.errors?.[field1] ? true : undefined}
/>
{actionData?.errors?.[field1] && (
<span>{actionData.errors.[field1]}</span>
)}
<button
type="submit"
name="intent"
value="update"
disabled={isSubmitting}
>
{isSubmitting ? "Saving..." : "Save"}
</button>
<button
type="submit"
name="intent"
value="delete"
disabled={isSubmitting}
>
Delete
</button>
</Form>
</div>
)
}
// Error Boundary
export function ErrorBoundary() {
const error = useRouteError()
if (isRouteErrorResponse(error)) {
return (
<div>
<h1>{error.status} {error.statusText}</h1>
<p>{error.data}</p>
</div>
)
}
return (
<div>
<h1>Error</h1>
<p>{error.message}</p>
</div>
)
}
Optimistic UI Pattern
import { useFetcher } from "@remix-run/react"
export default function [Component]({ [item] }: Props) {
const fetcher = useFetcher()
// Optimistic value
const optimistic[Value] = fetcher.formData
? fetcher.formData.get("[field]")
: [item].[field]
return (
<fetcher.Form method="post" action="/api/[action]">
<input
type="hidden"
name="[field]"
value={optimistic[Value]}
/>
<button type="submit">
{optimistic[Value]}
</button>
</fetcher.Form>
)
}
Best Practices
- Use loaders for server-side data fetching
- Implement actions for all mutations
- Add error boundaries to all routes
- Use Form component for progressive enhancement
- Leverage nested routes for better UX
- Implement proper TypeScript types
- Use useFetcher for non-navigation mutations
- Add loading states with useNavigation
- Cache responses with headers
- Validate all user input
Tags
remix
fullstack
react
ssr
Tested Models
gpt-4
claude-3-5-sonnet