How to fetch data from a Customer Account extension using Prisma in Shopify?

Topic summary

Problem: A Customer Account UI extension fails to fetch data from a relative path (/apps/wishlist/api) during dev (tunnel) with error “TypeError: Failed to construct ‘Request’: Failed to parse URL from /apps/wishlist/api.”

Context: The extension renders a wishlist page. It obtains a session token (shopify.sessionToken.get()) and tries to call the app backend with Authorization: Bearer , then queries product details via shopify.query (GraphQL) using hardcoded product IDs. UI includes loading/removal states and displays product image, title, price, and links.

Likely cause: In the extension sandbox, relative URLs are not supported; fetch requires an absolute URL. Customer Account UI extensions run in an isolated worker where same-origin assumptions don’t hold.

Suggested direction (implied by error/context): Use a fully qualified URL to your backend (tunnel URL) or a Shopify App Proxy endpoint on the shop domain (https://{shop}.myshopify.com/apps/…). Validate the session token server-side before accessing data (e.g., via Prisma).

Status: No resolution provided in-thread; author requests help on making the backend fetch work from the extension.

Summarized with AI on December 17. AI used: gpt-5.

File extension customer page

import '@shopify/ui-extensions/preact';
import {render} from 'preact';
import {useEffect, useState} from 'preact/hooks';

export default async () => {
  render(<FullPageExtension />, document.body);
};

function FullPageExtension() {
  const [wishlist, setWishlist] = useState([]);
  const [loading, setLoading] = useState(false);
  const [removeLoading, setRemoveLoading] = useState({
    id: null,
    loading: false,
  });

  async function fetchWishlist() {
    setLoading(true);

    try {
      const token = await shopify.sessionToken.get();

      const res = await fetch('/apps/wishlist/api', {
        headers: {
          Authorization: `Bearer ${token}`,
        },
      });
      const data2 = await res.json();
      setWishlist(data2);

      const productIds = [
        'gid://shopify/Product/7156668629077',
        'gid://shopify/Product/7156669775957',
        'gid://shopify/Product/7156669546581',
        'gid://shopify/Product/7156670300245',
        'gid://shopify/Product/7242516856917',
      ];
      
      const data = await shopify.query(
        `query wishlistProducts($ids: [ID!]!) {
          nodes(ids: $ids) {
            ... on Product {
              id
              title
              onlineStoreUrl
              handle
              priceRange {
                minVariantPrice {
                  amount
                  currencyCode
                }
              }
              featuredImage {
                url
              }
            }
          }
        }`,
        {
          variables: {
            ids: productIds,
          },
        }
      );

      setLoading(false);
      setWishlist(data.data?.nodes || []);
    } catch (error) {
      setLoading(false);
      console.log(error);
    }
  }

  async function deleteWishlistItem(id) {
    // Simulate a server request
    setRemoveLoading({loading: true, id});
    return new Promise((resolve) => {
      setTimeout(() => {
        // Send a request to your server to delete the wishlist item
        setWishlist(wishlist.filter((item) => item.id !== id));

        setRemoveLoading({loading: false, id: null});
        resolve();
      }, 750);
    });
  }

  useEffect(() => {
    fetchWishlist();
  }, []);

  return (
    <s-page heading="Wishlist">
      {/* <s-button slot="breadcrumb-actions" href="/account">
        Повернутися в профіль
      </s-button> */}
      <s-grid gridTemplateColumns="1fr 1fr 1fr" gap="base">
        {!loading &&
          wishlist.length > 0 &&
          wishlist.map((product) => {
            return (
              <s-section key={product.id}>
                <s-stack direction="block" gap="base" paddingBlockEnd="large">
                  <s-image src={product.featuredImage.url} />
                  <s-stack direction="block" gap="small-500">
                    <s-text color="subdued">{product.title}</s-text>
                    <s-text type="strong">
                      {shopify.i18n.formatCurrency(
                        product.priceRange.minVariantPrice.amount,
                        {
                          currency:
                            product.priceRange.minVariantPrice.currencyCode,
                        }
                      )}
                    </s-text>
                  </s-stack>
                </s-stack>
                <s-button slot="primary-action" href={product.onlineStoreUrl}>
                  View product
                </s-button>
                <s-button
                  slot="secondary-actions"
                  loading={
                    removeLoading.loading && product.id === removeLoading.id
                  }
                  onClick={() => {
                    deleteWishlistItem(product.id);
                  }}
                >
                  Remove
                </s-button>
              </s-section>
            );
          })}
        {!loading && wishlist.length === 0 && (
          <s-text>No items in your wishlist.</s-text>
        )}
      </s-grid>
    </s-page>
  );
}

When sending the request, I get an error. Development is being done in dev mode via a tunnel.

TypeError: Failed to construct 'Request': Failed to parse URL from /apps/wishlist/api
    at Gt.eval [as fetch] (eval at self.addEventListener.once (data:,self.addEventListener('message'%2Ce%3D%3E%7Btry%7Beval(e.data)%7Dcatch(err)%7Bthrow%20new%20Error('ExtensionSandboxError%3A%20'%2Berr.message)%7D%7D%2C%7Bonce%3Atrue%7D)%3B:1:41), <anonymous>:1:170268)
    at eval (FullPageExtension.jsx:40:9)
    at Generator.next (<anonymous>)

I need to retrieve the products that a user has added to their wishlist.

did u figure it out? I’m having similar issue… the very same route works from admin extension, but does not works in customer

Hi @plutalexandr

The error occurs because relative URLs like /apps/wishlist/api are not supported inside Shopify UI Extensions. Extensions run in a sandboxed environment that does not share the main browser’s origin, so it cannot automatically resolve the path to your app proxy.

Open your shopify.extension.toml file and add network_access = true under the [capabilities] section. This explicitly allows your extension to communicate with external servers.

Since you are using a session token for authentication, you should bypass the app proxy and call your backend directly. In your development environment, replace /apps/wishlist/api with your full tunnel URL (e.g., https://your-app-id.trycloudflare.com/api/wishlist).

Remember to restart your development server after updating the TOML file for the changes to take effect.

Hope this helps!