DEV Community

Alex Spinov
Alex Spinov

Posted on

Remix Has a Free API You Should Know About

Remix takes a fundamentally different approach to data loading and mutations. Instead of fetching from API endpoints, Remix uses loaders and actions that run on the server — and it's incredibly powerful.

Loaders — Server-Side Data Fetching

Every route can export a loader function that runs on the server before the component renders:

// app/routes/products.jsx
import { json } from "@remix-run/node"
import { useLoaderData } from "@remix-run/react"

export async function loader({ request }) {
  const url = new URL(request.url)
  const search = url.searchParams.get("q") || ""

  const products = await db.product.findMany({
    where: { name: { contains: search } },
    take: 20
  })

  return json({ products, search })
}

export default function Products() {
  const { products, search } = useLoaderData()
  return (
    <div>
      <h1>Products</h1>
      {products.map(p => <ProductCard key={p.id} product={p} />)}
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Actions — Server-Side Mutations

Actions handle form submissions and POST/PUT/DELETE requests:

// app/routes/products.new.jsx
import { json, redirect } from "@remix-run/node"
import { Form, useActionData } from "@remix-run/react"

export async function action({ request }) {
  const formData = await request.formData()
  const name = formData.get("name")
  const price = parseFloat(formData.get("price"))

  const errors = {}
  if (!name) errors.name = "Name is required"
  if (isNaN(price)) errors.price = "Valid price required"

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

  const product = await db.product.create({ data: { name, price } })
  return redirect(`/products/${product.id}`)
}

export default function NewProduct() {
  const actionData = useActionData()
  return (
    <Form method="post">
      <input name="name" placeholder="Product name" />
      {actionData?.errors?.name && <p>{actionData.errors.name}</p>}
      <input name="price" type="number" step="0.01" />
      {actionData?.errors?.price && <p>{actionData.errors.price}</p>}
      <button type="submit">Create</button>
    </Form>
  )
}
Enter fullscreen mode Exit fullscreen mode

Resource Routes — Pure API Endpoints

Routes without a default export become API-only:

// app/routes/api.users.jsx
import { json } from "@remix-run/node"

export async function loader({ request }) {
  const users = await db.user.findMany()
  return json(users, {
    headers: {
      "Cache-Control": "public, max-age=300"
    }
  })
}

export async function action({ request }) {
  const body = await request.json()
  const user = await db.user.create({ data: body })
  return json(user, { status: 201 })
}
Enter fullscreen mode Exit fullscreen mode

Nested Routes & Parallel Loading

Remix loads data for all nested routes in parallel:

app/routes/
  dashboard.jsx        → loads user data
  dashboard.stats.jsx  → loads stats (parallel!)
  dashboard.recent.jsx → loads recent items (parallel!)
Enter fullscreen mode Exit fullscreen mode
// app/routes/dashboard.jsx
export async function loader({ request }) {
  const user = await getUser(request)
  return json({ user })
}

// app/routes/dashboard.stats.jsx
export async function loader() {
  const stats = await getStats() // Runs IN PARALLEL with parent
  return json({ stats })
}
Enter fullscreen mode Exit fullscreen mode

Streaming with defer()

Stream slow data while showing fast data immediately:

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

export async function loader() {
  const fastData = await getFastData()
  const slowDataPromise = getSlowData() // Don't await!

  return defer({
    fast: fastData,
    slow: slowDataPromise
  })
}

export default function Page() {
  const { fast, slow } = useLoaderData()
  return (
    <div>
      <h1>{fast.title}</h1>
      <Suspense fallback={<p>Loading...</p>}>
        <Await resolve={slow}>
          {(data) => <SlowComponent data={data} />}
        </Await>
      </Suspense>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Key Takeaways

  • Loaders fetch data server-side before rendering
  • Actions handle mutations with progressive enhancement
  • Resource Routes are pure API endpoints
  • Nested Routes load data in parallel automatically
  • defer() streams slow data progressively
  • Everything works without JavaScript enabled (forms!)

Explore Remix docs for the complete API.


Building web scrapers or data pipelines? Check out my Apify actors for ready-made solutions, or email spinov001@gmail.com for custom development.

Top comments (0)