Shopify Headless with Custom VPS Backend
Headless e-commerce architecture separates the frontend presentation layer from backend commerce functionality, enabling flexibility and innovation. Shopify's Storefront API allows custom frontend applications while Shopify manages payments, inventory, and order processing. This approach enables building bespoke shopping experiences with technologies like React, Next.js, or Vue.js while leveraging Shopify's reliability. This guide covers implementing a headless Shopify store with a custom VPS backend including Storefront API integration, Hydrogen framework setup, custom frontend development, Node.js backend services, and caching strategies.
Table of Contents
- Headless Commerce Architecture
- Shopify Storefront API Setup
- Node.js Backend Infrastructure
- Hydrogen Framework Implementation
- Custom Frontend Development
- API Integration Layer
- Caching and Performance
- Deployment Strategy
- Security Considerations
- Conclusion
Headless Commerce Architecture
Headless commerce architecture decouples presentation from commerce logic, allowing independent evolution of each layer. Your architecture consists of Shopify (commerce backend), custom frontend (user experience), and optional middleware (business logic).
Benefits of headless approach:
- Complete design freedom for custom experiences
- Technology agnosticism - use modern frameworks
- Fast iteration independent of Shopify releases
- Multi-channel deployment (web, mobile, etc.)
Request flow in headless architecture:
User Request → Custom Frontend (Next.js/React) → API Layer (Express/Node.js)
→ Shopify Storefront API → Shopify Commerce Logic
Understand key endpoints:
- Products: retrieve catalog with images and variants
- Collections: organize products
- Checkout: create carts and initiate purchases
- Orders: retrieve order history
- Customers: manage user accounts
Shopify Storefront API Setup
The Storefront API provides secure access to Shopify data from your custom frontend.
Create Shopify development store:
# Visit: https://shopify.dev/docs/storefronts/headless
# Steps:
# 1. Create a Shopify Partner account at partners.shopify.com
# 2. Create a development store
# 3. Create a sales channel for your custom storefront
# 4. Add custom app for API access
Create custom app for API access:
# In Shopify admin:
# Settings > Apps and integrations > Develop apps
# Create app with name: "Custom Storefront API"
# Select scopes needed:
# - read_products
# - read_collections
# - read_orders
# - read_customers
# - write_orders
# - write_checkouts
# - write_customers
# Copy API credentials:
# - Storefront Access Token
# - API endpoint: https://your-store.myshopify.com/api/2024-01/graphql.json
Set up environment variables:
# Create .env file for your application
cat > /var/www/custom-storefront/.env << 'EOF'
SHOPIFY_STOREFRONT_ACCESS_TOKEN=your_token_here
SHOPIFY_STORE_DOMAIN=your-store.myshopify.com
SHOPIFY_API_VERSION=2024-01
SHOPIFY_API_ENDPOINT=https://your-store.myshopify.com/api/2024-01/graphql.json
EOF
Test Storefront API connectivity:
# Test GraphQL query
curl -X POST \
https://your-store.myshopify.com/api/2024-01/graphql.json \
-H "X-Shopify-Storefront-Access-Token: your_token_here" \
-H "Content-Type: application/json" \
-d '{
"query": "{
shop {
name
description
}
products(first: 5) {
edges {
node {
id
title
description
}
}
}
}"
}'
Node.js Backend Infrastructure
Set up Node.js backend to handle custom business logic and API orchestration.
Install Node.js and npm:
# Install Node.js LTS
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install nodejs -y
# Verify installation
node --version
npm --version
Initialize Node.js project:
mkdir /var/www/custom-backend
cd /var/www/custom-backend
# Initialize npm project
npm init -y
# Install dependencies
npm install express dotenv axios cors compression helmet morgan
npm install --save-dev nodemon
Create Express server:
cat > /var/www/custom-backend/server.js << 'EOF'
const express = require('express');
const cors = require('cors');
const compression = require('compression');
const helmet = require('helmet');
const morgan = require('morgan');
require('dotenv').config();
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(helmet());
app.use(compression());
app.use(cors());
app.use(morgan('combined'));
app.use(express.json());
// Health check endpoint
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date() });
});
// Root endpoint
app.get('/', (req, res) => {
res.json({
message: 'Custom Shopify Storefront Backend',
version: '1.0.0'
});
});
// Error handling
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({
error: 'Internal Server Error',
message: process.env.NODE_ENV === 'development' ? err.message : undefined
});
});
app.listen(PORT, () => {
console.log(`Backend running on port ${PORT}`);
});
EOF
Create Shopify API client:
cat > /var/www/custom-backend/shopifyClient.js << 'EOF'
const axios = require('axios');
class ShopifyClient {
constructor() {
this.storeDomain = process.env.SHOPIFY_STORE_DOMAIN;
this.accessToken = process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN;
this.endpoint = process.env.SHOPIFY_API_ENDPOINT;
}
async query(query, variables = {}) {
try {
const response = await axios.post(this.endpoint, {
query,
variables
}, {
headers: {
'X-Shopify-Storefront-Access-Token': this.accessToken,
'Content-Type': 'application/json'
}
});
if (response.data.errors) {
throw new Error(JSON.stringify(response.data.errors));
}
return response.data.data;
} catch (error) {
console.error('Shopify API Error:', error.message);
throw error;
}
}
// Get all products
async getProducts(first = 10, after = null) {
const query = `
query GetProducts($first: Int!, $after: String) {
products(first: $first, after: $after) {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
id
title
description
handle
priceRange {
minVariantPrice {
amount
currencyCode
}
}
images(first: 1) {
edges {
node {
src
altText
}
}
}
variants(first: 5) {
edges {
node {
id
title
price {
amount
currencyCode
}
available
}
}
}
}
}
}
}
`;
return this.query(query, { first, after });
}
// Get product by handle
async getProductByHandle(handle) {
const query = `
query GetProduct($handle: String!) {
productByHandle(handle: $handle) {
id
title
description
handle
priceRange {
minVariantPrice {
amount
currencyCode
}
}
images(first: 10) {
edges {
node {
src
altText
}
}
}
variants(first: 20) {
edges {
node {
id
title
price {
amount
currencyCode
}
available
selectedOptions {
name
value
}
}
}
}
reviews {
rating
title
body
author {
name
}
}
}
}
`;
return this.query(query, { handle });
}
// Get collections
async getCollections(first = 10) {
const query = `
query GetCollections($first: Int!) {
collections(first: $first) {
edges {
node {
id
title
handle
description
image {
src
}
}
}
}
}
`;
return this.query(query, { first });
}
// Create checkout
async createCheckout(lineItems) {
const query = `
mutation CreateCheckout($input: CheckoutCreateInput!) {
checkoutCreate(input: $input) {
checkout {
id
webUrl
lineItems(first: 10) {
edges {
node {
id
title
quantity
variant {
price {
amount
currencyCode
}
}
}
}
}
}
}
}
`;
return this.query(query, {
input: {
lineItems: lineItems.map(item => ({
variantId: item.variantId,
quantity: item.quantity
}))
}
});
}
}
module.exports = new ShopifyClient();
EOF
Create API routes:
cat > /var/www/custom-backend/routes.js << 'EOF'
const express = require('express');
const shopifyClient = require('./shopifyClient');
const router = express.Router();
// Get products
router.get('/api/products', async (req, res) => {
try {
const { first = 10, after = null } = req.query;
const data = await shopifyClient.getProducts(parseInt(first), after);
res.json(data);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Get single product
router.get('/api/products/:handle', async (req, res) => {
try {
const { handle } = req.params;
const data = await shopifyClient.getProductByHandle(handle);
res.json(data);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Get collections
router.get('/api/collections', async (req, res) => {
try {
const data = await shopifyClient.getCollections();
res.json(data);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Create checkout
router.post('/api/checkout', async (req, res) => {
try {
const { lineItems } = req.body;
const data = await shopifyClient.createCheckout(lineItems);
res.json(data);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;
EOF
Integrate routes into server:
# Update server.js to include routes
cat >> /var/www/custom-backend/server.js << 'EOF'
// Add routes
const routes = require('./routes');
app.use(routes);
EOF
Hydrogen Framework Implementation
Hydrogen is Shopify's React-based framework for building custom storefronts.
Initialize Hydrogen project:
# Install Hydrogen CLI
npm install -g @shopify/cli @shopify/hydrogen
# Create new project
cd /var/www
shopify hydrogen init custom-storefront --template demo-store
cd custom-storefront
Configure Hydrogen:
cat > hydrogen.config.js << 'EOF'
import { defineConfig } from '@shopify/hydrogen/config';
export default defineConfig({
storefront: {
storeId: process.env.SHOPIFY_STORE_ID,
storefrontToken: process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN,
requestDomain: process.env.SHOPIFY_STORE_DOMAIN,
},
});
EOF
Create custom components:
# ProductCard.jsx
cat > src/components/ProductCard.jsx << 'EOF'
import { Link } from '@shopify/hydrogen';
export default function ProductCard({ product }) {
const image = product.images?.edges[0]?.node;
const price = product.priceRange?.minVariantPrice;
return (
<Link to={`/products/${product.handle}`}>
<div className="product-card">
{image && (
<img
src={image.src}
alt={image.altText || product.title}
loading="lazy"
/>
)}
<h3>{product.title}</h3>
{price && (
<p className="price">
{new Intl.NumberFormat('en-US', {
style: 'currency',
currency: price.currencyCode,
}).format(price.amount)}
</p>
)}
</div>
</Link>
);
}
EOF
Create product listing page:
cat > src/pages/products.jsx << 'EOF'
import { Suspense } from 'react';
import ProductCard from '../components/ProductCard';
export default function ProductsPage() {
return (
<div className="products-page">
<h1>Products</h1>
<Suspense fallback={<div>Loading products...</div>}>
<ProductGrid />
</Suspense>
</div>
);
}
function ProductGrid() {
const { data } = useShopQuery({
query: PRODUCTS_QUERY,
});
return (
<div className="product-grid">
{data.products.edges.map(({ node }) => (
<ProductCard key={node.id} product={node} />
))}
</div>
);
}
const PRODUCTS_QUERY = `
query GetProducts {
products(first: 20) {
edges {
node {
id
title
handle
description
priceRange {
minVariantPrice {
amount
currencyCode
}
}
images(first: 1) {
edges {
node {
src
altText
}
}
}
}
}
}
}
`;
EOF
Custom Frontend Development
Build custom frontend with Next.js for maximum flexibility.
Initialize Next.js project:
cd /var/www
npx create-next-app@latest custom-frontend --typescript --tailwind
cd custom-frontend
npm install shopify-app-js zustand swr
Create Shopify API client library:
cat > lib/shopifyClient.ts << 'EOF'
const STOREFRONT_URL = `https://${process.env.NEXT_PUBLIC_SHOPIFY_DOMAIN}/api/2024-01/graphql.json`;
const ACCESS_TOKEN = process.env.NEXT_PUBLIC_SHOPIFY_ACCESS_TOKEN;
export async function shopifyFetch({
query,
variables,
}: {
query: string;
variables?: Record<string, any>;
}) {
try {
const response = await fetch(STOREFRONT_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Shopify-Storefront-Access-Token': ACCESS_TOKEN || '',
},
body: JSON.stringify({
query,
variables,
}),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.errors) {
throw new Error(JSON.stringify(data.errors));
}
return data.data;
} catch (error) {
console.error('Shopify API Error:', error);
throw error;
}
}
EOF
Create product listing page:
cat > pages/products.tsx << 'EOF'
import { FC } from 'react';
import Link from 'next/link';
import { shopifyFetch } from '../lib/shopifyClient';
interface Product {
id: string;
title: string;
handle: string;
priceRange: {
minVariantPrice: {
amount: string;
currencyCode: string;
};
};
images: {
edges: Array<{
node: {
src: string;
altText: string;
};
}>;
};
}
interface PageProps {
products: Product[];
}
const ProductsPage: FC<PageProps> = ({ products }) => {
return (
<div className="min-h-screen bg-gray-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<h1 className="text-3xl font-bold mb-8">Products</h1>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
</div>
</div>
);
};
function ProductCard({ product }: { product: Product }) {
const image = product.images?.edges[0]?.node;
const price = product.priceRange?.minVariantPrice;
return (
<Link href={`/products/${product.handle}`}>
<a className="group">
<div className="aspect-square overflow-hidden rounded-lg bg-gray-200">
{image && (
<img
src={image.src}
alt={image.altText || product.title}
className="w-full h-full object-cover"
/>
)}
</div>
<h3 className="mt-4 text-lg font-medium text-gray-900">
{product.title}
</h3>
{price && (
<p className="mt-2 text-lg font-medium text-gray-900">
${price.amount}
</p>
)}
</a>
</Link>
);
}
export async function getStaticProps() {
const data = await shopifyFetch({
query: `
query GetProducts {
products(first: 20) {
edges {
node {
id
title
handle
priceRange {
minVariantPrice {
amount
currencyCode
}
}
images(first: 1) {
edges {
node {
src
altText
}
}
}
}
}
}
}
`,
});
return {
props: {
products: data.products.edges.map((edge: any) => edge.node),
},
revalidate: 3600, // Revalidate every hour
};
}
export default ProductsPage;
EOF
API Integration Layer
Create middleware to orchestrate between frontend and Shopify.
Create API route for product search:
cat > pages/api/products/search.ts << 'EOF'
import { NextApiRequest, NextApiResponse } from 'next';
import { shopifyFetch } from '../../../lib/shopifyClient';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const { query } = req.query;
if (!query || typeof query !== 'string') {
return res.status(400).json({ error: 'Query parameter required' });
}
try {
const data = await shopifyFetch({
query: `
query SearchProducts($query: String!) {
search(query: $query, first: 10, types: PRODUCT) {
edges {
node {
... on Product {
id
title
handle
description
images(first: 1) {
edges {
node {
src
altText
}
}
}
priceRange {
minVariantPrice {
amount
currencyCode
}
}
}
}
}
}
}
`,
variables: { query },
});
res.status(200).json(data);
} catch (error: any) {
res.status(500).json({ error: error.message });
}
}
EOF
Create cart management API:
cat > pages/api/cart/index.ts << 'EOF'
import { NextApiRequest, NextApiResponse } from 'next';
import { shopifyFetch } from '../../../lib/shopifyClient';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method === 'POST') {
const { lineItems } = req.body;
try {
const data = await shopifyFetch({
query: `
mutation CreateCheckout($input: CheckoutCreateInput!) {
checkoutCreate(input: $input) {
checkout {
id
webUrl
lineItems(first: 10) {
edges {
node {
id
title
quantity
}
}
}
}
}
}
`,
variables: {
input: {
lineItems: lineItems.map((item: any) => ({
variantId: item.variantId,
quantity: item.quantity,
})),
},
},
});
res.status(200).json(data);
} catch (error: any) {
res.status(500).json({ error: error.message });
}
} else {
res.status(405).json({ error: 'Method not allowed' });
}
}
EOF
Caching and Performance
Implement caching strategies for optimal performance.
Configure ISR (Incremental Static Regeneration):
cat > next.config.js << 'EOF'
module.exports = {
images: {
domains: ['cdn.shopify.com'],
deviceSizes: [640, 750, 828, 1080, 1200],
imageSizes: [16, 32, 48, 64, 96, 128, 256],
},
revalidate: 3600, // Global ISR
};
EOF
Implement SWR for real-time data:
cat > hooks/useProducts.ts << 'EOF'
import useSWR from 'swr';
const fetcher = (url: string) => fetch(url).then(r => r.json());
export function useProducts() {
const { data, error, isLoading } = useSWR('/api/products', fetcher, {
revalidateOnFocus: false,
revalidateOnReconnect: false,
dedupingInterval: 60000, // 1 minute
});
return {
products: data,
isLoading,
isError: error,
};
}
EOF
Deployment Strategy
Deploy frontend and backend to production infrastructure.
Build and deploy Node.js backend:
cd /var/www/custom-backend
# Build application
npm run build
# Create systemd service
sudo cat > /etc/systemd/system/shopify-backend.service << 'EOF'
[Unit]
Description=Shopify Custom Backend
After=network.target
[Service]
Type=simple
User=www-data
WorkingDirectory=/var/www/custom-backend
Environment="NODE_ENV=production"
ExecStart=/usr/bin/node server.js
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable shopify-backend
sudo systemctl start shopify-backend
Configure Nginx reverse proxy:
sudo nano /etc/nginx/sites-available/shopify-backend.conf
# Add configuration
upstream node_backend {
server 127.0.0.1:3000;
}
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://node_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Deploy Next.js frontend:
cd /var/www/custom-frontend
# Build
npm run build
# Create systemd service for Next.js
sudo cat > /etc/systemd/system/shopify-frontend.service << 'EOF'
[Unit]
Description=Shopify Custom Storefront
After=network.target
[Service]
Type=simple
User=www-data
WorkingDirectory=/var/www/custom-frontend
Environment="NODE_ENV=production"
ExecStart=/usr/bin/npm start
Restart=always
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable shopify-frontend
sudo systemctl start shopify-frontend
Security Considerations
Implement security best practices for headless Shopify setup.
Validate API requests:
cat > middleware/validateShopifyRequest.ts << 'EOF'
import crypto from 'crypto';
export function isValidShopifyRequest(req: any) {
const hmacHeader = req.headers['x-shopify-hmac-sha256'];
if (!hmacHeader) {
return false;
}
const body = req.rawBody;
const secret = process.env.SHOPIFY_API_SECRET;
const hash = crypto
.createHmac('sha256', secret || '')
.update(body, 'utf8')
.digest('base64');
return hash === hmacHeader;
}
EOF
Protect sensitive endpoints:
cat > pages/api/protected.ts << 'EOF'
import { NextApiRequest, NextApiResponse } from 'next';
import { isValidShopifyRequest } from '../../middleware/validateShopifyRequest';
export default function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (!isValidShopifyRequest(req)) {
return res.status(401).json({ error: 'Unauthorized' });
}
// Protected endpoint logic
res.status(200).json({ message: 'Success' });
}
EOF
Conclusion
Building a headless Shopify storefront with custom VPS backend provides ultimate flexibility for e-commerce experiences. This architecture separates concerns allowing independent scaling and optimization of frontend, backend, and commerce layers. By leveraging Shopify's Storefront API, you maintain access to reliable payment processing, inventory management, and order fulfillment while maintaining complete control over customer experience. Proper caching, API optimization, and security practices ensure a fast, secure platform ready for production scale.


