Skip to main content

Your First Shopify App

In this lesson, you will go from zero to a running Shopify app installed on your development store. We will use the Shopify CLI to scaffold the project, understand the file structure, start the development server, and then use Claude Code to build your first feature.

Scaffolding With shopify app init

The Shopify CLI generates a complete app project with authentication, database, and UI already configured. Run the following:

# Create a new Shopify app
cd ~/shopify-projects
shopify app init

# The CLI will prompt you:
# ? Your app project name: masterclass-app
# ? Get started building your app:
# > Start with Remix (recommended)
# Start with a blank extension-only app
# Start with a template

Select Start with Remix -- this is Shopify's recommended framework and is what we will use throughout the course.

# After scaffolding completes
cd masterclass-app
Why Remix?

Shopify chose Remix as its recommended app framework because of its server-side rendering model, nested routing, and excellent data loading patterns. Remix actions and loaders map naturally to Shopify's OAuth flow and API calls. The Shopify Remix template includes authentication, session management, and App Bridge integration out of the box.

Understanding the Project Structure

The scaffolded project has a specific structure that is important to understand. Every directory serves a clear purpose:

masterclass-app/
├── app/ # Remix application
│ ├── routes/ # Page routes (file-based routing)
│ │ ├── app._index.jsx # Main app page (after auth)
│ │ ├── app.jsx # App layout with nav
│ │ ├── auth.$.jsx # OAuth callback handler
│ │ ├── auth.login/ # Login page
│ │ │ ├── route.jsx
│ │ │ └── login.css
│ │ └── webhooks.jsx # Webhook handler
│ ├── shopify.server.js # Shopify API client setup
│ ├── db.server.js # Database connection (SQLite)
│ └── entry.server.jsx # Remix server entry
├── extensions/ # Shopify extensions (UI, Functions, etc.)
├── prisma/
│ └── schema.prisma # Database schema
├── public/ # Static assets
├── shopify.app.toml # App configuration
├── shopify.web.toml # Web server configuration
├── package.json
├── remix.config.js
└── .env # Environment variables (auto-generated)

Key Files Explained

shopify.app.toml -- The app manifest. This defines your app's name, scopes (permissions), and extension configuration:

name = "masterclass-app"
client_id = "your-client-id"
application_url = "https://your-tunnel.trycloudflare.com"
embedded = true

[access_scopes]
scopes = "write_products,read_orders"

[webhooks]
api_version = "2026-04"

app/shopify.server.js -- The Shopify API client. This file sets up authentication and provides methods for making API calls:

import "@shopify/shopify-app-remix/adapters/node";
import {
ApiVersion,
AppDistribution,
shopifyApp,
} from "@shopify/shopify-app-remix/server";
import { PrismaSessionStorage } from "@shopify/shopify-app-session-storage-prisma";
import prisma from "./db.server";

const shopify = shopifyApp({
apiKey: process.env.SHOPIFY_API_KEY,
apiSecretKey: process.env.SHOPIFY_API_SECRET || "",
apiVersion: ApiVersion.April26,
scopes: process.env.SCOPES?.split(","),
appUrl: process.env.SHOPIFY_APP_URL || "",
authPathPrefix: "/auth",
sessionStorage: new PrismaSessionStorage(prisma),
distribution: AppDistribution.AppStore,
future: {
unstable_newEmbeddedAuthStrategy: true,
},
});

export default shopify;
export const apiVersion = ApiVersion.April26;
export const addDocumentResponseHeaders = shopify.addDocumentResponseHeaders;
export const authenticate = shopify.authenticate;
export const unauthenticated = shopify.unauthenticated;
export const login = shopify.login;
export const registerWebhooks = shopify.registerWebhooks;
export const sessionStorage = shopify.sessionStorage;

app/routes/app._index.jsx -- The main page of your app, rendered inside Shopify Admin as an embedded app.

Running the Development Server

Start your app with the Shopify CLI dev command:

shopify app dev

This single command does a remarkable amount of work:

The CLI will:

  1. Start a Remix dev server on localhost:3000
  2. Create a Cloudflare tunnel for HTTPS access
  3. Update your app's URL in the Partner Dashboard
  4. Open your browser to install the app on your dev store
First Run Takes Longer

On first run, the CLI creates database tables, generates OAuth credentials, and sets up the tunnel. Subsequent runs are much faster.

Installing on Your Dev Store

When the CLI opens your browser, you will see a Shopify authorization screen. Click Install app to grant the requested permissions. After installation, your app's main page appears embedded inside the Shopify Admin.

You should see a page with the Shopify Polaris UI that says "Nice work on building a Shopify app" with some sample content.

Building Your First Feature With Claude Code

Now comes the exciting part -- using Claude Code to build a real feature. We are going to add a product count dashboard that shows the total number of products in the store along with their status breakdown.

Open Claude Code in your project directory:

cd ~/shopify-projects/masterclass-app
claude

Give Claude Code this prompt:

Update the main app page (app/routes/app._index.jsx) to show a product
dashboard. Use the Shopify Admin GraphQL API to fetch the total number
of products and break them down by status (active, draft, archived).
Display the data using Polaris Cards and Layout components. Include a
loading state.

Claude Code will:

  1. Read the existing app._index.jsx file to understand the current structure
  2. Write a Remix loader function that calls the Shopify Admin API
  3. Use a GraphQL query to fetch product counts by status
  4. Update the JSX to render Polaris Card, Layout, and Text components
  5. Add loading and error states

Here is what the resulting code looks like:

import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import {
Page,
Layout,
Card,
BlockStack,
Text,
InlineGrid,
} from "@shopify/polaris";
import { authenticate } from "../shopify.server";

export const loader = async ({ request }) => {
const { admin } = await authenticate.admin(request);

const response = await admin.graphql(`
{
activeCount: productsCount(query: "status:active") {
count
}
draftCount: productsCount(query: "status:draft") {
count
}
archivedCount: productsCount(query: "status:archived") {
count
}
totalCount: productsCount {
count
}
}
`);

const data = await response.json();

return json({
products: {
total: data.data.totalCount.count,
active: data.data.activeCount.count,
draft: data.data.draftCount.count,
archived: data.data.archivedCount.count,
},
});
};

export default function Index() {
const { products } = useLoaderData();

return (
<Page title="Product Dashboard">
<Layout>
<Layout.Section>
<InlineGrid columns={4} gap="400">
<Card>
<BlockStack gap="200">
<Text variant="headingSm">Total Products</Text>
<Text variant="heading2xl">{products.total}</Text>
</BlockStack>
</Card>
<Card>
<BlockStack gap="200">
<Text variant="headingSm">Active</Text>
<Text variant="heading2xl" tone="success">
{products.active}
</Text>
</BlockStack>
</Card>
<Card>
<BlockStack gap="200">
<Text variant="headingSm">Draft</Text>
<Text variant="heading2xl" tone="caution">
{products.draft}
</Text>
</BlockStack>
</Card>
<Card>
<BlockStack gap="200">
<Text variant="headingSm">Archived</Text>
<Text variant="heading2xl" tone="subdued">
{products.archived}
</Text>
</BlockStack>
</Card>
</InlineGrid>
</Layout.Section>
</Layout>
</Page>
);
}

Save the file and check your browser -- Remix hot-reloads automatically, so you should see your product dashboard appear.

The Agentic Advantage

Notice what just happened. You described a feature in plain English, and Claude Code wrote the GraphQL query, the Remix loader, and the Polaris UI. It knew to use authenticate.admin(request) because it read your existing shopify.server.js file. It knew to use productsCount because it understands the Shopify Admin API. This is agentic coding in action.

Checkpoint

Before moving on, verify:

  • shopify app dev starts without errors
  • Your app is installed on your development store
  • The product dashboard shows real data from your store
  • You have successfully used Claude Code to modify your app
Common Issues

"App not loaded" error: Make sure you are using the Cloudflare tunnel URL, not localhost. The tunnel is created automatically by shopify app dev.

"Access denied" error: Check that your app's scopes in shopify.app.toml include read_products. Run shopify app dev --reset to re-authenticate.

Empty product counts: If your dev store has no products, go to the Shopify Admin and click "Add product" to create a few test products, or use the bulk import feature.

What You Learned

In this lesson you:

  1. Scaffolded a Shopify Remix app using the Shopify CLI
  2. Understood the project structure and key configuration files
  3. Ran the development server with tunneling and hot reload
  4. Installed the app on your development store
  5. Used Claude Code to build a product dashboard feature

In the next lesson, we will explore the Shopify Partner ecosystem and understand the business side of Shopify development.