Issue Description:
I’m building a simple Shopify review app with two APIs:
-
/app/routes/api/review.jsx → Works perfectly (displays all reviews to merchant in a table).
-
/app/routes/api/api.reviews.jsx → Returns a 404 error when fetching reviews for a specific product on the product page.
The second API is supposed to fetch reviews from the database for a single product, but it fails with ERROR LOADING REVIEWS on the frontend.
Current Setup:- Framework: Shopify Remix
-
Dev Command: shopify app dev --use-localhost
-
Theme App Extension: Enabled
-
Store: content-writer-app.myshopify.com (Password: iabayr)
**Relevant Files:**1. reviews-block.liquid (Extensions Block)
-
reviews-widget.js (Extensions Assets)
-
api.reviews.jsx (Failing API)
Troubleshooting Steps Taken:- Verified file paths and naming conventions.
- Tested API endpoints manually ( browser).
- Confirmed the block is properly registered in the theme.
Request for Help:
Could someone review the code snippets below or suggest common pitfalls for:
-
Remix API routing issues (why one API works but the other doesn’t)?
-
Theme App Extension compatibility (e.g., CORS, authentication)?
-
Debugging 404s in Shopify’s local development environment?
api.reviews.jsx
import prisma from "../../db.server";
export const loader = async ({ request }) => {
const url = new URL(request.url);
const productId = url.searchParams.get("productId");
if (!productId) {
return new Response(JSON.stringify({ error: "Missing productId" }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
const reviews = await prisma.review.findMany({
where: {
productId,
status: "APPROVED",
},
orderBy: {
createdAt: "desc",
},
});
return new Response(JSON.stringify({ reviews }), {
headers: { "Content-Type": "application/json" },
});
};
reviews-block.liquid
{% schema %}
{
"name": "Reviews Widget",
"target": "section",
"settings": []
}
{% endschema %}
<div id="ugc-reviews-widget" data-product-id="{{ product.id }}" style="padding: 20px;">
<!-- Loading/Empty/Error Message -->
<div id="ugc-reviews-message" style="text-align: center; color: #888;">Loading reviews...</div>
<!-- Reviews list -->
<div id="ugc-reviews-container" style="display: none; margin-top: 20px;"></div>
</div>
<script src="{{ 'reviews-widget.js' | asset_url }}" defer></script>
reviews-widget.js
document.addEventListener("DOMContentLoaded", async () => {
const widget = document.getElementById("ugc-reviews-widget");
const productId = widget.getAttribute("data-product-id");
try {
const response = await fetch(`/apps/all-in-one-writer-1/api/reviews?productId=${productId}`);
const data = await response.json();
const message = document.getElementById("ugc-reviews-message");
const container = document.getElementById("ugc-reviews-container");
if (!data || !data.reviews) {
message.innerText = "Failed to load reviews.";
return;
}
if (data.reviews.length === 0) {
message.innerText = "No reviews yet. Be the first to review!";
return;
}
// Reviews loaded
message.style.display = "none";
container.style.display = "block";
data.reviews.forEach((review) => {
const card = document.createElement("div");
card.style.padding = "16px";
card.style.marginBottom = "16px";
card.style.border = "1px solid #eee";
card.style.borderRadius = "8px";
card.style.boxShadow = "0 2px 8px rgba(0,0,0,0.05)";
card.style.backgroundColor = "#fff";
card.innerHTML = `
<div style="font-weight: bold; font-size: 16px;">${review.customerName || "Anonymous"}</div>
<div style="margin: 4px 0; color: #ffc107; font-size: 18px;">
${'⭐'.repeat(review.rating)}
</div>
${review.title ? `<div style="font-weight: 500; margin: 8px 0;">${review.title}</div>` : ''}
<div style="color: #555;">${review.body}</div>
`;
container.appendChild(card);
});
} catch (error) {
const message = document.getElementById("ugc-reviews-message");
message.innerText = "Error loading reviews.";
}
});