| 1 | ---
|
| 2 | title: Resource Routes
|
| 3 | ---
|
| 4 |
|
| 5 | # Resource Routes
|
| 6 |
|
| 7 | [MODES: framework, data]
|
| 8 |
|
| 9 | When server rendering, routes can serve "resources" instead of rendering components, like images, PDFs, JSON payloads, webhooks, etc.
|
| 10 |
|
| 11 | ## Defining a Resource Route
|
| 12 |
|
| 13 | A route becomes a resource route by convention when its module exports a loader or action but does not export a default component.
|
| 14 |
|
| 15 | Consider a route that serves a PDF instead of UI:
|
| 16 |
|
| 17 | ```ts
|
| 18 | route("/reports/pdf/:id", "pdf-report.ts");
|
| 19 | ```
|
| 20 |
|
| 21 | ```tsx filename=pdf-report.ts
|
| 22 | import type { Route } from "./+types/pdf-report";
|
| 23 |
|
| 24 | export async function loader({ params }: Route.LoaderArgs) {
|
| 25 | const report = await getReport(params.id);
|
| 26 | const pdf = await generateReportPDF(report);
|
| 27 | return new Response(pdf, {
|
| 28 | status: 200,
|
| 29 | headers: {
|
| 30 | "Content-Type": "application/pdf",
|
| 31 | },
|
| 32 | });
|
| 33 | }
|
| 34 | ```
|
| 35 |
|
| 36 | Note there is no default export. That makes this route a resource route.
|
| 37 |
|
| 38 | ## Linking to Resource Routes
|
| 39 |
|
| 40 | When linking to resource routes, use `<a>` or `<Link reloadDocument>`, otherwise React Router will attempt to use client side routing and fetching the payload (you'll get a helpful error message if you make this mistake).
|
| 41 |
|
| 42 | ```tsx
|
| 43 | <Link reloadDocument to="/reports/pdf/123">
|
| 44 | View as PDF
|
| 45 | </Link>
|
| 46 | ```
|
| 47 |
|
| 48 | ## Handling different request methods
|
| 49 |
|
| 50 | GET requests are handled by the `loader`, while POST, PUT, PATCH, and DELETE are handled by the `action`:
|
| 51 |
|
| 52 | ```tsx
|
| 53 | import type { Route } from "./+types/resource";
|
| 54 |
|
| 55 | export function loader(_: Route.LoaderArgs) {
|
| 56 | return Response.json({ message: "I handle GET" });
|
| 57 | }
|
| 58 |
|
| 59 | export function action(_: Route.ActionArgs) {
|
| 60 | return Response.json({
|
| 61 | message: "I handle everything else",
|
| 62 | });
|
| 63 | }
|
| 64 | ```
|
| 65 |
|
| 66 | Calling this `action` through [`<Form>`][form] or [`useFetcher`][fetcher] still revalidates matched UI loaders. A plain `fetch()` to the resource URL does not. See [Revalidation Optimization][optimize-revalidation] for more info.
|
| 67 |
|
| 68 | ## Return Types
|
| 69 |
|
| 70 | Resource Routes are flexible when it comes to the return type - you can return [`Response`][Response] instances or [`data()`][data] objects. A good general rule of thumb when deciding which type to use is:
|
| 71 |
|
| 72 | - If you're using resource routes intended for external consumption, return `Response` instances
|
| 73 | - Keeps the resulting response encoding explicit in your code rather than having to wonder how React Router might convert `data() -> Response` under the hood
|
| 74 | - If you're accessing resource routes from [fetchers][fetcher] or [`<Form>`][form] submissions, return `data()`
|
| 75 | - Keeps things consistent with the loaders/actions in your UI routes
|
| 76 | - Allows you to stream promises down to your UI through `data()`/[`Await`][await]
|
| 77 |
|
| 78 | ## Error Handling
|
| 79 |
|
| 80 | Throwing an `Error` from Resource route (or anything other than a `Response`/`data()`) will trigger [`handleError`][handleError] and result in a 500 HTTP Response:
|
| 81 |
|
| 82 | ```tsx
|
| 83 | export function action() {
|
| 84 | let db = await getDb();
|
| 85 | if (!db) {
|
| 86 | // Fatal error - return a 500 response and trigger `handleError`
|
| 87 | throw new Error("Could not connect to DB");
|
| 88 | }
|
| 89 | // ...
|
| 90 | }
|
| 91 | ```
|
| 92 |
|
| 93 | If a resource route generates a `Response` (via `new Response()` or `data()`), it is considered a successful execution and will not trigger `handleError` because the API has successfully produced a Response for the HTTP request. This applies to thrown responses as well as returned responses with a 4xx/5xx status code. This behavior aligns with `fetch()` which does not return a rejected promise on 4xx/5xx Responses.
|
| 94 |
|
| 95 | ```tsx
|
| 96 | export function action() {
|
| 97 | // Non-fatal error - don't trigger `handleError`:
|
| 98 | throw new Response(
|
| 99 | { error: "Unauthorized" },
|
| 100 | { status: 401 },
|
| 101 | );
|
| 102 |
|
| 103 | // These 3 are equivalent to the above
|
| 104 | return new Response(
|
| 105 | { error: "Unauthorized" },
|
| 106 | { status: 401 },
|
| 107 | );
|
| 108 |
|
| 109 | throw data({ error: "Unauthorized" }, { status: 401 });
|
| 110 |
|
| 111 | return data({ error: "Unauthorized" }, { status: 401 });
|
| 112 | }
|
| 113 | ```
|
| 114 |
|
| 115 | ### Error Boundaries
|
| 116 |
|
| 117 | [Error Boundaries][error-boundary] are only applicable when a resource route is accessed from a UI, such as from a [`fetcher`][fetcher] call or a [`<Form>`][form] submission. If you `throw` from your resource route in these cases, it will bubble to the nearest `ErrorBoundary` in the UI.
|
| 118 |
|
| 119 | [handleError]: ../api/framework-conventions/entry.server.tsx#handleerror
|
| 120 | [data]: ../api/utils/data
|
| 121 | [Response]: https://developer.mozilla.org/en-US/docs/Web/API/Response
|
| 122 | [fetcher]: ../api/hooks/useFetcher
|
| 123 | [form]: ../api/components/Form
|
| 124 | [await]: ../api/components/Await
|
| 125 | [error-boundary]: ../start/framework/route-module#errorboundary
|
| 126 | [optimize-revalidation]: ./optimize-revalidation
|