Create new product

Topic summary

A developer is building a Remix-based Shopify app that allows customers to create products with image uploads. They’ve implemented a form modal in the Shopify theme that collects product details (title, tags) and up to three images via FormData, then submits to /apps/greetabl/template_manager.

Technical Issue:

  • Image preview returns null in console logs
  • File status shows as “FAILED” instead of “READY”
  • The Remix router code includes functions for preparing files and staged uploads using Shopify’s API

Code Structure:

  • Frontend: JavaScript form handler with file upload capability
  • Backend: Remix action handler with prepareFiles() and prepareFilesToCreate() functions
  • Implementation includes polling mechanism (2-second intervals) to check file upload status

Current Status:
The issue remains unresolved. The developer has shared their code and a screenshot showing the failed upload status, seeking help to identify what’s missing in the Remix implementation.

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

Hello support team.
I hope this message finds you well.
I am building remix Shopify app.
Customer can create their product in the Shopify.
So I made the form modal for it

document.querySelector(‘.modal-form’).onsubmit = async function (event) {
event.preventDefault();

const form = event.target;
const formData = new FormData();

// Add form values
formData.append(‘productTitle’, document.getElementById(‘productTitle’).value);
formData.append(‘productTag’, email_${document.getElementById('productTag').value});
formData.append(‘image1’, document.getElementById(‘image1’).files[0]);
if (document.getElementById(‘image2’).files[0]) {
formData.append(‘image2’, document.getElementById(‘image2’).files[0]);
}
if (document.getElementById(‘image3’).files[0]) {
formData.append(‘image3’, document.getElementById(‘image3’).files[0]);
}

try {
const response = await fetch(‘/apps/greetabl/template_manager’, {
method: ‘POST’,
body: formData,
});

const result = await response.json();
if (result.success) {
alert(‘Product created successfully!’);
// form.reset(); // Reset the form
} else {
alert(Error: ${result.error});
}
} catch (error) {
console.error(‘Submission error:’, error);
alert(‘An error occurred. Please try again.’);
}
};

There is this code in Shopify theme.

import { json } from “@remix-run/node”;
import fetch from “node-fetch”;

const prepareFiles = (files) =>
files.map((file) => ({
filename: file.name,
mimeType: file.type,
resource: file.type.includes(“image”) ? “IMAGE” : “FILE”,
fileSize: file.size.toString(),
httpMethod: “POST”,
}));

const prepareFilesToCreate = (stagedTargets, files) =>
stagedTargets.map((stagedTarget, index) => {
return {
originalSource: stagedTarget.resourceUrl,
contentType: files[index].type.includes(“image”) ? “IMAGE” : “FILE”,
filename: files[index].name,
};
});

export const action = async ({ request }) => {
const formData = await request.formData();

const productTitle = formData.get(“productTitle”);
const productTag = formData.get(“productTag”);
const newTag = “pending”;
let updatedTags = [productTag, newTag];
const image1 = formData.get(“image1”);
const image2 = formData.get(“image2”);
const image3 = formData.get(“image3”);

try {
// Step 1: Upload images to Shopify
const imageFiles = [image1, image2, image3].filter(Boolean);
const uploadedImages = await uploadImagesToShopify(imageFiles);

// for (const file of imageFiles) {
// const uploadedImagesrc=await uploadImageToShopify(file);
// if (uploadedImage) images.push({ src: uploadedImage });
// }

// Step 2: Create product with uploaded images
const query = mutation CreateProduct($input: ProductInput!) { productCreate(input: $input) { product { id title images(first: 10) { edges { node { id src } } } } userErrors { field message } } };

const input = {
title: productTitle,
tags: updatedTags,
images: uploadedImages.map((image) => ({ src: image.imageUrl })),
variants: [
{
price: “10.00”, // Set product price
},
],
};

const response = await fetch(
https://${process.env.SHOPIFY_STORE_DOMAIN}/admin/api/2023-10/graphql.json,
{
method: “POST”,
headers: {
“Content-Type”: “application/json”,
“X-Shopify-Access-Token”: process.env.SHOPIFY_ADMIN_API_TOKEN,
},
body: JSON.stringify({ query, variables: { input: input } }),
}
);

const result = await response.json();

if (result.errors) {
throw new Error(result.errors.map((err) => err.message).join(", "));
}

return json({ success: true, product: result.data.productCreate.product });
} catch (error) {
console.error(“Error creating product:”, error);
return json({ success: false, error: error.message });
}
};

// Helper function to upload image
async function uploadImagesToShopify(files) {
const preparedFiles = prepareFiles(files);
const uploadMutation = mutation stagedUploadsCreate($input: [StagedUploadInput!]!) { stagedUploadsCreate(input: $input) { stagedTargets { url resourceUrl parameters { name value } } userErrors { field message } } };

// Step 1: Request a signed upload URL from Shopify
const result = await fetch(
https://${process.env.SHOPIFY_STORE_DOMAIN}/admin/api/2023-10/graphql.json,
{
method: “POST”,
headers: {
“Content-Type”: “application/json”,
“X-Shopify-Access-Token”: process.env.SHOPIFY_ADMIN_API_TOKEN,
},
body: JSON.stringify({ query: uploadMutation, variables: { input: preparedFiles } }),
}
);

const response = await result.json();

const promises = ;

files.forEach((file, index) => {
const url = response.data.stagedUploadsCreate.stagedTargets[index].url;
const params = response.data.stagedUploadsCreate.stagedTargets[index].parameters;
const formData = new FormData();

params.forEach((param) => {
formData.append(param.name, param.value);
});
formData.append(“file”, file);

const promise = fetch(url, {
method: “POST”,
body: formData,
});
promises.push(promise);
});

await Promise.all(promises);

const createMutation = mutation fileCreate($files: [FileCreateInput!]!) { fileCreate(files: $files) { files { id preview { image { url } } } userErrors { field message } } };

const stagedTargets = response.data.stagedUploadsCreate.stagedTargets;
const filesToCreate = prepareFilesToCreate(stagedTargets, files);

const createResult = await fetch(
https://${process.env.SHOPIFY_STORE_DOMAIN}/admin/api/2023-10/graphql.json,
{
method: “POST”,
headers: {
“Content-Type”: “application/json”,
“X-Shopify-Access-Token”: process.env.SHOPIFY_ADMIN_API_TOKEN,
},
body: JSON.stringify({
query: createMutation,
variables: {
files: filesToCreate,
},
}),
}
);

const responseData = await createResult.json();

const createdfiles = responseData.data.fileCreate.files;

console.log(“---------0”, createdfiles);

let results = ; // Array to store all file details

for (const file of createdfiles) {
const fileId = file.id;

let status = “”;
let imageUrl = “”;

// Poll for file status
while (status === “”) {
const createFilesMutation = query ($id: ID!) { node(id: $id) { ... on MediaImage { id fileStatus image { url } } } };
const responseQuery = await fetch(
https://${process.env.SHOPIFY_STORE_DOMAIN}/admin/api/2023-10/graphql.json,
{
method: “POST”,
headers: {
“Content-Type”: “application/json”,
“X-Shopify-Access-Token”: process.env.SHOPIFY_ADMIN_API_TOKEN,
},
body: JSON.stringify({
query: createFilesMutation,
variables: {
id: fileId,
},
}),
}
);

const responseJson = await responseQuery.json();
console.log(“---------1”, responseJson.data.node.fileStatus);

// If the file status is READY, exit the loop
if (responseJson.data.node.fileStatus === “READY”) {
status = responseJson.data.node.fileStatus;
imageUrl = responseJson.data.node.image.url;
console.log(‘___+++++’, imageUrl);
} else {
// Add a delay to prevent spamming the server
await new Promise((resolve) => setTimeout(resolve, 2000));
}
}

// Add the file details to the results array
results.push({
fileId: fileId,
status: status,
imageUrl: imageUrl,
});
}

// Return all results
return {
stagedfiles: results,
stagedTargets: responseData.data.stagedUploadsCreate.stagedTargets,
errors: responseData.data.stagedUploadsCreate.userErrors,
};

}

There is this code in remix app router folder.
So when submitting the form, this code will be called.
But image preview is null in console.log(‘--------0’,…) and console.log(“---------1”, responseJson.data.node.fileStatus) is FAILED.

What am I missing in the remix code?
I hope for you to solve my issue.
Sorry to bother you.