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
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:
- Start a Remix dev server on
localhost:3000 - Create a Cloudflare tunnel for HTTPS access
- Update your app's URL in the Partner Dashboard
- Open your browser to install the app on your dev store
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:
- Read the existing
app._index.jsxfile to understand the current structure - Write a Remix
loaderfunction that calls the Shopify Admin API - Use a GraphQL query to fetch product counts by status
- Update the JSX to render Polaris
Card,Layout, andTextcomponents - 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.
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 devstarts 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
"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:
- Scaffolded a Shopify Remix app using the Shopify CLI
- Understood the project structure and key configuration files
- Ran the development server with tunneling and hot reload
- Installed the app on your development store
- 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.