API 404 Error in Custom Shopify Reviews App

Topic summary

A developer is encountering a 404 error with one of two APIs in a custom Shopify reviews app built with Remix.

Working vs. Failing:

  • /app/routes/api/review.jsx successfully displays all reviews to merchants
  • /app/routes/api/api.reviews.jsx fails when fetching product-specific reviews for the storefront, showing “ERROR LOADING REVIEWS”

Technical Setup:

  • Framework: Shopify Remix with Theme App Extension
  • Development: Running via shopify app dev --use-localhost
  • Components: reviews-block.liquid, reviews-widget.js, and the failing API route

Troubleshooting Completed:

  • File paths and naming conventions verified
  • Manual browser testing of endpoints performed
  • Theme block registration confirmed

Help Requested:
The developer seeks guidance on:

  1. Why Remix routing works for one API but not the other
  2. Potential Theme App Extension compatibility issues (CORS, authentication)
  3. Best practices for debugging 404 errors in Shopify’s local development environment

Code snippets for api.reviews.jsx and reviews-widget.js are provided, showing the API expects a productId query parameter and returns JSON responses.

Summarized with AI on October 29. AI used: claude-sonnet-4-5-20250929.

Issue Description:

I’m building a simple Shopify review app with two APIs:

  1. /app/routes/api/review.jsx → Works perfectly (displays all reviews to merchant in a table).

  2. /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

**Relevant Files:**1. reviews-block.liquid (Extensions Block)

  1. reviews-widget.js (Extensions Assets)

  2. 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:

  1. Remix API routing issues (why one API works but the other doesn’t)?

  2. Theme App Extension compatibility (e.g., CORS, authentication)?

  3. 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.";
    }
  });