CORS issue when requesting data from shopify app (remix template) during local theme development

Topic summary

Core Issue:
Developers encounter CORS errors when making requests from Shopify themes or extensions to Remix-based Shopify apps during local development, particularly when using app proxies or tunnels.

Primary Solution (Posts 4, 17, 19-20):

  • Install remix-utils package and use its cors() function
  • Handle OPTIONS preflight requests explicitly in both loader and action functions
  • For older Remix templates: Add serverDependenciesToBundle: [/^remix-utils.*/] to remix.config.js
  • For Vite-based templates: No config changes needed

Key Implementation Pattern:

if (request.method === 'OPTIONS') {
  return await cors(request, json({ status: 200 }));
}
const response = json({ data });
return await cors(request, response);

Important Caveats:

  • CORS errors typically occur when hosting on VPS/production servers, not with Cloudflare tunnel URLs during development
  • App proxy may not work in local development but functions correctly when deployed
  • For checkout extensions: Use authenticate.public.checkout(request) instead of authenticate.admin(request)
  • Session tokens require clicking the app in store admin at least once to initialize

Status: Multiple users confirmed the solution works after properly handling OPTIONS requests in loader functions.

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

The way to get this working is to use the CORS function from the remix-utils package.

If installing this on the Remix template of the shopify app, you will need to modify the remix.config.js file by including serverDependenciesToBundle: [ /^remix-utils.*/ ] in the module.exports as documented here. This is because remix-utils is published as ESM only and the remix.config.js file has the serverModuleFormat set to cjs. My editor still yells at me about the incorrect import method, but it works nonetheless. This is my updated remix.config.js file:

remix.config.js

if (
  process.env.HOST &&
  (!process.env.SHOPIFY_APP_URL ||
    process.env.SHOPIFY_APP_URL === process.env.HOST)
) {
  process.env.SHOPIFY_APP_URL = process.env.HOST;
  delete process.env.HOST;
}

/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  ignoredRouteFiles: ["**/.*"],
  appDirectory: "app",
  serverModuleFormat: "cjs",
  future: {
    v2_errorBoundary: true,
    v2_headers: true,
    v2_meta: true,
    v2_normalizeFormMethod: true,
    v2_routeConvention: true,
    v2_dev: {
      port: process.env.HMR_SERVER_PORT || 8002,
    },
  },
  serverDependenciesToBundle: [
    /^remix-utils.*/,
  ],
};

Then, import cors from the remix-utils package and update the return in your loader function of your API route with the cors function, as per their docs linked above. It should look like the following:

api.get-user.jsx

import { json } from '@remix-run/node';
import { cors } from 'remix-utils/cors';

export async function loader({ request }) {

  const response = json({ body: 'data' });

  return await cors(request, response);
}

I am now successfully making requests from the theme which is running locally.

3 Likes