Перейти к содержимому

14. Resource Routes

Resource Routes — это маршруты без UI-компонента. Они возвращают данные (JSON, PDF, изображения) или обрабатывают запросы (webhooks). Идеально для создания API-эндпоинтов.

Обычный маршрут экспортирует default компонент. Resource Route — только loader или action, без компонента:

app/routes/api.users.ts
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 — это делает маршрут "ресурсным"
app/routes/api.products.ts
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.ts
import { 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}"\`,
},
});
}
app/routes/webhooks.stripe.ts
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 });
}
app/routes/events.ts
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",
},
});
}