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

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.