Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Resource routes: JSON and files

Not every address is a page. A mobile app needs the list of posts as JSON, an accountant wants a CSV export, feed readers want RSS. For this React Router has resource routes: a route module without a default component. Its loader or action returns a ready HTTP response, and the framework sends it as is, without the HTML shell.

JSON

Add a route to app/routes.ts outside the layout, so there is no extra header loader:

// app/routes.ts
route("api/posts", "routes/api.posts.ts"),

Create app/routes/api.posts.ts — note the .ts extension, no JSX is needed here:

// app/routes/api.posts.ts
import { getPosts } from "../data.server";
 
export async function loader() {
  const posts = await getPosts();
  return Response.json(posts);
}

Open http://localhost:5173/api/posts — the browser shows the array of posts as JSON. Response.json is a standard method that sets the Content-Type: application/json header itself.

Receiving data

A resource route can accept requests through an action:

// app/routes/api.posts.ts
import type { Route } from "./+types/api.posts";
import { createPost } from "../data.server";
 
export async function action({ request }: Route.ActionArgs) {
  const { title, body } = await request.json();
  const post = await createPost(String(title), String(body));
  return Response.json(post, { status: 201 });
}

A POST request with a JSON body to /api/posts creates a post and returns it with status 201. You can check it with:

curl -X POST http://localhost:5173/api/posts -H "Content-Type: application/json" -d '{"title":"From the API","body":"Text"}'

A file to download

The response can be anything — just set the right headers:

// app/routes/posts.csv.ts
import { getPosts } from "../data.server";
 
export async function loader() {
  const posts = await getPosts();
  const lines = ["id;title", ...posts.map((p) => `${p.id};${p.title}`)];
 
  return new Response(lines.join("\n"), {
    headers: {
      "Content-Type": "text/csv; charset=utf-8",
      "Content-Disposition": 'attachment; filename="posts.csv"',
    },
  });
}

With the route route("posts.csv", "routes/posts.csv.ts") the address /posts.csv downloads a file with the list of posts.

Linking to a resource

// app/routes/home.tsx
<Link to="/posts.csv" reloadDocument>Download CSV</Link>

The reloadDocument prop tells the link to do a normal browser navigation instead of loading route data.

Common mistake

Linking to a resource route with a plain Link without reloadDocument. The router tries to render a "page" for a route that has no component and shows an error. For resources use Link with reloadDocument or a plain a tag.

Thanks for reading!

Donate to the site — we work hard for you.