E-commerce Store Migration to Headless Commerce Architecture
Headless Commerce — separation of backend (catalog, orders, payments) and frontend (storefront). The backend works as an API layer, the frontend — as a separate application on React, Vue, Next.js, or mobile client. This is not a trend but an engineering solution with specific trade-offs.
When migration is justified
Migrate if:
- Multiple storefronts with shared catalog (web + mobile app + B2B portal)
- Current monolith can't handle rendering load
- Frontend team wants independent deployment without backend sync
- Maximum loading speed needed (SSG for catalog via Next.js)
Don't migrate if:
- Single simple store without specific performance requirements
- No separate frontend team
- Market launch timeline is critical (migration takes 3-6 months)
Backend options for headless
| Platform | API | Features |
|---|---|---|
| Bagisto | REST + GraphQL | Laravel, open-source, full control |
| Medusa.js | REST | Node.js, quick customization |
| Vendure | GraphQL | TypeScript, plugin architecture |
| Shopify | GraphQL Storefront API | SaaS, limited customization |
| WooCommerce | REST (WP REST API) | Proven, but slow |
| Magento 2 | REST + GraphQL | Enterprise, high complexity |
Architecture after migration
┌─────────────────────────────────────────────────────┐
│ CDN (Cloudflare) │
└────────────┬──────────────────────┬─────────────────┘
│ │
┌────────▼───────┐ ┌─────────▼────────┐
│ Next.js (SSG) │ │ React SPA (CSR) │
│ Storefront │ │ B2B Portal │
└────────┬───────┘ └─────────┬────────┘
│ │
└──────────┬───────────┘
│ REST/GraphQL
┌─────────▼──────────┐
│ API Gateway │
│ (Kong / Nginx) │
└─────────┬──────────┘
│
┌──────────────┼──────────────┐
│ │ │
┌──────▼──────┐ ┌─────▼─────┐ ┌────▼─────┐
│ Commerce │ │ Search │ │ CMS │
│ Backend │ │ (ES/ │ │ (Strapi/ │
│ (Bagisto) │ │ Typesens)│ │ Sanity) │
└─────────────┘ └───────────┘ └──────────┘
Migration phases
Phase 1: Audit and API design (2-3 weeks)
Current monolith inventory:
- List of all entities (products, categories, orders, customers, coupons)
- Entity dependencies
- Custom fields and logic not covered by standard API
API contract design: OpenAPI 3.0 specification before development starts. This allows the frontend team to start work in parallel.
Phase 2: Building API layer (4-8 weeks)
For Bagisto: GraphQL available via bagisto/graphql-api package:
composer require bagisto/graphql-api
php artisan vendor:publish --provider="Webkul\GraphQLAPI\Providers\GraphQLAPIServiceProvider"
Sample catalog query:
query GetProducts($categoryId: ID, $page: Int) {
products(
categoryId: $categoryId
page: $page
limit: 24
) {
data {
id
sku
name
price
images {
url
altText
}
variants {
id
price
attributes {
code
value
}
}
}
paginatorInfo {
currentPage
lastPage
total
}
}
}
Phase 3: Strangler Fig — gradual replacement
Direct monolith replacement is risky. The Strangler Fig pattern is used:
- New headless backend deploys in parallel
- Nginx/API Gateway routes requests: part to old monolith, part to new API
- Pages migrate iteratively: first catalog, then checkout, then account
- After full traffic migration, monolith is disabled
# Traffic switching in nginx
location /api/v1/ {
proxy_pass http://new-api-backend:8000;
}
location / {
proxy_pass http://old-monolith:80;
}
Phase 4: Data migration
If changing platforms (e.g., WooCommerce → Bagisto):
// Artisan command to import WooCommerce products
class ImportWooCommerceProducts extends Command
{
public function handle(): void
{
$client = new WooCommerceClient(config('woocommerce'));
$page = 1;
do {
$products = $client->get('products', ['per_page' => 100, 'page' => $page]);
foreach ($products as $product) {
ImportProductJob::dispatch($product)->onQueue('import');
}
$page++;
} while (count($products) === 100);
}
}
Phase 5: Frontend application (6-10 weeks in parallel)
Next.js storefront with ISR (Incremental Static Regeneration) for catalog pages:
// pages/products/[slug].tsx
export async function getStaticProps({ params }) {
const product = await commerceClient.getProduct(params.slug);
return {
props: { product },
revalidate: 3600, // Cache update every hour
};
}
export async function getStaticPaths() {
const slugs = await commerceClient.getAllProductSlugs();
return {
paths: slugs.map(slug => ({ params: { slug } })),
fallback: 'blocking',
};
}
Phase 6: Cart state and authentication
In headless architecture, cart lives in API, not in session. Client stores cart_token in localStorage:
const useCart = () => {
const [cartToken, setCartToken] = useLocalStorage<string>('cart_token', '');
const addToCart = async (productId: string, qty: number) => {
const response = await api.post('/api/v1/checkout/cart/add', {
product_id: productId,
quantity: qty,
}, {
headers: cartToken ? { 'cart-token': cartToken } : {},
});
if (response.data.token) {
setCartToken(response.data.token);
}
};
};
Performance after migration
| Metric | Monolith (PHP SSR) | Headless (Next.js ISR) |
|---|---|---|
| TTFB (catalog) | 400-800ms | 20-50ms (CDN) |
| LCP | 2.5-4s | 0.8-1.5s |
| Frontend deployment | With backend | Independent |
| A/B testing | Difficult | Edge Middleware |
Total migration timeline
Average store (10-50k SKU, standard checkout): 3-5 months with two parallel teams. Critical path — API design and contract test coverage between frontend and backend.







