Zum Hauptinhalt springen

Remix-Framework-Entwicklung in Cursor: Ein vollständiger Leitfaden

Remix development in Cursor

Remix ist ein Full-Stack-Web-Framework, das auf React Router aufbaut und progressive Verbesserung, Web-Standards und serverseitiges Rendering betont. Remix v2 führte Flat Routes, verbesserte Fehlerbehandlung und ein vereinfachtes Datenladungsmodell ein. Cursor ist mit seiner VS Code-kompatiblen Basis und dem integrierten KI-Assistenten ein idealer Editor für die Remix-Entwicklung -- er versteht .tsx-Routen-Dateien, bietet IntelliSense für Loader-Rückgabetypen und kann gesamte Routen-Module aus natürlichsprachlichen Prompts generieren.

Dieser Leitfaden behandelt den vollständigen Remix-Entwicklungs-Workflow in Cursor: Projekteinrichtung, Routing-Konventionen, Loader und Actions, verschachtelte Routen, Error Boundaries und KI-gestützte Routen-Generierung.

Einrichtung der Remix-Entwicklung in Cursor​

Voraussetzungen​

Remix v2 benötigt Node.js 18 oder neuer (Node 20 LTS wird empfohlen). Überprüfen Sie Ihre Installation:

node --version
npm --version
tipp

Das integrierte Terminal von Cursor (Ctrl+`) synchronisiert sich mit dem Explorer-Panel, sodass Sie ein Projekt scaffolden und sofort generierte Dateien bearbeiten können, ohne von Ihrem Code wegzunavigieren.

Installation der Remix-Erweiterung​

Cursor erbt die VS Code-Erweiterungskompatibilität. Für die Remix-Entwicklung installieren Sie diese Erweiterungen aus der Erweiterungsansicht (Ctrl+Shift+X):

  1. ES7 React/Redux/GraphQL/React-Native snippets -- nützlich für React-Komponenten-Scaffolding.
  2. Tailwind CSS IntelliSense -- wenn Sie Tailwind für das Styling verwenden.
  3. Prisma -- wenn Ihre Remix-App Prisma für Daten verwendet.

Cursors eingebaute TypeScript-Unterstützung verarbeitet die Remix-Typinferenz sofort, einschließlich MetaFunction-, LoaderFunction- und ActionFunction-Typen.

warnung

Installieren Sie nicht die veraltete "Remix"-Syntaxhervorhebungs-Erweiterung. Remix-Dateien sind Standard-.tsx/.ts-Dateien, die Cursor bereits nativ unterstützt. Die veraltete Erweiterung kann mit Cursors TypeScript-Sprachserver kollidieren.

Scaffolding eines Remix-Projekets​

Verwenden Sie die offizielle Remix-CLI, um ein neues Projekt zu erstellen:

npx create-remix@latest my-remix-app --template remix-run/default --no-install --no-git

cd my-remix-app
npm install

Öffnen Sie das Projekt in Cursor:

cursor .

Die Standard-Projektstruktur:

my-remix-app/
├── app/
│ ├── components/
│ ├── routes/
│ │ └── _index.tsx
│ ├── entry.client.tsx
│ ├── entry.server.tsx
│ ├── root.tsx
│ ├── tailwind.css
│ └── entry.client.tsx
├── public/
├── vite.config.ts
├── tsconfig.json
└── package.json
tipp

Bitten Sie Cursor, ein vollständiges Projekt zu scaffolden: "Erstelle ein Remix v2-Projekt mit TypeScript, Tailwind CSS und Prisma. Richte die Vite-Konfiguration und ein Root-Layout mit einer Navigationsleiste ein." Cursor generiert alle Konfigurationsdateien und die anfängliche Routenstruktur.

Routing-Konventionen​

Remix v2 verwendet dateibasiertes Flat Routing. Routen-Dateien im Verzeichnis app/routes/ werden direkt auf URL-Pfade abgebildet.

Grundlegende Routen​

DateiURL-Pfad
app/routes/_index.tsx/
app/routes/about.tsx/about
app/routes/blog.tsx/blog
app/routes/blog.$slug.tsx/blog/:slug

Verschachtelte Routen mit Flat Routes​

Verschachtelte Layouts verwenden die Punkt-Notation. Eine Datei namens app/routes/blog._index.tsx wird innerhalb des app/routes/blog.tsx-Layouts gerendert:

app/routes/
├── _index.tsx → /
├── about.tsx → /about
├── blog.tsx → /blog (layout)
├── blog._index.tsx → /blog (index, renders in blog layout)
├── blog.$slug.tsx → /blog/:slug (renders in blog layout)
├── blog.new.tsx → /blog/new
└── blog.edit.$id.tsx → /blog/edit/:id

Pfadlose Layout-Routen​

Verwenden Sie den Unterstrich-Präfix für Layout-Routen, die kein URL-Segment hinzufügen:

app/routes/
├── _auth.tsx → Layout wrapper (no URL segment)
├── _auth.login.tsx → /login (renders inside _auth layout)
├── _auth.register.tsx → /register (renders inside _auth layout)
└── _auth.forgot-password.tsx → /forgot-password

Routengruppen mit Klammern​

Verwenden Sie Klammern, um Routen zu gruppieren, ohne die URL zu beeinflussen:

app/routes/
├── (marketing)/
│ ├── _index.tsx → /
│ ├── pricing.tsx → /pricing
│ └── features.tsx → /features
├── (app)/
│ ├── dashboard.tsx → /dashboard
│ └── settings.tsx → /settings
tipp

Fragen Sie Cursor: "Generiere die Flat-Route-Dateistruktur für eine Remix-App mit öffentlichen Marketing-Seiten (Start, Preise, Über), einem authentifizierten App-Bereich (Dashboard, Einstellungen, Profil) und einem Blog mit einzelnen Beitragsseiten." Cursor erstellt alle notwendigen Dateien mit korrekten Namenskonventionen.

Loader und Actions​

Loader rufen Daten auf dem Server ab, bevor die Seite gerendert wird. Actions verarbeiten Formularübermittlungen und Mutationen. Beide laufen auf dem Server und geben Ihnen direkten Zugriff auf Ihre Datenbank und APIs.

Definition eines Loaders​

// app/routes/blog.$slug.tsx
import type { LoaderFunctionArgs, MetaFunction } from '@remix-run/node';
import { json } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';
import { getPost } from '~/models/post.server';

export const meta: MetaFunction<typeof loader> = ({ data }) => {
if (!data?.post) {
return [{ title: 'Post not found' }];
}
return [
{ title: data.post.title },
{ name: 'description', content: data.post.excerpt },
];
};

export async function loader({ params }: LoaderFunctionArgs) {
const post = await getPost(params.slug!);

if (!post) {
throw new Response('Not Found', { status: 404 });
}

return json({ post });
}

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

return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
warnung

Typpen Sie Ihre useLoaderData immer mit typeof loader. Dies gibt Ihnen volle IntelliSense für die Datenstruktur in Ihrer Komponente. Ohne den Generic kann Cursor keine genaue Autovervollständigung für post.title, post.content usw. bereitstellen.

Definition einer Action​

Actions verarbeiten Non-GET-Anfragen (POST, PUT, DELETE, PATCH). Sie paaren sich natürlich mit HTML-Formularen:

// app/routes/blog.$slug.tsx (continued)
import { Form, redirect, useActionData } from '@remix-run/react';
import type { ActionFunctionArgs } from '@remix-run/node';
import { createComment } from '~/models/comment.server';

interface ActionData {
errors?: {
name?: string;
body?: string;
};
}

export async function action({ params, request }: ActionFunctionArgs) {
const formData = await request.formData();
const name = formData.get('name');
const body = formData.get('body');

const errors: ActionData['errors'] = {};

if (!name || typeof name !== 'string' || name.trim().length < 2) {
errors.name = 'Name must be at least 2 characters';
}

if (!body || typeof body !== 'string' || body.trim().length < 5) {
errors.body = 'Comment must be at least 5 characters';
}

if (Object.keys(errors).length > 0) {
return json<ActionData>({ errors }, { status: 400 });
}

await createComment({
postId: params.slug!,
authorName: name as string,
body: body as string,
});

return redirect(`/blog/${params.slug}`);
}

export default function BlogPost() {
const actionData = useActionData<typeof action>();

return (
<article>
{/* Post content */}
<section>
<h2>Leave a comment</h2>
<Form method="post">
<div>
<label htmlFor="name">Name</label>
<input type="text" name="name" id="name" />
{actionData?.errors?.name && (
<p className="error">{actionData.errors.name}</p>
)}
</div>
<div>
<label htmlFor="body">Comment</label>
<textarea name="body" id="body" rows={4} />
{actionData?.errors?.body && (
<p className="error">{actionData.errors.body}</p>
)}
</div>
<button type="submit">Submit</button>
</Form>
</section>
</article>
);
}
tipp

Fragen Sie Cursor: "Erstelle eine Remix-Route mit einem Loader, der eine Produktliste aus meiner Prisma-Datenbank abruft, und einer Action, die ein Suchformular mit Query- und Kategorie-Filtern verarbeitet." Cursor generiert den Loader, die Action und die Formular-UI mit ordnungsgemäßer Validierung.

Mehrere Actions pro Route​

Verwenden Sie Intent-Felder, um mehrere Actions in einer einzelnen Route zu verarbeiten:

export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const intent = formData.get('intent');

switch (intent) {
case 'create':
return handleCreate(formData);
case 'update':
return handleUpdate(formData);
case 'delete':
return handleDelete(formData);
default:
return json({ error: 'Unknown intent' }, { status: 400 });
}
}
<Form method="post">
<input type="hidden" name="intent" value="create" />
<button type="submit">Create</button>
</Form>

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

Verschachtelte Routen​

Verschachtelte Routen ermöglichen es Ihnen, Seiten aus übergeordneten Layouts und untergeordneten Inhalten zusammenzusetzen. Die übergeordnete Route rendert ein <Outlet />, in dem untergeordnete Routen erscheinen.

Erstellen eines Layouts mit Outlet​

// app/routes/blog.tsx
import { Outlet, NavLink, useLoaderData } from '@remix-run/react';
import type { LoaderFunctionArgs } from '@remix-run/node';
import { json } from '@remix-run/node';
import { getCategories } from '~/models/category.server';

export async function loader({}: LoaderFunctionArgs) {
const categories = await getCategories();
return json({ categories });
}

export default function BlogLayout() {
const { categories } = useLoaderData<typeof loader>();

return (
<div className="blog-layout">
<nav className="sidebar">
<h2>Categories</h2>
<ul>
{categories.map((cat) => (
<li key={cat.id}>
<NavLink to={`/blog/category/${cat.slug}`}>
{cat.name}
</NavLink>
</li>
))}
</ul>
</nav>
<main className="content">
<Outlet />
</main>
</div>
);
}
warnung

Wenn Ihre verschachtelte Route eine leere Seite rendert, überprüfen Sie, ob das übergeordnete Layout eine <Outlet />-Komponente enthält. Ohne sie hat der Inhalt der untergeordneten Route keinen Ort zum Rendern.

Verschachteltes Datenladen​

Jede Route in einer verschachtelten Hierarchie führt ihren Loader parallel aus. Das bedeutet, dass übergeordnete und untergeordnete Loader gleichzeitig ausgeführt werden, was die wahrgenommene Leistung verbessert:

// app/routes/blog.$slug.tsx
export async function loader({ params }: LoaderFunctionArgs) {
const post = await getPost(params.slug!);
return json({ post });
}

// app/routes/blog.tsx (parent)
export async function loader() {
const categories = await getCategories();
return json({ categories });
}

Optimistische UI mit useNavigation​

Verfolgen Sie den Navigationsstatus, um während Übermittlungen optimistische UI anzuzeigen:

import { useNavigation, Form } from '@remix-run/react';

export default function CreatePost() {
const navigation = useNavigation();
const isSubmitting = navigation.state === 'submitting';

return (
<Form method="post">
<input name="title" placeholder="Title" disabled={isSubmitting} />
<textarea name="content" placeholder="Content" disabled={isSubmitting} />
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Creating...' : 'Create Post'}
</button>
</Form>
);
}
tipp

Fragen Sie Cursor: "Füge optimistische UI zu meinem Remix-Formular hinzu, sodass der Senden-Button einen Ladezustand anzeigt und die Formularfelder während der Übermittlung deaktiviert sind." Cursor verdrahtet useNavigation und verarbeitet die deaktivierten Zustände korrekt.

Error Boundaries​

Remix v2 führte pro-Routen-Error Boundaries ein. Wenn eine Route einen Fehler auslöst, wird nur das Error Boundary dieser Route gerendert -- der Rest der Seite bleibt intakt.

Routen-Level-Error Boundary​

// app/routes/blog.$slug.tsx
import { isRouteErrorResponse, useRouteError } from '@remix-run/react';

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

if (isRouteErrorResponse(error)) {
if (error.status === 404) {
return (
<div>
<h1>Post not found</h1>
<p>The blog post you are looking for does not exist.</p>
<a href="/blog">Back to blog</a>
</div>
);
}
return (
<div>
<h1>Error {error.status}</h1>
<p>{error.statusText}</p>
</div>
);
}

if (error instanceof Error) {
return (
<div>
<h1>Unexpected Error</h1>
<p>{error.message}</p>
</div>
);
}

return <h1>Unknown error occurred</h1>;
}
warnung

Error Boundaries müssen als benannte Exports exportiert werden (export function ErrorBoundary). Standard-Exports sind nur für die Routen-Komponente. Wenn Sie sie als Standard exportieren, wird Remix sie nicht als Error Boundary erkennen.

Root-Error Boundary​

Das Root-Layout (app/root.tsx) sollte ebenfalls ein ErrorBoundary exportieren, um Fehler abzufangen, die nach oben durchsickern:

// app/root.tsx
import { Links, Meta, Outlet, Scripts, useRouteError } from '@remix-run/react';

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

return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Application Error</title>
<Links />
</head>
<body>
<main>
<h1>Something went wrong</h1>
<p>Please try refreshing the page.</p>
</main>
<Scripts />
</body>
</html>
);
}

Fehlerbehandlung in Loadern​

export async function loader({ params }: LoaderFunctionArgs) {
const user = await getUser(params.id);

if (!user) {
throw new Response('User not found', {
status: 404,
statusText: 'Not Found',
});
}

const posts = await getUserPosts(user.id);

if (posts.length === 0) {
throw new Response('No posts found', {
status: 404,
});
}

return json({ user, posts });
}
tipp

Fragen Sie Cursor: "Füge umfassende Fehlerbehandlung zu meiner Remix-Route hinzu. Einschließlich einer 404-Antwort, wenn die Ressource nicht gefunden wird, eines 500 für Datenbankfehler und eines Routen-Level-Error Boundaries mit entsprechender UI für jeden Fall." Cursor generiert die Loader-Fehlerlogik und die Error-Boundary-Komponente.

KI-gestützte Routen-Generierung in Cursor​

Cursors KI-Assistent kann gesamte Routen-Module generieren -- einschließlich Loadern, Actions, Komponenten, Meta-Funktionen und Error Boundaries -- aus einem einzigen Prompt.

Generierung einer vollständigen Route aus einem Prompt​

Öffnen Sie Cursor Chat (Ctrl+L) und beschreiben Sie die Route:

Create a Remix v2 route at app/routes/tasks.$id.tsx that:
1. Loads a single task from Prisma by ID in the loader
2. Has an action to update the task (title, description, status)
3. Shows a form pre-filled with the task data
4. Validates inputs: title (min 3 chars), description (min 10 chars)
5. Includes a meta function with the task title
6. Has an error boundary for 404 (task not found)
7. Redirects to /tasks after successful update

Cursor generiert eine vollständige Routen-Datei mit Loader, Action, Formular-Komponente, Meta-Funktion und Error Boundary. Der generierte Code verwendet die Remix v2-API (LoaderFunctionArgs, ActionFunctionArgs) und umfasst ordnungsgemäße Formularvalidierung mit Inline-Fehlermeldungen, einem Status-Dropdown, das mit dem aktuellen Wert der Aufgabe vorausgewählt ist, und einer Weiterleitung zu /tasks bei Erfolg.

tipp

Wenn Sie Cursor bitten, Routen zu generieren, geben Sie immer die Remix-Version ("v2") und den Flat-Route-Dateipfad an. Dies stellt sicher, dass Cursor die richtige API-Oberfläche verwendet (z. B. LoaderFunctionArgs statt der veralteten LoaderFunction mit params).

KI-gestützte Ressourcen-Routen​

Ressourcen-Routen geben Daten zurück, ohne UI zu rendern -- perfekt für API-Endpunkte:

Create a Remix resource route at app/routes/api.health.tsx that returns
JSON with status "ok", server uptime, and current timestamp. Support GET only.
// app/routes/api.health.tsx
import type { LoaderFunctionArgs } from '@remix-run/node';
import { json } from '@remix-run/node';

export async function loader({}: LoaderFunctionArgs) {
return json({
status: 'ok',
uptime: process.uptime(),
timestamp: new Date().toISOString(),
});
}

Testen und Bereitstellung​

Bereitstellung auf Vercel​

Installieren Sie den Vercel-Adapter:

npm install @remix-run/vercel

Konfigurieren Sie vite.config.ts:

import { vitePlugin as remix } from '@remix-run/dev';
import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';

export default defineConfig({
plugins: [
remix({
future: {
v3_fetcherPersist: true,
v3_relativeSplatPath: true,
v3_throwAbortReason: true,
v3_singleFetch: true,
v3_lazyRouteDiscovery: true,
},
}),
tsconfigPaths(),
],
});
warnung

Testen Sie vor der Bereitstellung den Produktions-Build lokal mit npm run build && npm start. Serverseitiger Code, der im Dev-Modus funktioniert, kann sich in der Produktion anders verhalten -- besonders Code, der auf process.env-Variablen oder Node.js-Built-in-Module angewiesen ist, die die Bereitstellungsplattform möglicherweise nicht unterstützt.

Fazit​

Remix v2 gepaart mit Cursor schafft eine leistungsstarke Full-Stack-Entwicklungsumgebung. Die Betonung des Frameworks auf Web-Standards, progressive Verbesserung und verschachteltes Routing bildet sich sauber auf das Dateisystem ab, und Cursors KI-Assistent kann gesamte Routen scaffolden -- einschließlich Loadern, Actions, Error Boundaries und Formularen -- aus natürlichsprachlichen Beschreibungen. Beginnen Sie mit den Flat-Route-Konventionen, bauen Sie Ihre Datenschicht mit typisierten Loadern und validierten Actions auf, fügen Sie verschachtelte Layouts mit <Outlet /> hinzu, und lassen Sie Cursor den wiederkehrenden Boilerplate-Code generieren, damit Sie sich auf die Geschäftslogik Ihrer Anwendung konzentrieren können.