14. Resource Routes
Resource Routes — это маршруты без UI-компонента. Они возвращают данные (JSON, PDF, изображения) или обрабатывают запросы (webhooks). Идеально для создания API-эндпоинтов.
Что такое Resource Route?
Заголовок раздела «Что такое Resource Route?»Обычный маршрут экспортирует default компонент. Resource Route — только loader или action, без компонента:
import { json } from "@remix-run/node";
export async function loader() { const users = await db.user.findMany(); return json(users); // → GET /api/users → JSON}
export async function action({ request }) { if (request.method === "POST") { const body = await request.json(); const user = await db.user.create({ data: body }); return json(user, { status: 201 }); } return json({ error: "Method not allowed" }, { status: 405 });}// НЕТ export default — это делает маршрут "ресурсным"JSON API
Заголовок раздела «JSON API»export async function loader({ request }) { const url = new URL(request.url); const category = url.searchParams.get("category"); const page = Number(url.searchParams.get("page") ?? "1");
const products = await db.product.findMany({ where: category ? { category } : undefined, skip: (page - 1) * 20, take: 20, });
return json({ products, page });}Файловые эндпоинты
Заголовок раздела «Файловые эндпоинты»// app/routes/download.$filename.tsimport { createReadStream } from "fs";
export async function loader({ params }) { const file = await getFile(params.filename);
if (!file) { throw new Response("File not found", { status: 404 }); }
return new Response(file.buffer, { headers: { "Content-Type": file.mimeType, "Content-Disposition": \`attachment; filename="\${params.filename}"\`, }, });}Webhooks
Заголовок раздела «Webhooks»export async function action({ request }) { const signature = request.headers.get("stripe-signature"); const body = await request.text();
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 handleSubscriptionCanceled(event.data.object); break; }
return json({ received: true });}Server-Sent Events (SSE)
Заголовок раздела «Server-Sent Events (SSE)»export async function loader({ request }) { const stream = new ReadableStream({ start(controller) { const encoder = new TextEncoder();
const interval = setInterval(() => { controller.enqueue( encoder.encode(\`data: \${JSON.stringify({ time: new Date() })}\n\n\`) ); }, 1000);
request.signal.addEventListener("abort", () => { clearInterval(interval); controller.close(); }); }, });
return new Response(stream, { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", }, });}