Advanced Shopify App Development: Building Custom Solutions
Building custom Shopify apps opens up endless possibilities for extending store functionality. Whether you're creating internal tools or public apps for the Shopify App Store, understanding the fundamentals is crucial.
Understanding Shopify App Architecture
Shopify apps are web applications that integrate with Shopify stores using APIs and webhooks.
Types of Shopify Apps
- Public Apps - Available in the Shopify App Store
- Custom Apps - Built for specific stores
- Private Apps - Legacy authentication method (deprecated)
Modern App Stack
Frontend (React/Polaris)
↓
Backend (Node.js/Express)
↓
Shopify APIs (Admin API, Storefront API)
↓
Database (PostgreSQL/MongoDB)
Setting Up Your Development Environment
Start with the Shopify CLI for the fastest setup.
Initialize a New App
# Install Shopify CLI
npm install -g @shopify/cli @shopify/app
# Create new app
npm init @shopify/app@latest
# Choose your stack
? What type of app are you building?
> Node.js with React
# Start development server
npm run devProject Structure
my-shopify-app/
├── web/
│ ├── frontend/ # React frontend
│ │ ├── components/
│ │ ├── pages/
│ │ └── App.jsx
│ ├── backend/ # Node.js backend
│ │ ├── routes/
│ │ ├── middleware/
│ │ └── index.js
│ └── shopify.js # Shopify configuration
├── extensions/ # App extensions
└── shopify.app.toml # App configuration
OAuth Authentication Flow
Implementing secure OAuth authentication is the first step.
OAuth Implementation
// web/backend/middleware/auth.js
import { Shopify } from '@shopify/shopify-api';
export async function verifyRequest(req, res, next) {
const session = await Shopify.Utils.loadCurrentSession(
req,
res,
app.get('use-online-tokens')
);
if (!session) {
return res.redirect('/auth');
}
try {
// Verify session is still valid
const client = new Shopify.Clients.Graphql(
session.shop,
session.accessToken
);
await client.query({
data: `{ shop { name } }`,
});
req.session = session;
next();
} catch (error) {
console.error('Session validation failed:', error);
return res.redirect('/auth');
}
}Session Storage
// web/backend/utils/session-storage.js
import { Session } from '@shopify/shopify-api';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export const sessionStorage = {
async storeSession(session) {
await prisma.session.upsert({
where: { id: session.id },
update: {
shop: session.shop,
state: session.state,
isOnline: session.isOnline,
accessToken: session.accessToken,
scope: session.scope,
},
create: {
id: session.id,
shop: session.shop,
state: session.state,
isOnline: session.isOnline,
accessToken: session.accessToken,
scope: session.scope,
},
});
return true;
},
async loadSession(id) {
const sessionData = await prisma.session.findUnique({
where: { id },
});
if (!sessionData) return undefined;
return new Session({
id: sessionData.id,
shop: sessionData.shop,
state: sessionData.state,
isOnline: sessionData.isOnline,
accessToken: sessionData.accessToken,
scope: sessionData.scope,
});
},
async deleteSession(id) {
await prisma.session.delete({
where: { id },
});
return true;
},
};Working with Shopify Admin API
The Admin API is your primary interface for store data manipulation.
GraphQL Queries
// web/backend/services/products.js
export async function getProducts(session, first = 10) {
const client = new Shopify.Clients.Graphql(
session.shop,
session.accessToken
);
const query = `
query GetProducts($first: Int!) {
products(first: $first) {
edges {
node {
id
title
handle
status
totalInventory
variants(first: 5) {
edges {
node {
id
title
price
inventoryQuantity
}
}
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
`;
const response = await client.query({
data: {
query,
variables: { first },
},
});
return response.body.data.products;
}GraphQL Mutations
// web/backend/services/products.js
export async function createProduct(session, productData) {
const client = new Shopify.Clients.Graphql(
session.shop,
session.accessToken
);
const mutation = `
mutation CreateProduct($input: ProductInput!) {
productCreate(input: $input) {
product {
id
title
handle
}
userErrors {
field
message
}
}
}
`;
const response = await client.query({
data: {
query: mutation,
variables: {
input: {
title: productData.title,
descriptionHtml: productData.description,
vendor: productData.vendor,
productType: productData.productType,
tags: productData.tags,
},
},
},
});
const result = response.body.data.productCreate;
if (result.userErrors.length > 0) {
throw new Error(result.userErrors[0].message);
}
return result.product;
}Implementing Webhooks
Webhooks allow your app to react to store events in real-time.
Webhook Registration
// web/backend/webhooks/register.js
import { Shopify } from '@shopify/shopify-api';
export async function registerWebhooks(session) {
const webhooks = [
{
topic: 'PRODUCTS_CREATE',
path: '/webhooks/products/create',
},
{
topic: 'PRODUCTS_UPDATE',
path: '/webhooks/products/update',
},
{
topic: 'ORDERS_CREATE',
path: '/webhooks/orders/create',
},
{
topic: 'APP_UNINSTALLED',
path: '/webhooks/app/uninstalled',
},
];
for (const webhook of webhooks) {
const response = await Shopify.Webhooks.Registry.register({
shop: session.shop,
accessToken: session.accessToken,
topic: webhook.topic,
path: webhook.path,
webhookHandler: async (topic, shop, body) => {
// Webhook handler logic
},
});
if (!response.success) {
console.error(
`Failed to register ${webhook.topic} webhook:`,
response.result
);
}
}
}Webhook Handlers
// web/backend/webhooks/handlers.js
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export async function handleProductCreate(shop, data) {
const product = JSON.parse(data);
// Store product data in your database
await prisma.product.create({
data: {
shopifyId: product.id.toString(),
shop,
title: product.title,
handle: product.handle,
status: product.status,
createdAt: new Date(product.created_at),
},
});
console.log(`Product created: ${product.title} for shop: ${shop}`);
}
export async function handleAppUninstalled(shop, data) {
// Clean up shop data
await prisma.session.deleteMany({
where: { shop },
});
await prisma.product.deleteMany({
where: { shop },
});
console.log(`App uninstalled from shop: ${shop}`);
}Building the Frontend with Polaris
Shopify Polaris provides a consistent UI that matches the Shopify admin.
Polaris Component Example
// web/frontend/components/ProductList.jsx
import { useState, useCallback } from 'react';
import {
Page,
Card,
ResourceList,
ResourceItem,
Text,
Badge,
Button,
Stack,
} from '@shopify/polaris';
import { useAuthenticatedFetch } from '../hooks';
export function ProductList() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(false);
const fetch = useAuthenticatedFetch();
const fetchProducts = useCallback(async () => {
setLoading(true);
try {
const response = await fetch('/api/products');
const data = await response.json();
setProducts(data.products);
} catch (error) {
console.error('Failed to fetch products:', error);
} finally {
setLoading(false);
}
}, [fetch]);
return (
<Page
title="Products"
primaryAction={{
content: 'Refresh',
onAction: fetchProducts,
}}
>
<Card>
<ResourceList
loading={loading}
resourceName={{ singular: 'product', plural: 'products' }}
items={products}
renderItem={(product) => {
const { id, title, status, totalInventory } = product;
return (
<ResourceItem
id={id}
accessibilityLabel={`View details for ${title}`}
>
<Stack alignment="center">
<Stack.Item fill>
<Text variant="bodyMd" fontWeight="bold">
{title}
</Text>
</Stack.Item>
<Badge status={status === 'ACTIVE' ? 'success' : 'warning'}>
{status}
</Badge>
<Text variant="bodySm" color="subdued">
{totalInventory} in stock
</Text>
</Stack>
</ResourceItem>
);
}}
/>
</Card>
</Page>
);
}App Billing Implementation
Implement subscription billing for your app.
Creating Billing Plans
// web/backend/services/billing.js
export async function createSubscription(session, planName) {
const client = new Shopify.Clients.Graphql(
session.shop,
session.accessToken
);
const plans = {
basic: { name: 'Basic Plan', price: 9.99 },
pro: { name: 'Pro Plan', price: 29.99 },
enterprise: { name: 'Enterprise Plan', price: 99.99 },
};
const plan = plans[planName];
const mutation = `
mutation CreateSubscription($name: String!, $price: Decimal!) {
appSubscriptionCreate(
name: $name
returnUrl: "${process.env.SHOPIFY_APP_URL}/billing/callback"
test: ${process.env.NODE_ENV !== 'production'}
lineItems: [
{
plan: {
appRecurringPricingDetails: {
price: { amount: $price, currencyCode: USD }
interval: EVERY_30_DAYS
}
}
}
]
) {
appSubscription {
id
status
}
confirmationUrl
userErrors {
field
message
}
}
}
`;
const response = await client.query({
data: {
query: mutation,
variables: {
name: plan.name,
price: plan.price,
},
},
});
return response.body.data.appSubscriptionCreate;
}Testing Your App
Comprehensive testing ensures reliability.
Unit Testing
// web/backend/__tests__/products.test.js
import { describe, it, expect, beforeEach } from 'vitest';
import { getProducts, createProduct } from '../services/products';
describe('Product Service', () => {
let mockSession;
beforeEach(() => {
mockSession = {
shop: 'test-shop.myshopify.com',
accessToken: 'test-token',
};
});
it('should fetch products', async () => {
const products = await getProducts(mockSession, 5);
expect(products).toBeDefined();
expect(products.edges).toBeInstanceOf(Array);
});
it('should create a product', async () => {
const productData = {
title: 'Test Product',
description: '<p>Test description</p>',
vendor: 'Test Vendor',
productType: 'Test Type',
tags: ['test', 'sample'],
};
const product = await createProduct(mockSession, productData);
expect(product.title).toBe(productData.title);
});
});Deployment Strategies
Deploy your app for production use.
Deployment Checklist
- ✅ Set up production database
- ✅ Configure environment variables
- ✅ Set up SSL certificates
- ✅ Configure webhook endpoints
- ✅ Test OAuth flow
- ✅ Enable error monitoring (Sentry, LogRocket)
- ✅ Set up CI/CD pipeline
Hosting Options
- Shopify Hosting - Managed hosting via Shopify
- Vercel - Serverless deployment
- Heroku - Traditional PaaS
- AWS/GCP - Full control and scalability
Conclusion
Building Shopify apps requires understanding OAuth, APIs, webhooks, and the Shopify ecosystem. With the right architecture and tools, you can create powerful solutions that enhance store functionality.
Start small, test thoroughly, and iterate based on merchant feedback to build apps that truly solve problems.
Need help building a custom Shopify app? Let's discuss your requirements and create a solution tailored to your business needs.
