DEV Community

Cover image for A detailed guide to create an AI Shopify App - Step by Step
Adib ⚡️
Adib ⚡️

Posted on • Updated on

A detailed guide to create an AI Shopify App - Step by Step

Hey there, future app creator!

So, you want to build a Shopify app? That's awesome! Whether you're a coding whiz or just starting out, this guide will walk you through creating your very own app for the Shopify platform. We'll cover everything from setting up your workspace to getting your app live in the Shopify App Store.

Quick Start (aka the "I'm in a hurry" version)

Short on time? No worries! Here's the express route:

  1. We've got a starter kit that handles all the boring setup stuff.
  2. You can jump right into making your app do cool things.
  3. This guide is still super helpful for understanding how Shopify apps work.

Want to dive in?

Grab our ShopiFast starter kit and let's go!

Now, if you're ready for the full adventure, let's get started!

1. Introduction

What are Shopify apps?

Shopify apps are extensions that add functionality to Shopify stores. They can range from simple tools to complex solutions that integrate with various aspects of a merchant's business.

Why build a custom Shopify app?

Building a custom Shopify app allows you to create unique solutions for merchants, potentially fill gaps in the market, and even generate revenue through the Shopify App Store.

Overview of the app we'll be building

In this tutorial, we'll create a simple inventory management app that helps merchants track their product stock levels and sends notifications when items are running low.

2. Prerequisites and Setup

Creating a Shopify Partner account

  1. Go to the Shopify Partners website.
  2. Click "Join now" and follow the registration process.
  3. Once approved, log in to your Shopify Partners dashboard.

Installing necessary tools

Node.js and npm

  1. Visit the official Node.js website.
  2. Download and install the LTS (Long Term Support) version for your operating system.

  3. To verify the installation, open a terminal and run:

    node --version
    npm --version
    

Shopify CLI

  1. Open your terminal.
  2. Install Shopify CLI globally by running:

    npm install -g @shopify/cli @shopify/theme
    
  3. Verify the installation:

    shopify version
    

Setting up a development store

  1. Log in to your Shopify Partners dashboard.
  2. Click "Stores" in the left sidebar.
  3. Click "Add store" and select "Development store".
  4. Follow the prompts to create your development store.

3. Creating Your Shopify App

Using Shopify CLI to generate a new app

  1. Open your terminal and navigate to your desired project directory.
  2. Run the following command to create new app with node.js template.

    shopify app inti template=node
    

Note: running the command without template argument , will create a remix app.

  1. Install dependencies
yarn install
Enter fullscreen mode Exit fullscreen mode
  1. You can create a Shopify app either via the Shopify CLI or from the Shopify Partners Dashboard. If you create the app from the dashboard, you’ll need to connect it to your project manually. If you use the CLI, it will handle app creation for you.

create a shopify app 

  1. Choose the app name

create node.js shopify app

  1. Choose the store you created before for development

chose shopify app store to run the app

chose shopify app name

  1. To install your app on a shop, it must be accessible from the internet. You have two options: use your own tunneling service, such as Ngrok, or let Shopify handle it, which is recommended. Shopify uses Cloudflare Argo Tunnel to expose your app to the world. When you use Shopify’s method, a new URL is generated each time you run the app. The main downside is that this URL changes every time.

run shopify app

  1. I f you chose to use your own tunnel tool, you need to update the urls in shopify.app.toml file with your tunnel urls. Else , just skip this step.

.......

application_url = "https://copy-b-christopher-editorials.trycloudflare.com"

.......

redirect_urls = [
  "https://copy-b-christopher-editorials.trycloudflare.com/auth/callback",
  "https://copy-b-christopher-editorials.trycloudflare.com/auth/shopify/callback",
  "https://copy-b-christopher-editorials.trycloudflare.com/api/auth/callback"
]
.......
Enter fullscreen mode Exit fullscreen mode
  1. Install the app on your store, by click on p

run shopify app

congratulations you just created your first shopify app.

4. Understanding Shopify App Architecture

The generated project structure for a Shopify app consists of three main components. Let's explore each one:

run shopify app

1. Frontend

  • This is the user interface of your app that will be embedded in the Shopify store admin.
  • It's where merchants interact with your app.
  • Typically built using React, Polaris, JavaScript.

The frontend directory contains the React-based user interface:

  • App.jsx and Routes.jsx: Define the main app structure and routing.

  • assets folder: Contains static files like images and SVGs.

  • components folder: Houses reusable React components.

  • providers : Contains context providers for app-wide state management.

  • hooks folder: Custom React hooks for shared logic.

  • useAppQuery.js and useAuthenticatedFetch.js: handle data fetching and authenticated data fetchting.

  • pages folder: Contains individual page components, used in Shopify admin.

2. Backend

  • This is the server-side component aka API.
  • It handles various backend tasks, including:
    • Authentication
    • Interacting with the Shopify API
    • Database operations
    • Business logic processing
  • This project is using Node.js, Express and sqlite database

  • index.js: The main entry point for the Node.js server.

  • database.sqlite: SQLite database file for local data storage.

  • privacy.js: Handles app mandatory compliance webhooks, required for public apps listed on shopify app store.

  • shopify.js: configures a Shopify app using the @shopify/shopify-app-express package. It sets up API version, authentication paths, webhooks, and uses SQLite for session storage. And, also includes billing configuration.

3. Shopify App Configurations

client_id = "8538fc16e6b9da87768f65308885c52c"
name = "hellofy"
handle = "hellofy"
application_url = "https://copy-b-christopher-editorials.trycloudflare.com"
embedded = true

[build]
automatically_update_urls_on_dev = true
dev_store_url = "helllofy.myshopify.com"
include_config_on_deploy = true

[access_scopes]
# Learn more at https://shopify.dev/docs/apps/tools/cli/configuration#access_scopes
scopes = "write_products"

[auth]
redirect_urls = [
  "https://copy-b-christopher-editorials.trycloudflare.com/auth/callback",
  "https://copy-b-christopher-editorials.trycloudflare.com/auth/shopify/callback",
  "https://copy-b-christopher-editorials.trycloudflare.com/api/auth/callback"
]

[webhooks]
api_version = "2024-07"

[pos]
embedded = false

Enter fullscreen mode Exit fullscreen mode
  • This component contains the configuration for your app.
  • It includes settings such as:
    • App name, handle

test shopify app on store

- API scopes (permissions your app needs)
- App URLs (for installation, callback handling, etc.)
- And other configurations
Enter fullscreen mode Exit fullscreen mode

Learn more about app configuration here: App configuration

Understanding these components and their roles will help you navigate and develop your Shopify app more effectively.

5. Configuring Your App

Setting up app name and description

  1. Open shopify.app.toml in your project root.
  2. Update the name and scopes fields to match your app's requirements.

Configuring app scopes and permissions

In the same shopify.app.toml file, update the scopes array with the required permissions, e.g.:

scopes = "write_products"
Enter fullscreen mode Exit fullscreen mode

you can learn more about scopes here.

Setting up API credentials

Shopify credentials are handled differently in development and production environments.

  • During development, the Shopify CLI automatically provides these credentials, which you can view by running "shopify app env show". There's no need to manually add them to your .env file.

shopify env variables

  • For production, however, you must explicitly set these credentials in your environment. You can obtain the necessary credentials (API key and API secret key) either by running the same CLI command or by accessing your Shopify Partners dashboard:
    1. In your Shopify Partners dashboard, go to "Apps" and select your app.
    2. Under "App credentials", you'll find your API key and API secret key.

Setting up the database

By default the create project comes with sqlite database, in most cases sqlite is optimal for production. There’re a various databases you can use based on your cases: MonogDB, Postgres, MySQL …

Let’s change it to Postgres with Prisma ORM.

  1. Install prisma
yarn add prisma @prisma/client
Enter fullscreen mode Exit fullscreen mode
  1. Setup prisma
yarn prisma init
Enter fullscreen mode Exit fullscreen mode
  1. Install session storage manager to let Shopify manage session:
yarn add  @shopify/shopify-app-session-storage-prisma
Enter fullscreen mode Exit fullscreen mode
  1. Update shopify.js to the following
import { LATEST_API_VERSION } from "@shopify/shopify-api";
import { shopifyApp } from "@shopify/shopify-app-express";
import { PrismaSessionStorage } from '@shopify/shopify-app-session-storage-prisma';
import { restResources } from "@shopify/shopify-api/rest/admin/2023-04";
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();
const storage = new PrismaSessionStorage(prisma);

const shopify = shopifyApp({
  api: {
    apiVersion: LATEST_API_VERSION,
    restResources,
    billing: undefined,
  },
  auth: {
    path: "/api/auth",
    callbackPath: "/api/auth/callback",
  },
  webhooks: {
    path: "/api/webhooks",
  },
  sessionStorage: storage,
});

export default shopify;

Enter fullscreen mode Exit fullscreen mode
  1. Update Prisma schema
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model Session {
  id            String    @id
  shop          String
  state         String
  isOnline      Boolean   @default(false)
  scope         String?
  expires       DateTime?
  accessToken   String
  userId        BigInt?
  firstName     String?
  lastName      String?
  email         String?
  accountOwner  Boolean   @default(false)
  locale        String?
  collaborator  Boolean?  @default(false)
  emailVerified Boolean?  @default(false)
}

Enter fullscreen mode Exit fullscreen mode
  1. Generate prisma models
yarn prisma generate
Enter fullscreen mode Exit fullscreen mode
  1. create a docker-compose with postgres service
version: "3"
services:
  postgres:
    ports:
      - 5432:5432
    environment:
      - POSTGRES_DB=hellofy
      - POSTGRES_USER=default
      - POSTGRES_PASSWORD=secret
    image: postgres:13.3

volumes:
  postgres_data:

Enter fullscreen mode Exit fullscreen mode
  1. Run the database
docker-compose up -d postgres
Enter fullscreen mode Exit fullscreen mode
  1. Update .env with the right database url
DATABASE_URL="postgresql://default:secret@localhost:5432/hellofy?schema=public"
Enter fullscreen mode Exit fullscreen mode
  1. Generate database migration:
yarn prisma migrate dev
Enter fullscreen mode Exit fullscreen mode
  • enter migration name: ex: init

generate database migration

Now, run the app again and you see the session now is storage in postgres

shopify database schema

Congratulations 🥳! You just created a first Shopify App.

6. Developing Core Functionality

Building a Shopify App to Update Product Descriptions with OpenAI

Follow these steps to integrate OpenAI into your Shopify app to update product descriptions.

  1. Create an OpenAI Account

Create an OpenAI account by visiting OpenAI Platform.

  1. Get OpenAI Credentials
....
OPENAI_API_KEY=sk-proj-..........XZIFFL29UxC9T3BlbkFJ..............
Enter fullscreen mode Exit fullscreen mode
  1. Install open ai library
yarn add openai
Enter fullscreen mode Exit fullscreen mode
  1. Add openai library to index.js

import OpenAI from "openai";

// Initialize OpenAI
const openai = new OpenAI({
  apiKey: proccess.env.OPENAI_API_KEY,
});

Enter fullscreen mode Exit fullscreen mode
  1. Create products page in pages folder
import {
  Badge,
  Card,
  ChoiceList,
  EmptySearchResult,
  Frame,
  IndexFilters,
  IndexTable,
  Page,
  Toast,
  useIndexResourceState,
  useSetIndexFiltersMode,
} from "@shopify/polaris";
import { useCallback, useState, useEffect } from "react";
import { useAppQuery, useAuthenticatedFetch } from "../hooks";
import ProductUpdate from "../components/ProductUpdate";
import { useProducts } from "../services/product";

const Products = () => {
  const emptyToastProps = { content: null };
  const [toastProps, setToastProps] = useState(emptyToastProps);
  const fetch = useAuthenticatedFetch();

  const { data, isLoading } = useProducts();

  const [showModal, setShowModal] = useState(false);
  const [itemStrings, setItemStrings] = useState([
    "All",
    "Active",
    "Draft",
    "Archived",
  ]);
  const [selected, setSelected] = useState(0);
  const [sortSelected, setSortSelected] = useState(["product asc"]);
  const { mode, setMode } = useSetIndexFiltersMode();
  const [tone, setStatus] = useState(undefined);
  const [type, setType] = useState(undefined);
  const [queryValue, setQueryValue] = useState("");

  const [products, setProducts] = useState([]);

  useEffect(() => {
    if (data?.data) {
      const transformedProducts = data.data.map((product) => ({
        id: product.id,
        price: `$${product.variants[0].price}`,
        product: product.title,
        tone: (
          <Badge tone={product.status === "active" ? "success" : "info"}>
            {product.status}
          </Badge>
        ),
        inventory: `${product.variants[0].inventory_quantity} in stock`,
        type: product.product_type,
        description: product.body_html,
        image: product.image?.src,
      }));
      setProducts(transformedProducts);
    }
  }, [data]);

  const resourceName = {
    singular: "product",
    plural: "products",
  };

  const { selectedResources, allResourcesSelected, handleSelectionChange } =
    useIndexResourceState(products);

  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

  const disambiguateLabel = (key, value) => {
    switch (key) {
      case "type":
        return value.map((val) => `type: ${val}`).join(", ");
      case "tone":
        return value.map((val) => `tone: ${val}`).join(", ");
      default:
        return value;
    }
  };

  const isEmpty = (value) => {
    if (Array.isArray(value)) {
      return value.length === 0;
    } else {
      return value === "" || value == null;
    }
  };

  const deleteView = (index) => {
    const newItemStrings = [...itemStrings];
    newItemStrings.splice(index, 1);
    setItemStrings(newItemStrings);
    setSelected(0);
  };

  const duplicateView = async (name) => {
    setItemStrings([...itemStrings, name]);
    setSelected(itemStrings.length);
    await sleep(1);
    return true;
  };

  const onCreateNewView = async (value) => {
    await sleep(500);
    setItemStrings([...itemStrings, value]);
    setSelected(itemStrings.length);
    return true;
  };

  const handleStatusChange = useCallback((value) => setStatus(value), []);
  const handleTypeChange = useCallback((value) => setType(value), []);
  const handleFiltersQueryChange = useCallback(
    (value) => setQueryValue(value),
    []
  );
  const handleStatusRemove = useCallback(() => setStatus(undefined), []);
  const handleTypeRemove = useCallback(() => setType(undefined), []);
  const handleQueryValueRemove = useCallback(() => setQueryValue(""), []);
  const handleFiltersClearAll = useCallback(() => {
    handleStatusRemove();
    handleTypeRemove();
    handleQueryValueRemove();
  }, [handleStatusRemove, handleQueryValueRemove, handleTypeRemove]);

  const appliedFilters = [];
  if (tone && !isEmpty(tone)) {
    const key = "tone";
    appliedFilters.push({
      key,
      label: disambiguateLabel(key, tone),
      onRemove: handleStatusRemove,
    });
  }
  if (type && !isEmpty(type)) {
    const key = "type";
    appliedFilters.push({
      key,
      label: disambiguateLabel(key, type),
      onRemove: handleTypeRemove,
    });
  }

  const rowMarkup = products.map(
    (
      { id, product, price, tone, inventory, type, description, image },
      index
    ) => (
      <IndexTable.Row
        id={id}
        key={id}
        selected={selectedResources.includes(id)}
        position={index}
        selectionRange={{ startIndex: 0, endIndex: 2 }}
      >
        <IndexTable.Cell>
          {image ? (
            <img
              src={image}
              alt={"product thumbnail" + product}
              width={40}
              height={40}
            />
          ) : (
            <div>no image</div>
          )}
        </IndexTable.Cell>
        <IndexTable.Cell>{product}</IndexTable.Cell>
        <IndexTable.Cell>{price}</IndexTable.Cell>
        <IndexTable.Cell>{tone}</IndexTable.Cell>
        <IndexTable.Cell>{inventory}</IndexTable.Cell>
        <IndexTable.Cell>{type}</IndexTable.Cell>
        <IndexTable.Cell>{description}</IndexTable.Cell>
      </IndexTable.Row>
    )
  );

  const emptyStateMarkup = (
    <EmptySearchResult
      title={"No products yet"}
      description={"Try changing the filters or search term"}
      withIllustration
    />
  );

  const primaryAction =
    selected === 0
      ? {
          type: "save-as",
          onAction: onCreateNewView,
          disabled: false,
          loading: false,
        }
      : {
          type: "save",
          onAction: async () => {
            await sleep(1);
            return true;
          },
          disabled: false,
          loading: false,
        };

  const [show, setShow] = useState(true);
  const [description, setDescription] = useState("");
  const [isGenerating, setIsGenerating] = useState(false);

  const [active, setActive] = useState(false);

  const toggleActive = useCallback(() => setActive((active) => !active), []);

  const toastMarkup = active ? (
    <Toast content="Select only 1 product" onDismiss={toggleActive} />
  ) : null;

  const generate = () => {
    if (selectedResources.length !== 1) {
      toggleActive();
      return;
    }
    setIsGenerating(true);
    const response = fetch("/api/generate", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ selectedResources }),
    });
    response
      .then((response) => response.json())
      .then((data) => {
        setDescription(data.description);
        setShow(true);
        setIsGenerating(false);
      })
      .catch((error) => {
        console.error("Error:", error);
        setIsGenerating(false);
      });

    return true;
  };

  return (
    <Frame>
      {toastMarkup}
      <ProductUpdate
        active={show}
        setActive={setShow}
        description={description}
        productId={selectedResources[0] || 9581094469949}
      />
      <Page
        title={"Products"}
        primaryAction={{
          content: "Generate",
          onAction: generate,
          loading: isGenerating,
        }}
        fullWidth
      >
        <Card padding="0">
          <IndexFilters
            sortOptions={[
              {
                label: "Product",
                value: "product asc",
                directionLabel: "Ascending",
              },
              {
                label: "Product",
                value: "product desc",
                directionLabel: "Descending",
              },
              { label: "Status", value: "tone asc", directionLabel: "A-Z" },
              { label: "Status", value: "tone desc", directionLabel: "Z-A" },
              { label: "Type", value: "type asc", directionLabel: "A-Z" },
              { label: "Type", value: "type desc", directionLabel: "Z-A" },
              {
                label: "Description",
                value: "description asc",
                directionLabel: "Ascending",
              },
              {
                label: "Description",
                value: "description desc",
                directionLabel: "Descending",
              },
            ]}
            sortSelected={sortSelected}
            queryValue={queryValue}
            queryPlaceholder="Searching in all"
            onQueryChange={handleFiltersQueryChange}
            onQueryClear={() => {}}
            onSort={setSortSelected}
            primaryAction={primaryAction}
            cancelAction={{
              onAction: () => {},
              disabled: false,
              loading: false,
            }}
            tabs={itemStrings.map((item, index) => ({
              content: item,
              index,
              onAction: () => {},
              id: `${item}-${index}`,
              isLocked: index === 0,
              actions:
                index === 0
                  ? []
                  : [
                      {
                        type: "rename",
                        onAction: () => {},
                        onPrimaryAction: async (value) => {
                          const newItemsStrings = itemStrings.map(
                            (item, idx) => {
                              if (idx === index) {
                                return value;
                              }
                              return item;
                            }
                          );
                          await sleep(1);
                          setItemStrings(newItemsStrings);
                          return true;
                        },
                      },
                      {
                        type: "duplicate",
                        onPrimaryAction: async (name) => {
                          await sleep(1);
                          duplicateView(name);
                          return true;
                        },
                      },
                      {
                        type: "delete",
                        onPrimaryAction: async () => {
                          await sleep(1);
                          deleteView(index);
                          return true;
                        },
                      },
                    ],
            }))}
            selected={selected}
            onSelect={setSelected}
            canCreateNewView
            onCreateNewView={onCreateNewView}
            mode={mode}
            setMode={setMode}
            filters={[]}
            appliedFilters={appliedFilters}
            onClearAll={handleFiltersClearAll}
          />
          <IndexTable
            resourceName={resourceName}
            itemCount={products.length}
            selectedItemsCount={
              allResourcesSelected ? "All" : selectedResources.length
            }
            onSelectionChange={handleSelectionChange}
            emptyState={emptyStateMarkup}
            headings={[
              { title: "Thumbnail", hidden: true },
              { title: "Product" },
              { title: "Price" },
              { title: "Status" },
              { title: "Inventory" },
              { title: "Type" },
              { title: "Description" },
            ]}
          >
            {rowMarkup}
          </IndexTable>
        </Card>
      </Page>
    </Frame>
  );
};

export default Products;

Enter fullscreen mode Exit fullscreen mode
  1. create api route to get product in index.js

const getProducts = async (_req, res) => {
  try {
    const products = await shopify.api.rest.Product.all({
      session: res.locals.shopify.session,
    });
    res.status(200).send(products);
  } catch (error) {
    console.error(`Failed to fetch products: ${error.message}`);
    res.status(500).send({ error: error.message });
  }
};

app.get("/api/products", getProducts);

Enter fullscreen mode Exit fullscreen mode
  1. Update useFetch hook
import { useCallback } from 'react';
import { useQueryClient } from 'react-query';
import { useAuthenticatedFetch } from './useAuthenticatedFetch';

export const useFetch = () => {
    const fetch = useAuthenticatedFetch();
    const queryClient = useQueryClient();

    const get = useCallback(
        (url) => {
            return fetch(url, {
                method: 'GET',
                headers: { 'Content-Type': 'application/json' },
            })
                .then((r) => r.text())
                .then((text) => JSON.parse(text));
        },
        [fetch]
    );

    const post = useCallback(
        (url, body) =>
            fetch(url, {
                method: 'POST',
                body: JSON.stringify(body),
                headers: { 'Content-Type': 'application/json' },
            }).then((r) => r.json()),
        [fetch]
    );

    return {
        get,
        post,
        mutate: (key) => queryClient.invalidateQueries(key),
    };
};

Enter fullscreen mode Exit fullscreen mode
  1. Create services/product.js in frontend folder to handle product resource operations

import { useCallback, useState } from 'react';
import { useQuery, useQueryClient } from 'react-query';
import { useFetch } from '../hooks';

export const useProducts = () => {
    const { get } = useFetch();
    const { data, ...rest } = useQuery({
        queryFn: () => get('/api/products'),
        queryKey: ['products'],
        refetchOnWindowFocus: false,
    });

    return {
        data: data || null,
        ...rest,
    };
};

export const useProductUpdate = () => {
    const { post, mutate } = useFetch();
    const queryClient = useQueryClient();
    const [isLoading, setIsLoading] = useState(false);

    const update = useCallback(
        async (body) => {
            setIsLoading(true);
            const response = await post('/api/update', body);
            await mutate("products")
            setIsLoading(false);
            return response;
        }, [post, queryClient]);

    return {
        update,
        isLoading,
    };
}

Enter fullscreen mode Exit fullscreen mode
  1. In the products page we select a single product and send it to the api to generate the description. Let’s add a api endpoint for that. Add the following changes to index.js :

const generateDescription = async (_req, res) => {
  const { selectedResources } = _req.body;
  if (!selectedResources) {
    return res.status(400).send({ success: false, message: "No resources selected" });
  }

  try {
    const productId = selectedResources[0];
    const product = await shopify.api.rest.Product.find({
      session: res.locals.shopify.session,
      id: productId,
    });

    if (!product) {
      return res.status(400).send({ success: false, message: "Product not found" });
    }

    const prompt = `Generate a good description for the product ${product.title}, ${product.body_html}, ${product.product_type}, ${product.handle}, ${product.tags}`;
    const completion = await openai.chat.completions.create({
      messages: [{ role: "system", content: prompt }],
      model: "gpt-4o-mini",
    });

    const description = completion.choices[0]?.message.content;
    res.status(200).send({ success: true, description });
  } catch (error) {
    console.error(`Error generating description: ${error.message}`);
    res.status(500).send({ success: false, error: error.message });
  }
};

app.post("/api/generate", generateDescription);

Enter fullscreen mode Exit fullscreen mode

Here, we use a simple prompt based on some product fields to generate a better description. You can tweak the prompt with additional information to create more accurate descriptions.

  1. After generating the description, it returns to frontend for review and edit by show a modal with prompt output. Here’s the modal
import { Modal, TextField } from "@shopify/polaris";
import { useCallback } from "react";
import { useProductUpdate } from "../services/product";

export default function ProductUpdate({
  active,
  setActive,
  description,
  productId,
}) {
  if (!active && !description && !productId) {
    return null;
  }
  const handleChange = useCallback(() => setActive(!active), [active]);

  const { update, isLoading } = useProductUpdate();
  const handleUpdate = async () => {
    await update({ description, productId });
    handleChange();
  };

  return (
    <Modal
      open={active}
      onClose={handleChange}
      title="Update product description"
      primaryAction={{
        content: "Update",
        onAction: handleUpdate,
        loading: isLoading,
        disabled: isLoading,
      }}
      secondaryActions={[
        {
          content: "Cancel",
          onAction: handleChange,
        },
      ]}
    >
      <Modal.Section>
        <TextField
          label="Generated description ✨"
          value={description}
          onChange={handleChange}
          multiline={4}
          autoComplete="off"
        />
      </Modal.Section>
    </Modal>
  );
}

Enter fullscreen mode Exit fullscreen mode
  1. After reviewing / editing the description, we update the description in the store product with this endpoint:

const updateProductDescription = async (_req, res) => {
  const { description, productId } = _req.body;
  if (!description) {
    return res.status(400).send({ success: false, message: "Description is required" });
  }

  try {
    const product = new shopify.api.rest.Product({ session: res.locals.shopify.session });
    product.id = productId;
    product.body_html = description;
    await product.save({ update: true });

    res.status(200).send({ success: true, data: product });
  } catch (error) {
    console.error(`Error updating product description: ${error.message}`);
    res.status(500).send({ success: false, error: error.message });
  }
};
app.post("/api/update", updateProductDescription);

Enter fullscreen mode Exit fullscreen mode

Congratulations 🎉, you now have a useful app!

Shopify app demo
![helllofy--hellofy--Shopify-ezgif.com-video-to-gif-converter.gif]

7. Deploy the App to Fly.io

Fly.io is a great choice for deploying your Shopify app, providing a simple and efficient way to get your app online, but can you go with any other provider. Follow these steps to deploy your Shopify app to Fly.io.

Prerequisites

  • You have a Shopify app ready for deployment.
  • You have Docker installed on your machine.
  • You have Flyctl (Fly.io command line tool) installed. If not, you can install it here.

1. Create a Fly.io Account

  1. Go to Fly.io.
  2. Click on Sign Up and create an account.

2. Install Flyctl

  1. Follow the installation instructions for your operating system from the Flyctl installation guide.

3. Initialize Fly.io in Your Project

  1. Open your terminal.
  2. Navigate to your project directory.
  3. Run the following command to log in to Fly.io:

    flyctl auth login
    
    
  4. Initialize your Fly.io app:

    flyctl launch
    
    
- Follow the prompts to create a new app.
- Choose a region close to you or your target users.
- When asked if you want to deploy now, select **no**. We will configure Docker first.
Enter fullscreen mode Exit fullscreen mode

4. Create a database

There are many database options to choose from: Supabase, CockroachDB, Amazon RDS, or even managing your own on a VPS. For this example, since we're using Fly.io, we'll go with Fly.io Postgres. It's a great fit and integrates seamlessly with Fly.io's .

  1. Create a PostgreSQL Database

Create a PostgreSQL database instance using the Fly.io CLI:

flyctl postgres create --name your-db-name --region your-region
Enter fullscreen mode Exit fullscreen mode

2.Configure Database Access

Fly.io will provide you with a connection string and credentials for your PostgreSQL database. You can view these details by running:

flyctl postgres attach --app your-app-name your-db-name
Enter fullscreen mode Exit fullscreen mode

5. Configure Fly.io

  1. Open the fly.toml file generated by the flyctl launch command.
  2. Ensure the [[services]] section looks like this:

    [[services]]
      internal_port = 3000
      protocol = "tcp"
    
      [[services.ports]]
        handlers = ["http"]
        port = 80
    
      [[services.ports]]
        handlers = ["tls", "http"]
        port = 443
    
      [[services.tcp_checks]]
        interval = "15s"
        timeout = "2s"
        grace_period = "1s"
        restart_limit = 0
    
    
  3. Specify secrets for your Fly App

Required Secrets for Your App

To configure your app, you'll need the following secret values:

  • DATABASE_URL
  • OPENAI_API_KEY
  • SHOPIFY_API_KEY
  • SHOPIFY_API_SECRET
  • SCOPES

You can obtain the values for these variables from the previous setup steps.

Setting Secrets in Your Fly.io App

To set these secrets in your Fly.io app, follow these steps:

  1. Open your terminal and navigate to your project directory.
  2. Use the Fly.io CLI to set each secret by running the following commands:

    flyctl secrets set DATABASE_URL=your_database_url
    flyctl secrets set OPENAI_API_KEY=your_openai_api_key
    flyctl secrets set SHOPIFY_API_KEY=your_shopify_api_key
    flyctl secrets set SHOPIFY_API_SECRET=your_shopify_api_secret
    flyctl secrets set SCOPES=your_scopes
    
    

Replace your_database_url, your_openai_api_key, your_shopify_api_key, your_shopify_api_secret, and your_scopes with the actual values you obtained earlier

6. Deploy Your App

  1. In your terminal, run:

    flyctl deploy
    
  2. Fly.io will build and deploy your app. This may take a few minutes.

7. Access Your App

  1. Once the deployment is complete, Fly.io will provide a URL for your app.
  2. Open the URL in your browser to see your Shopify app running.

7. Connect Your App to Shopify

  1. Log in to your Shopify Partners dashboard.
  2. Go to Apps and find your app.
  3. In the App setup section, update the app URL to the Fly.io URL provided after deployment.
  4. Save the changes.

What's Next?

This app is just the beginning; there's a lot more you can do to enhance it. Here are some tasks to consider if you want to continue developing the app:

  • Add more functionalities
  • Implement user settings
  • Improve the prompting system
  • Enable multiple product generations
  • Clean up the project structure
  • Set up billing
  • Learn how to handle mandatory webhooks

Additional Tips

  • Deepen your understanding of the Shopify API. Shopify frequently updates its platform, so staying current is essential.
  • Learn more about building Shopify extensions.
  • Master Shopify billing.
  • Get familiar with Liquid.js.
  • Explore public app listing.

By focusing on these areas, you'll be well-equipped to create a robust and feature-rich Shopify app. Happy coding 🚀💻✨!

Get a full ready Shopify app boilerplate @ https://shopifast.dev

ShopiFast: A Shopify app boilerplate - starter kit -template
The source code for the app is available on github: https://github.com/adibmed/shopify-app-openai

Top comments (0)