Sending data from frontend to backend using fetch() in Shopify app

Topic summary

A developer is building a Shopify embedded app using Node.js and React that creates a scripttag to capture wishlist button clicks. They need to send customer and product IDs from the client-side scripttag to their backend for database storage via ngrok.

Core Issue:
Multiple CORS and authentication errors when using fetch() to communicate between the storefront and app backend:

  • With headers: CORS preflight redirect error
  • Without headers (using “no-cors” mode): 400 error on the auth endpoint

Code Context:
The scripttag attempts a POST request to send wishlist data, but the provided server.js code appears corrupted/reversed (text is backwards), making it difficult to verify CORS configuration.

Community Responses:

  • One user suggests adding "ngrok-skip-browser-warning": "69420" header to bypass ngrok’s browser warning
  • Another developer reports similar issues with the Remix template, noting that authenticated fetch doesn’t automatically include Bearer tokens and the template lacks App Bridge support by default

Status: Unresolved. The discussion highlights authentication and CORS configuration challenges specific to Shopify app development with ngrok tunneling.

Summarized with AI on November 10. AI used: claude-sonnet-4-5-20250929.

I’m developing an embedded app for Shopify using node.js and react.js. In this app, I’m creating a scripttag when the app is installed, and that scripttag, waits for a wishlist button to be pressed and stores the customer and product id, whenever the clients press the “add to wishlist” button.
Now, I need to store these variables in a database and I cannot do it on the client-side. Therefore, I’m trying to send them to my app using the fetch() function. Also, I’m using ngrok for my HTTPS requests.

  1. If I use headers in my request, it gives me “Access to fetch at ‘https://f865bfefa4b2.ngrok.io/data.js’ from origin ‘https://tesrun.myshopify.com’ has been blocked by CORS policy: Response to preflight request doesn’t pass access control check: Redirect is not allowed for a preflight request.”
  2. If I don’t use headers, It asks me to make the mode"no-cors" which I did.
  3. Now, I’m getting this error: “shop_events_listener-68ba3f1321f00bf07cb78a03841621079812265e950cdccade3463749ea2705e.js:1 GET https://f865bfefa4b2.ngrok.io/auth 400”

So, please suggest, how should I send my data back and forth in Shopify app.

scripttag.js

function addWishList(customer, productid){
var wishlist1 = {
"customer" : customer,
"pid": productid
}
const response1 = fetch(`https://f865bfefa4b2.ngrok.io/pages/data.js`, {
method: 'POST',
mode: "no-cors",
body: JSON.stringify(wishlist1)})
// const responseJson = response1.json();
// const confirmationUrl = responseJson.data.appSubscriptionCreate.confirmationUrl
// console.log(responseJson);

console.log('adding item to the wishlist!!')
}

data.js

fetch(`https://f865bfefa4b2.ngrok.io/scripttag.js`, {
method: "GET",
mode: "no-cors"
})
.then(response => response.json()) 
.then(json => console.log(json))
.catch(err => console.log(err));

server.js

require('isomorphic-fetch');
const dotenv = require('dotenv');
const Koa = require('koa');
const next = require('next');
const { default: createShopifyAuth } = require('@shopify/koa-shopify-auth');
const { verifyRequest } = require('@shopify/koa-shopify-auth');
const session = require('koa-session');
const fetch = require('node-fetch');
const scriptTag = require('./server/scriptTag');
//var koaRouter = require('koa-router');

//const Cryptr = require('cryptr');

dotenv.config();
const {default: graphQLProxy} = require('@shopify/koa-shopify-graphql-proxy');
const {ApiVersion} = require('@shopify/koa-shopify-graphql-proxy');

const port = parseInt(process.env.PORT, 10) || 3000;
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();

const { SHOPIFY_API_SECRET_KEY, SHOPIFY_API_KEY } = process.env;

app.prepare().then(() => {
const server = new Koa();
server.use(session({ secure: true, sameSite: 'none' }, server));
server.keys = [SHOPIFY_API_SECRET_KEY];
server.use(graphQLProxy({version: ApiVersion.October20}));
server.use(
createShopifyAuth({
apiKey: SHOPIFY_API_KEY,
secret: SHOPIFY_API_SECRET_KEY,
scopes: ['read_products', 'write_products', 'write_script_tags', 'read_script_tags'],
async afterAuth(ctx) {
const { shop, accessToken } = ctx.session;
console.log(shop);
console.log(accessToken);
ctx.cookies.set('shopOrigin', shop, {
httpOnly: false,
secure: true,
sameSite: 'none'
}) 

await scriptTag(ctx, accessToken, shop);
// const confirmationUrl = responseJson.data.scriptTagCreate.scriptTag;
console.log(`hi`);

ctx.redirect('/');
},
}),
);

server.use(verifyRequest());
server.use(async (ctx) => {
await handle(ctx.req, ctx.res);
ctx.respond = false;
ctx.res.statusCode = 200;
return
});

server.listen(port, () => {
console.log(`> Ready on http://localhost:${port}`);
});
});
1 Like

Hi Zahraazeem98,

Did you ever figure this out? Also, do you have any issues processing the response from your fetch? Specifically, I don’t see a body being returned even though my api is definitely returning it. It seems like it’s being stripped somewhere.

1 Like

Hi @zahraazeem98 , You have to do skip ngrok browser warning
const url = “https://97fd-61-247-227-239.in.ngrok.io/”;
const headers = new Headers({ “ngrok-skip-browser-warning”: “69420” });

async function cookieTest() {
fetch(${url}api/auth/cookieTest, {
method: “get”,
headers: headers,
})
.then((response) => response.json())
.then((data) => console.log(data))
.catch((err) => console.log(err));
}
if useful marks as like and accept solution

Does any body knows anything about this?
I’ve been trying to days to send the Bearer {token} from my plugin to my microservices proxy using Remix template and no matter what I do the Bearer header is not sent.

The documentation says that remix templates are already using authenticated fetch transparently but no matter what i do when I use the fetch I get no Authorization header in my backend.

I noticed a couple of things:
I can actually retrieve the session object from my react application, but is not in jwt format is totally decoded so I get the full object.

Also noticed that there isn’t any AplicationBridge included by default in the remix template, also the global shopify object doesn’t exists.

So my question is, how do I align the plugin to use the new ShopifyApp() as a new app declaration when a new app is created using app bridge?

Fetch is also been imported from node-fetch and there isn’t any global declarations that act as a wrapper, so I think there is a gap in the documentation for developers trying to create a plugin from the Remix template.

This is my react jsx code:

import React, { useState } from "react";
import {
Page,
Layout,
Text,
Card,
FormLayout,
TextField,
Button,
} from "@shopify/polaris";
import { Form } from "@remix-run/react";
import ConfigService from '../api/config-service';
import { authenticate } from "../shopify.server";

export const action = async({ request }) => {
const { email, password } = await request.formData();
const { session } = await authenticate.admin(request);

const configService = new ConfigService();
const data = await configService.createUserAccount({ email, password }, session.accessToken, session.shop);
console.log(data);
return { ...request.formData };
};

export default function Index() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [error] = useState('');
const [isSubmitting] = useState(false);

return (

);
}

by default the remix template doesn’t provide app bridge support and doesn’t expose the global shopify object.