Skip to content

Remix Resource Routes — JSON, Files, and Non-Page Routes

DodaTech Updated 2026-06-28 3 min read

In this tutorial, you will learn about Remix Resource Routes. We cover key concepts, practical examples, and best practices to help you master this topic.

Learn Remix resource routes: create JSON APIs, file downloads, webhooks, and sitemaps without page components for non-HTML responses.

In this lesson, you'll create resource routes that return JSON, files, or other non-HTML content, useful for APIs, webhooks, and static file generation.

What You'll Learn

How resource routes differ from page routes, create JSON endpoints, handle file downloads, build webhooks, and generate sitemaps.

Why It Matters

Resource routes let you build APIs and webhooks within your Remix app without a separate backend. They use the same loaders/actions pattern but return non-HTML responses.

Real-World Use

DodaZIP's API endpoints for the mobile app are built as Remix resource routes, sharing loaders and utilities with the web app.

flowchart LR
    A[Resource Route] --> B[Loader: GET]
    A --> C[Action: POST]
    B --> D[JSON / File / Text]
    C --> E[Process Webhook]
    style A fill:#121212,color:#fff

JSON Endpoint

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

export const loader = async () => {
  const users = await db.user.findMany({ take: 100 });
  return json(users);
};

A GET request to /api/users returns a JSON array of users. No HTML is rendered.

File Download

// app/routes/reports.$id.download.ts
export const loader = async ({ params }) => {
  const report = await generateReport(params.id);
  
  return new Response(report, {
    headers: {
      "Content-Type": "application/pdf",
      "Content-Disposition": `attachment; filename="report-${params.id}.pdf"`,
    },
  });
};

Webhook Handler

// app/routes/webhooks.stripe.ts
export const action = async ({ request }) => {
  const signature = request.headers.get("stripe-signature");
  const body = await request.text();
  
  // Verify webhook signature
  const event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET);
  
  switch (event.type) {
    case "payment_intent.succeeded":
      await handlePaymentSuccess(event.data.object);
      break;
    case "customer.subscription.deleted":
      await handleSubscriptionCancellation(event.data.object);
      break;
  }
  
  return json({ received: true });
};

Sitemap Generator

// app/routes/sitemap[.]xml.ts
export const loader = async () => {
  const posts = await db.post.findMany({ where: { published: true } });
  
  const urls = posts.map(post => `
    <url>
      <loc>https://example.com/blog/${post.slug}</loc>
      <lastmod>${post.updatedAt.toISOString()}</lastmod>
      <changefreq>weekly</changefreq>
    </url>
  `).join("");
  
  const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
    <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
      <url><loc>https://example.com/</loc></url>
      ${urls}
    </urlset>`;
  
  return new Response(sitemap, {
    headers: { "Content-Type": "application/xml" },
  });
};

Common Mistakes

  1. Not setting correct Content-Type: Resource routes must set their own Content-Type header. Omitting it causes incorrect Parsing.
  2. Returning HTML by mistake: Without specifying headers, Remix may default to HTML. Always set the correct Content-Type.
  3. Forgetting CORS headers for external consumers: Browser-based API consumers need CORS headers. Set them in the response.
  4. Not handling OPTIONS requests: API endpoints should handle OPTIONS for CORS preflight requests.
  5. Exposing sensitive data in webhook responses: Webhooks should return minimal confirmation, not full data payloads.

Practice Questions

  1. What is a resource route? Answer: A route that returns non-HTML content (JSON, files, XML) instead of rendering a page component.

  2. How do you set the response Content-Type? Answer: By passing { headers: { "Content-Type": "application/json" } } to the Response constructor.

  3. Can resource routes have page components? Answer: No. Resource routes don't export a default component. They only export loaders and/or actions.

  4. How do you create a file download endpoint? Answer: Return a Response with file data and Content-Disposition: attachment header.

Challenge

Build a set of RESTful resource routes for a todo API: GET /api/todos, POST /api/todos, GET /api/todos/:id, PUT /api/todos/:id, DELETE /api/todos/:id.

Mini Project

Create an image upload API: POST /api/upload accepts multipart form data, saves the file, and returns the URL. GET /api/files/:id serves the file. Add a webhook endpoint that processes uploaded images.

FAQ

Can resource routes use sessions and cookies?

: Yes. Resource routes have access to the full request context, including cookies and session data.

Do resource routes support streaming?

: Yes. Return a ReadableStream as the response body for streaming responses.

Can I use resource routes for server-sent events?

: Yes. Set Content-Type: text/event-stream and stream events to the client.

Are resource routes cached?

: By default, no. Set cache headers in the response for Caching behavior.

What's Next

Learn about Remix Params and Query for handling URL parameters and search query strings.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro