# App Development Reference Expert guidance for building custom Shopify apps using Shopify CLI, modern frameworks, and best practices. ## Core Capabilities ### 1. Shopify CLI Setup Install and configure Shopify CLI for app development. **Install Shopify CLI:** ```bash # Using npm npm install -g @shopify/cli @shopify/app # Using Homebrew (macOS) brew tap shopify/shopify brew install shopify-cli # Verify installation shopify version ``` **Create New App:** ```bash # Create app with Node.js/React shopify app init # Choose template: # - React Router 7 (recommended) # - Node.js + React # - PHP # - Ruby # App structure created: my-app/ ├── app/ # React Router app routes ├── extensions/ # App extensions ├── shopify.app.toml # App configuration ├── package.json └── README.md ``` **Note:** As of Hydrogen 2025.5.0+, Shopify templates default to React Router 7 instead of Remix. **App Configuration (shopify.app.toml):** ```toml # This file stores app configuration name = "my-app" client_id = "your-client-id" application_url = "https://your-app.com" embedded = true [access_scopes] # API access scopes scopes = "write_products,read_orders,read_customers" [auth] redirect_urls = [ "https://your-app.com/auth/callback", "https://your-app.com/auth/shopify/callback" ] [webhooks] api_version = "2026-01" [[webhooks.subscriptions]] topics = ["products/create", "products/update"] uri = "/webhooks" ``` ### 2. Development Workflow **Start Development Server:** ```bash # Start dev server with tunnelling shopify app dev # Server starts with: # - Local development URL: http://localhost:3000 # - Public tunnel URL: https://random-subdomain.ngrok.io # - App installed in development store ``` **Deploy App:** ```bash # Deploy to production shopify app deploy # Generate app version and deploy extensions ``` **Environment Variables (.env):** ```bash SHOPIFY_API_KEY=your_api_key SHOPIFY_API_SECRET=your_api_secret SCOPES=write_products,read_orders HOST=your-app-domain.com SHOPIFY_APP_URL=https://your-app.com DATABASE_URL=postgresql://... ``` ### 3. App Architecture (React Router 7) Modern Shopify app using React Router 7 framework. **app/routes/app._index.jsx (Home Page):** ```javascript import { useLoaderData } from "react-router"; import { authenticate } from "../shopify.server"; import { Page, Layout, Card, DataTable, Button, } from "@shopify/polaris"; export async function loader({ request }) { const { admin, session } = await authenticate.admin(request); // Fetch products using GraphQL const response = await admin.graphql(` query { products(first: 10) { edges { node { id title handle status } } } } `); const { data } = await response.json(); return { products: data.products.edges.map(e => e.node), shop: session.shop, }; } export default function Index() { const { products, shop } = useLoaderData(); const rows = products.map((product) => [ product.title, product.handle, product.status, ]); return ( ); } ``` **app/routes/app.product.$id.jsx (Product Detail):** ```javascript import { data } from "react-router"; import { useLoaderData, useSubmit } from "react-router"; import { authenticate } from "../shopify.server"; import { Page, Layout, Card, Form, FormLayout, TextField, Button, } from "@shopify/polaris"; import { useState } from "react"; export async function loader({ request, params }) { const { admin } = await authenticate.admin(request); const response = await admin.graphql(` query GetProduct($id: ID!) { product(id: $id) { id title description status vendor } } `, { variables: { id: `gid://shopify/Product/${params.id}` }, }); const { data: responseData } = await response.json(); return data({ product: responseData.product }); } export async function action({ request, params }) { const { admin } = await authenticate.admin(request); const formData = await request.formData(); const title = formData.get("title"); const description = formData.get("description"); const response = await admin.graphql(` mutation UpdateProduct($input: ProductInput!) { productUpdate(input: $input) { product { id title } userErrors { field message } } } `, { variables: { input: { id: `gid://shopify/Product/${params.id}`, title, description, }, }, }); const { data: responseData } = await response.json(); if (responseData.productUpdate.userErrors.length > 0) { return data({ errors: responseData.productUpdate.userErrors }, { status: 400 }); } return data({ success: true }); } export default function ProductDetail() { const { product } = useLoaderData(); const submit = useSubmit(); const [title, setTitle] = useState(product.title); const [description, setDescription] = useState(product.description); const handleSubmit = () => { const formData = new FormData(); formData.append("title", title); formData.append("description", description); submit(formData, { method: "post" }); }; return ( ); } ``` ### 4. App Extensions Extend Shopify functionality with various extension types. **Admin Action Extension:** Create button in admin product page: ```bash shopify app generate extension # Choose: Admin action # Name: Export Product ``` **extensions/export-product/src/index.jsx:** ```javascript import { extend, AdminAction } from "@shopify/admin-ui-extensions"; extend("Admin::Product::SubscriptionAction", (root, { data }) => { const { id, title } = data.selected[0]; const button = root.createComponent(AdminAction, { title: "Export Product", onPress: async () => { // Call your app API const response = await fetch("/api/export", { method: "POST", body: JSON.stringify({ productId: id }), headers: { "Content-Type": "application/json" }, }); if (response.ok) { root.toast.show("Product exported successfully!"); } else { root.toast.show("Export failed", { isError: true }); } }, }); root.append(button); }); ``` **Theme App Extension:** Add app block to themes: ```bash shopify app generate extension # Choose: Theme app extension # Name: Product Reviews ``` **extensions/product-reviews/blocks/reviews.liquid:** ```liquid {% schema %} { "name": "Product Reviews", "target": "section", "settings": [ { "type": "text", "id": "heading", "label": "Heading", "default": "Customer Reviews" }, { "type": "range", "id": "reviews_to_show", "label": "Reviews to Show", "min": 1, "max": 10, "default": 5 } ] } {% endschema %}

{{ block.settings.heading }}

{% comment %} Fetch reviews from your app API {% endcomment %}
{% stylesheet %} .product-reviews { padding: 2rem; } .review { margin-bottom: 1.5rem; padding-bottom: 1.5rem; border-bottom: 1px solid #eee; } .rating { color: #ffa500; margin-bottom: 0.5rem; } {% endstylesheet %} ``` ### 5. Webhooks in Apps Handle Shopify events in your app. **app/routes/webhooks.jsx:** ```javascript import { authenticate } from "../shopify.server"; import db from "../db.server"; export async function action({ request }) { const { topic, shop, session, admin, payload } = await authenticate.webhook(request); console.log(`Webhook received: ${topic} from ${shop}`); switch (topic) { case "APP_UNINSTALLED": // Clean up app data await db.session.deleteMany({ where: { shop } }); break; case "PRODUCTS_CREATE": // Handle new product console.log("New product created:", payload.id, payload.title); await handleProductCreated(payload); break; case "PRODUCTS_UPDATE": // Handle product update console.log("Product updated:", payload.id); await handleProductUpdated(payload); break; case "ORDERS_CREATE": // Handle new order console.log("New order:", payload.id, payload.email); await handleOrderCreated(payload); break; case "CUSTOMERS_CREATE": // Handle new customer await handleCustomerCreated(payload); break; default: console.log("Unhandled webhook topic:", topic); } return new Response("OK", { status: 200 }); } async function handleProductCreated(product) { // Process new product await db.product.create({ data: { shopifyId: product.id, title: product.title, handle: product.handle, }, }); } async function handleOrderCreated(order) { // Send email notification, update inventory, etc. console.log(`Order ${order.id} received for ${order.email}`); } ``` **Register Webhooks (app/shopify.server.js):** ```javascript import "@shopify/shopify-app-remix/adapters/node"; import { ApiVersion, AppDistribution, shopifyApp, DeliveryMethod, } from "@shopify/shopify-app-remix/server"; const shopify = shopifyApp({ apiKey: process.env.SHOPIFY_API_KEY, apiSecretKey: process.env.SHOPIFY_API_SECRET, scopes: process.env.SCOPES?.split(","), appUrl: process.env.SHOPIFY_APP_URL, authPathPrefix: "/auth", sessionStorage: new SQLiteSessionStorage(), distribution: AppDistribution.AppStore, apiVersion: ApiVersion.January26, webhooks: { APP_UNINSTALLED: { deliveryMethod: DeliveryMethod.Http, callbackUrl: "/webhooks", }, PRODUCTS_CREATE: { deliveryMethod: DeliveryMethod.Http, callbackUrl: "/webhooks", }, PRODUCTS_UPDATE: { deliveryMethod: DeliveryMethod.Http, callbackUrl: "/webhooks", }, ORDERS_CREATE: { deliveryMethod: DeliveryMethod.Http, callbackUrl: "/webhooks", }, }, }); export default shopify; export const authenticate = shopify.authenticate; ``` ### 6. App Proxy Create custom storefront routes that access your app. **Setup in Partner Dashboard:** ``` Subpath prefix: apps Subpath: reviews Proxy URL: https://your-app.com/api/proxy ``` **Result:** ``` https://store.com/apps/reviews → proxies to → https://your-app.com/api/proxy ``` **Handle Proxy Requests (app/routes/api.proxy.jsx):** ```javascript import { data } from "react-router"; export async function loader({ request }) { const url = new URL(request.url); // Verify proxy request const signature = url.searchParams.get("signature"); const shop = url.searchParams.get("shop"); if (!verifyProxySignature(signature, request)) { return data({ error: "Invalid signature" }, { status: 401 }); } // Handle different paths const path = url.searchParams.get("path_prefix"); if (path === "/apps/reviews/product") { const productId = url.searchParams.get("product_id"); const reviews = await getProductReviews(productId); return data({ reviews }); } return data({ message: "App Proxy" }); } function verifyProxySignature(signature, request) { // Verify HMAC signature // Implementation depends on your setup return true; } ``` ### 7. Polaris UI Components Use Shopify's design system for consistent admin UI. **Common Components:** ```javascript import { Page, Layout, Card, Button, TextField, Select, Checkbox, Badge, Banner, DataTable, Modal, Toast, Frame, } from "@shopify/polaris"; export default function MyPage() { return (