DEV Community

Alex Spinov
Alex Spinov

Posted on

Remix Has a Free API — Full-Stack React Without the Complexity

Remix is the full-stack web framework built on web standards — now part of React Router v7. It gives you server-side rendering, nested routing, and progressive enhancement without the complexity of Next.js.

Why Remix?

  • Web standards first — uses fetch, Request, Response, FormData natively
  • Nested routing — parallel data loading, no waterfalls
  • Progressive enhancement — forms work without JavaScript
  • No client-side state management — the URL is your state
  • Error boundaries — per-route error handling
  • Streaming — defer non-critical data for faster first paint

Quick Start

npx create-remix@latest my-app
cd my-app
npm run dev
Enter fullscreen mode Exit fullscreen mode

Route-Based Architecture

app/
  routes/
    _index.tsx          → /
    about.tsx           → /about
    dashboard.tsx       → /dashboard (layout)
    dashboard._index.tsx → /dashboard/
    dashboard.settings.tsx → /dashboard/settings
    blog.$slug.tsx      → /blog/:slug (dynamic)
    $.tsx               → catch-all (404)
Enter fullscreen mode Exit fullscreen mode

Loaders (Server-Side Data)

// app/routes/users.tsx
import type { LoaderFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";

// This runs on the SERVER — never sent to the browser
export async function loader({ request }: LoaderFunctionArgs) {
  const url = new URL(request.url);
  const search = url.searchParams.get("search") || "";

  const users = await db.user.findMany({
    where: { name: { contains: search } },
    select: { id: true, name: true, email: true },
  });

  return json({ users, search });
}

// This runs on the CLIENT
export default function Users() {
  const { users, search } = useLoaderData<typeof loader>();

  return (
    <div>
      <h1>Users</h1>
      <form method="get">
        <input name="search" defaultValue={search} placeholder="Search..." />
        <button type="submit">Search</button>
      </form>
      <ul>
        {users.map((user) => (
          <li key={user.id}>{user.name}  {user.email}</li>
        ))}
      </ul>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Actions (Form Handling)

// app/routes/contacts.new.tsx
import type { ActionFunctionArgs } from "@remix-run/node";
import { redirect, json } from "@remix-run/node";
import { useActionData, Form } from "@remix-run/react";

export async function action({ request }: ActionFunctionArgs) {
  const formData = await request.formData();
  const name = formData.get("name") as string;
  const email = formData.get("email") as string;

  const errors: Record<string, string> = {};
  if (!name) errors.name = "Name is required";
  if (!email?.includes("@")) errors.email = "Valid email required";

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

  await db.contact.create({ data: { name, email } });
  return redirect("/contacts");
}

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

  return (
    <Form method="post">
      <div>
        <label>Name</label>
        <input name="name" />
        {actionData?.errors?.name && <span>{actionData.errors.name}</span>}
      </div>
      <div>
        <label>Email</label>
        <input name="email" type="email" />
        {actionData?.errors?.email && <span>{actionData.errors.email}</span>}
      </div>
      <button type="submit">Create Contact</button>
    </Form>
  );
}
Enter fullscreen mode Exit fullscreen mode

This form works without JavaScript enabled — progressive enhancement.

Nested Routes (Parallel Data Loading)

// app/routes/dashboard.tsx (layout)
export async function loader() {
  const user = await getUser();
  return json({ user });
}

export default function Dashboard() {
  const { user } = useLoaderData<typeof loader>();
  return (
    <div className="flex">
      <nav>Welcome, {user.name}</nav>
      <main>
        <Outlet /> {/* Child routes render here */}
      </main>
    </div>
  );
}

// app/routes/dashboard.analytics.tsx (child)
export async function loader() {
  // This loads IN PARALLEL with parent loader!
  const analytics = await getAnalytics();
  return json({ analytics });
}

export default function Analytics() {
  const { analytics } = useLoaderData<typeof loader>();
  return <div>Views: {analytics.views}</div>;
}
Enter fullscreen mode Exit fullscreen mode

Streaming (Deferred Data)

import { defer } from "@remix-run/node";
import { Await, useLoaderData } from "@remix-run/react";
import { Suspense } from "react";

export async function loader() {
  // Critical data — wait for it
  const product = await getProduct();

  // Non-critical — stream it later
  const reviewsPromise = getReviews(); // Don't await!
  const recommendationsPromise = getRecommendations();

  return defer({
    product,
    reviews: reviewsPromise,
    recommendations: recommendationsPromise,
  });
}

export default function Product() {
  const { product, reviews, recommendations } = useLoaderData<typeof loader>();

  return (
    <div>
      <h1>{product.name}</h1>
      <p>${product.price}</p>

      <Suspense fallback={<div>Loading reviews...</div>}>
        <Await resolve={reviews}>
          {(reviews) => (
            <ul>{reviews.map((r) => <li key={r.id}>{r.text}</li>)}</ul>
          )}
        </Await>
      </Suspense>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Error Boundaries

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

  if (isRouteErrorResponse(error)) {
    return (
      <div>
        <h1>{error.status}</h1>
        <p>{error.statusText}</p>
      </div>
    );
  }

  return <div>Something went wrong</div>;
}
Enter fullscreen mode Exit fullscreen mode

Each route has its own error boundary — errors don't crash the whole app.

Remix vs Next.js vs SvelteKit

Feature Remix Next.js SvelteKit
Data loading Loaders (web fetch) getServerSideProps/RSC load functions
Forms Progressive enhancement Client-side Progressive enhancement
Nested routes Built-in (parallel) Layout groups Layout groups
Streaming defer + Suspense RSC streaming Deferred
State management URL + server Client state libs URL + stores
Deploy Anywhere Vercel-optimized Anywhere

Need to scrape data from any website and get it in structured JSON? Check out my web scraping tools on Apify — no coding required, results in minutes.

Have a custom data extraction project? Email me at spinov001@gmail.com — I build tailored scraping solutions for businesses.

Top comments (0)