Интеграция Medusa.js с фронтендом (Next.js/Gatsby)
Medusa предоставляет Store API и опциональный TypeScript SDK @medusajs/js-sdk для работы с фронтендом. Официальный Next.js Storefront Starter — готовая точка отсчёта, но для продуктовых проектов требует существенной доработки. Gatsby используется реже — подходит для каталогов с медленно меняющимся контентом благодаря SSG.
Настройка Medusa JS SDK
npm install @medusajs/js-sdk @medusajs/types
// lib/medusa/client.ts
import Medusa from '@medusajs/js-sdk';
export const medusaClient = new Medusa({
baseUrl: process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL!,
auth: {
type: 'session', // или 'jwt' для headless
},
publishableKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY,
});
Next.js App Router: страница продуктов
// app/products/[handle]/page.tsx
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { medusaClient } from '@/lib/medusa/client';
import { ProductTemplate } from '@/components/products/product-template';
type Props = { params: { handle: string } };
// SSG: генерация статических путей
export async function generateStaticParams() {
const { products } = await medusaClient.store.product.list({
fields: 'handle',
limit: 200,
});
return products.map(p => ({ handle: p.handle }));
}
// Динамические метатеги
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { products } = await medusaClient.store.product.list({
handle: params.handle,
fields: 'title,description,thumbnail',
});
const product = products[0];
if (!product) return {};
return {
title: product.title,
description: product.description ?? undefined,
openGraph: {
images: product.thumbnail ? [{ url: product.thumbnail }] : [],
},
};
}
export default async function ProductPage({ params }: Props) {
const { products } = await medusaClient.store.product.list({
handle: params.handle,
fields: '*variants,*variants.prices,*images,*options',
});
const product = products[0];
if (!product) notFound();
return <ProductTemplate product={product} />;
}
Управление корзиной (Cart Context)
// context/cart-context.tsx
'use client';
import { createContext, useContext, useEffect, useState } from 'react';
import { medusaClient } from '@/lib/medusa/client';
import type { HttpTypes } from '@medusajs/types';
type CartContextType = {
cart: HttpTypes.StoreCart | null;
addItem: (variantId: string, quantity: number) => Promise<void>;
removeItem: (lineItemId: string) => Promise<void>;
updateItem: (lineItemId: string, quantity: number) => Promise<void>;
isLoading: boolean;
};
const CartContext = createContext<CartContextType | null>(null);
export function CartProvider({ children }: { children: React.ReactNode }) {
const [cart, setCart] = useState<HttpTypes.StoreCart | null>(null);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const cartId = localStorage.getItem('cart_id');
if (cartId) {
medusaClient.store.cart.retrieve(cartId)
.then(({ cart }) => setCart(cart))
.catch(() => localStorage.removeItem('cart_id'));
}
}, []);
const addItem = async (variantId: string, quantity: number) => {
setIsLoading(true);
try {
let currentCart = cart;
// Создаём корзину, если не существует
if (!currentCart) {
const { cart: newCart } = await medusaClient.store.cart.create({
region_id: process.env.NEXT_PUBLIC_MEDUSA_REGION_ID,
});
localStorage.setItem('cart_id', newCart.id);
currentCart = newCart;
}
const { cart: updatedCart } = await medusaClient.store.cart.createLineItem(
currentCart.id,
{ variant_id: variantId, quantity }
);
setCart(updatedCart);
} finally {
setIsLoading(false);
}
};
const removeItem = async (lineItemId: string) => {
if (!cart) return;
setIsLoading(true);
try {
const { cart: updatedCart } = await medusaClient.store.cart.deleteLineItem(
cart.id,
lineItemId
);
setCart(updatedCart);
} finally {
setIsLoading(false);
}
};
const updateItem = async (lineItemId: string, quantity: number) => {
if (!cart) return;
const { cart: updatedCart } = await medusaClient.store.cart.updateLineItem(
cart.id,
lineItemId,
{ quantity }
);
setCart(updatedCart);
};
return (
<CartContext.Provider value={{ cart, addItem, removeItem, updateItem, isLoading }}>
{children}
</CartContext.Provider>
);
}
export const useCart = () => {
const ctx = useContext(CartContext);
if (!ctx) throw new Error('useCart must be used within CartProvider');
return ctx;
};
Checkout flow
// app/checkout/page.tsx
'use client';
import { useState } from 'react';
import { useCart } from '@/context/cart-context';
import { medusaClient } from '@/lib/medusa/client';
import { useRouter } from 'next/navigation';
export default function CheckoutPage() {
const { cart } = useCart();
const router = useRouter();
const [step, setStep] = useState<'address' | 'delivery' | 'payment' | 'review'>('address');
const handleAddressSubmit = async (addressData: Record<string, string>) => {
if (!cart) return;
await medusaClient.store.cart.update(cart.id, {
shipping_address: {
first_name: addressData.firstName,
last_name: addressData.lastName,
address_1: addressData.address,
city: addressData.city,
postal_code: addressData.postalCode,
country_code: addressData.country,
phone: addressData.phone,
},
email: addressData.email,
});
setStep('delivery');
};
const handlePaymentComplete = async (paymentSessionId: string) => {
if (!cart) return;
// Инициируем платёж
await medusaClient.store.payment.initiatePaymentSession(cart, {
provider_id: 'stripe',
});
// Создаём заказ
const { order } = await medusaClient.store.cart.complete(cart.id);
localStorage.removeItem('cart_id');
router.push(`/order/confirmed?id=${order.id}`);
};
// Рендер по шагам
return <div>{/* ... компоненты по шагам ... */}</div>;
}
Gatsby интеграция (SSG каталог)
// gatsby-config.ts
import type { GatsbyConfig } from 'gatsby';
const config: GatsbyConfig = {
plugins: [
{
resolve: 'gatsby-source-medusa',
options: {
storeUrl: process.env.GATSBY_MEDUSA_BACKEND_URL,
publishableApiKey: process.env.GATSBY_MEDUSA_PUBLISHABLE_KEY,
// Какие сущности тянуть в GraphQL
entities: ['products', 'collections', 'regions'],
batchSize: 100,
},
},
],
};
export default config;
// src/pages/{MedusaProduct.handle}.tsx — File System Route API
import { graphql, PageProps } from 'gatsby';
export const query = graphql`
query ProductPage($id: String!) {
medusaProduct(id: { eq: $id }) {
title handle description thumbnail
variants { id sku prices { amount currency_code } }
images { url }
}
}
`;
export default function ProductPage({ data }: PageProps<Queries.ProductPageQuery>) {
const product = data.medusaProduct;
// ... рендер
}
Мультирегиональность и локализация
// middleware.ts (Next.js)
import { NextRequest, NextResponse } from 'next/server';
const REGION_MAP: Record<string, string> = {
RU: process.env.MEDUSA_REGION_RU!,
BY: process.env.MEDUSA_REGION_BY!,
DE: process.env.MEDUSA_REGION_EU!,
DEFAULT: process.env.MEDUSA_REGION_DEFAULT!,
};
export function middleware(request: NextRequest) {
const country = request.geo?.country ?? 'DEFAULT';
const regionId = REGION_MAP[country] ?? REGION_MAP.DEFAULT;
const response = NextResponse.next();
response.cookies.set('medusa_region', regionId, {
maxAge: 60 * 60 * 24, // 24 часа
sameSite: 'lax',
});
return response;
}
Сроки интеграции
- Next.js Storefront Starter + подключение к Medusa + кастомизация: 2–3 недели
- Кастомный Next.js фронтенд с нуля (App Router, SSR/SSG, корзина, checkout): 6–10 недель
- Gatsby SSG-каталог с динамической корзиной (hybrid): 4–6 недель
- Мультирегиональный магазин с локализацией URL и контента: +2–3 недели к базовой оценке







