Free Guide·25 min read·March 2026

How to Build Your First Shopify App in 2026

A complete beginner's guide — from understanding Shopify's app architecture to submitting your finished app to the App Store. No prior Shopify experience required.

Shopify powers over 4 million online stores worldwide, and its app ecosystem is one of the richest in e-commerce. Whether you want to build a tool for your own store, solve a niche merchant problem, or launch a SaaS product on the Shopify App Store, this guide walks you through every step.

By the end, you'll have a working Shopify app running on a development store, and you'll know exactly how to get it reviewed and published. Let's build.

1Step 1 of 5

Shopify App Architecture Basics

Before you write a single line of code, you need to understand how Shopify apps actually work. A Shopify app is not a WordPress plugin that lives inside the platform — it's an external web application that communicates with Shopify through APIs.

How Apps Connect to Shopify

Your app lives on your own server (or a serverless platform like Vercel or Fly.io). When a merchant installs your app, Shopify uses OAuth 2.0 to establish a secure connection. After authentication, your app can:

The Three Types of Shopify Apps

Public Apps

Listed on the Shopify App Store. Any merchant can install them. Must pass Shopify's review process.

Custom Apps

Built for a single store. Created directly in the Shopify admin. Great for client work and bespoke solutions.

Unlisted Apps

Distributed via a direct install link. Not on the App Store. Useful for B2B or invite-only apps.

Key Technologies in 2026

The Shopify development stack has evolved significantly. Here's what you'll be working with:

📋 Note

This guide focuses on building a public app using the official Remix template. The concepts apply to all app types — the main difference is distribution.
2Step 2 of 5

Setting Up Your Development Store

You need a Shopify store to test your app against. Shopify provides free development stores for this exact purpose — they have full functionality but can't process real transactions.

Step-by-Step: Create a Dev Store

  1. Join the Shopify Partner Program — go to partners.shopify.com and sign up. It's free and gives you access to unlimited development stores.
  2. Create a development store — from your Partner Dashboard, click Stores → Add store → Development store. Choose "Create a store to test and build".
  3. Configure the store — give it a name (e.g., my-app-test-store), select a purpose, and pick your country/region.
  4. Add test data — populate your store with sample products so you have realistic data to work with. You can use Shopify's built-in sample data or import a CSV.

💡 Pro Tip

Create at least two dev stores — one for active development and one for testing fresh installs. This helps catch bugs in your onboarding flow.

Enable Developer Preview

Development stores let you toggle "developer preview" for upcoming APIs and features. Go to Settings → Developer preview in your dev store to enable previews for features you want to build against.

3Step 3 of 5

App Scaffolding with Shopify CLI

The Shopify CLI is the fastest way to go from zero to a running app. It generates the full project structure, handles OAuth, sets up your database, and even creates a tunnel so Shopify can reach your local server.

Prerequisites

Initialize Your App

Terminal
# Install Shopify CLI globally (if you haven't)
npm install -g @shopify/cli @shopify/app

# Create a new app
shopify app init

# You'll be prompted for:
#   → App name: my-first-shopify-app
#   → Template: Remix (recommended)
#   → Package manager: npm / yarn / pnpm

The CLI generates a complete project structure:

Project Structure
my-first-shopify-app/
├── app/
│   ├── routes/
│   │   ├── app._index.tsx      # Main app page
│   │   ├── app.tsx             # App layout with nav
│   │   ├── auth.$.tsx          # OAuth callback
│   │   └── webhooks.tsx        # Webhook handler
│   ├── shopify.server.ts       # Shopify API client setup
│   └── db.server.ts            # Database (Prisma) setup
├── prisma/
│   └── schema.prisma           # Database schema
├── extensions/                  # Theme/checkout extensions
├── shopify.app.toml            # App configuration
├── remix.config.js
└── package.json

Start the Dev Server

Terminal
cd my-first-shopify-app

# Start the development server
shopify app dev

# This command:
#   1. Starts your Remix dev server
#   2. Creates a Cloudflare tunnel (so Shopify can reach localhost)
#   3. Opens your browser to install the app on your dev store
#   4. Hot-reloads on file changes

When you run shopify app dev for the first time, the CLI will ask you to:

  1. Log in with your Partner account
  2. Create or select an app in your Partner Dashboard
  3. Select a development store to install on

💡 Pro Tip

The Cloudflare tunnel URL changes every time you restart shopify app dev. The CLI updates your app's config automatically, so you don't need to manually change URLs in the Partner Dashboard.

Understanding the Key Files

Let's look at the files you'll work with most:

shopify.app.toml — your app's configuration. Defines API scopes (permissions), webhooks, and extension settings:

shopify.app.toml
name = "my-first-shopify-app"
client_id = "your-client-id"

[access_scopes]
scopes = "read_products,write_products"

[webhooks]
api_version = "2026-01"

  [[webhooks.subscriptions]]
  topics = ["products/update"]
  uri = "/webhooks"

app/shopify.server.ts — initializes the Shopify API client. This is where authentication and API configuration lives:

app/shopify.server.ts
import "@shopify/shopify-app-remix/adapters/node";
import { AppDistribution, shopifyApp } from "@shopify/shopify-app-remix/server";

const shopify = shopifyApp({
  apiKey: process.env.SHOPIFY_API_KEY,
  apiSecretKey: process.env.SHOPIFY_API_SECRET || "",
  appUrl: process.env.SHOPIFY_APP_URL || "",
  scopes: process.env.SCOPES?.split(","),
  distribution: AppDistribution.AppStore,
  // ... session storage, webhooks, etc.
});

export default shopify;
4Step 4 of 5

Building a Simple App Feature

Let's build something real: a product tagger that automatically adds tags to products based on their price range. This feature teaches you the core patterns you'll use in any Shopify app — reading data from the Admin API, displaying it with Polaris, and writing data back.

Step 4a: Define Your Data Loader

In Remix, you fetch server-side data with a loader function. Let's create a route that loads products from the store:

app/routes/app.tagger.tsx
import { json } from "@remix-run/node";
import type { LoaderFunctionArgs } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import shopify from "../shopify.server";

export const loader = async ({ request }: LoaderFunctionArgs) => {
  // authenticate() validates the session and returns an admin API client
  const { admin } = await shopify.authenticate.admin(request);

  // Query products using the GraphQL Admin API
  const response = await admin.graphql(`
    {
      products(first: 20) {
        edges {
          node {
            id
            title
            variants(first: 1) {
              edges {
                node {
                  price
                }
              }
            }
            tags
          }
        }
      }
    }
  `);

  const data = await response.json();
  return json({
    products: data.data.products.edges.map((edge: any) => ({
      id: edge.node.id,
      title: edge.node.title,
      price: parseFloat(edge.node.variants.edges[0]?.node.price || "0"),
      tags: edge.node.tags,
    })),
  });
};

Step 4b: Build the UI with Polaris

Polaris components make your app look native inside the Shopify admin. Here's the UI for our product tagger:

app/routes/app.tagger.tsx (continued)
import {
  Page,
  Layout,
  Card,
  DataTable,
  Button,
  Banner,
  Badge,
} from "@shopify/polaris";

export default function ProductTagger() {
  const { products } = useLoaderData<typeof loader>();
  const fetcher = useFetcher();

  // Determine which tag each product should get
  const getRecommendedTag = (price: number) => {
    if (price >= 100) return "premium";
    if (price >= 50) return "mid-range";
    return "budget";
  };

  const rows = products.map((product: any) => [
    product.title,
    `$${product.price.toFixed(2)}`,
    product.tags.join(", ") || "No tags",
    <Badge key={product.id} tone="info">
      {getRecommendedTag(product.price)}
    </Badge>,
    <fetcher.Form method="post" key={product.id}>
      <input type="hidden" name="productId" value={product.id} />
      <input
        type="hidden"
        name="tag"
        value={getRecommendedTag(product.price)}
      />
      <Button submit size="slim">
        Apply Tag
      </Button>
    </fetcher.Form>,
  ]);

  return (
    <Page title="Product Tagger">
      <Layout>
        <Layout.Section>
          <Banner tone="info">
            This tool suggests price-based tags for your products.
            Click "Apply Tag" to add the recommended tag.
          </Banner>
        </Layout.Section>
        <Layout.Section>
          <Card>
            <DataTable
              columnContentTypes={["text", "numeric", "text", "text", "text"]}
              headings={["Product", "Price", "Current Tags", "Suggested", ""]}
              rows={rows}
            />
          </Card>
        </Layout.Section>
      </Layout>
    </Page>
  );
}

Step 4c: Handle the Form Submission

Add an action function to handle the tag mutation:

app/routes/app.tagger.tsx (action)
import type { ActionFunctionArgs } from "@remix-run/node";

export const action = async ({ request }: ActionFunctionArgs) => {
  const { admin } = await shopify.authenticate.admin(request);
  const formData = await request.formData();

  const productId = formData.get("productId") as string;
  const tag = formData.get("tag") as string;

  // Use the GraphQL mutation to add tags
  const response = await admin.graphql(`
    mutation addTags($id: ID!, $tags: [String!]!) {
      tagsAdd(id: $id, tags: $tags) {
        node {
          ... on Product {
            id
            tags
          }
        }
        userErrors {
          message
        }
      }
    }
  `, {
    variables: { id: productId, tags: [tag] },
  });

  const data = await response.json();

  if (data.data.tagsAdd.userErrors.length > 0) {
    return json({ error: data.data.tagsAdd.userErrors[0].message });
  }

  return json({ success: true });
};

Step 4d: Add Navigation

Register your new page in the app's navigation sidebar. Open app/routes/app.tsx and add a nav link:

app/routes/app.tsx
// In your app layout's navigation:
<NavMenu>
  <a href="/app" rel="home">Home</a>
  <a href="/app/tagger">Product Tagger</a>
</NavMenu>

⚠️ Heads Up

Always request only the minimum scopes your app needs. For our tagger, we need read_products and write_products. Requesting unnecessary scopes will get your app rejected in review.
5Step 5 of 5

Testing & Submitting to the App Store

You've got a working feature. Now let's make sure it's production-ready and get it into the Shopify App Store.

Testing Checklist

  • 1Fresh install flow — uninstall and reinstall the app. Does OAuth work smoothly?
  • 2Multiple stores — test on at least 2 dev stores with different configurations
  • 3Error handling — what happens with API rate limits? Network failures? Empty stores?
  • 4Responsive layout — test your embedded app on different screen sizes
  • 5Performance — loading data for stores with 1000+ products? Add pagination
  • 6Webhook reliability — use the CLI to trigger test webhooks
  • 7Uninstall cleanup — handle the app/uninstalled webhook to clean up store data

Deploy Your App

Before submitting, you need to deploy your app to a production server. Popular hosting options:

Fly.io

Simple deployment with the Shopify CLI. Run `shopify app deploy` for one-command deployment.

Heroku

Classic PaaS option. Good if you're already familiar with it. Add the Heroku Postgres addon for your DB.

Railway

Modern PaaS with automatic deployments from GitHub. Includes built-in Postgres.

Vercel + PlanetScale

Serverless option. Great for apps with low to moderate traffic. Requires some config adjustments.

Terminal
# Deploy to Fly.io (simplest option)
shopify app deploy

# Or build and deploy manually
npm run build
# Then deploy to your hosting provider of choice

Prepare Your App Store Listing

A great listing is just as important as great code. You'll need:

Submit for Review

  1. Go to partners.shopify.com → Apps → Your App → Distribution
  2. Select "Shopify App Store" as your distribution method
  3. Fill in all listing fields (name, description, screenshots, etc.)
  4. Complete the App submission checklist — Shopify provides this to make sure you haven't missed anything
  5. Click "Submit for review"

📋 Note

Review typically takes 5–10 business days. Common rejection reasons include: requesting unnecessary API scopes, missing error states, and poor loading performance. Fix these before submitting to save time.

Common Rejection Reasons (and How to Avoid Them)

Excessive API scopes

Only request scopes your app actually uses. Remove any leftover scopes from development.

Missing error handling

Show user-friendly messages when API calls fail. Handle rate limits with retries.

Slow loading times

Paginate large datasets. Use skeleton loading states. Optimize GraphQL queries.

No uninstall webhook

Handle `app/uninstalled` to clean up session data and respect merchant privacy.

Poor mobile experience

Test your embedded UI at narrow widths. Polaris components are responsive by default — don't fight them.

What's Next?

You now have the foundation to build real Shopify apps. Here are natural next steps to level up:

Want hands-on help?

Building your first app is exciting — and sometimes confusing. If you want a senior Shopify developer to pair-program with you, review your code, or help you debug that one weird OAuth issue, we're here.

Start with a $9 Quick Question, move to a $19 code review, or book a $49 live session with an experienced Shopify developer on ShopCraft. Pick the depth of help you need.

Get Expert Help — From $9