Разработка интернет-магазина на Medusa.js
Medusa — Node.js e-commerce фреймворк с открытым исходным кодом, позиционирующийся как альтернатива Shopify для разработчиков. Версия 2.x (released 2024) полностью переписана: модульная архитектура на основе Medusa Modules, новый IoC-контейнер, улучшенный workflow-engine. Стек: TypeScript, Node.js 20+, PostgreSQL, Redis.
Архитектура Medusa 2.x
В отличие от монолитной v1, Medusa 2.x строится на независимых модулях:
┌─────────────────────────────────────────┐
│ Medusa Application │
├─────────────┬────────────┬──────────────┤
│ HTTP Layer │ Workflows │ Subscribers │
│ (API + MW) │ (Sagas) │ (Events) │
├─────────────┴────────────┴──────────────┤
│ Module Container │
├──────────┬──────────┬───────────────────┤
│ Product │ Order │ Cart │ Auth │
│ Module │ Module │ Module │ Module │
├──────────┴──────────┴─────────┴─────────┤
│ Infrastructure Layer │
│ PostgreSQL + Redis + S3 │
└─────────────────────────────────────────┘
Каждый модуль — независимый пакет с собственной схемой БД, сервисами и событиями. Это позволяет заменять отдельные части системы (например, заменить встроенный Product Module на кастомный с другой структурой данных).
Установка и базовая конфигурация
npx create-medusa-app@latest mystore --db-url postgresql://user:pass@localhost/medusa
cd mystore
# Структура проекта
# src/
# api/ — кастомные роуты
# workflows/ — бизнес-логика
# modules/ — кастомные модули
# subscribers/ — обработчики событий
# jobs/ — scheduled tasks
# medusa-config.ts
Конфигурационный файл medusa-config.ts:
import { defineConfig, loadEnv } from '@medusajs/framework/config';
loadEnv(process.env.NODE_ENV || 'development', process.cwd());
export default defineConfig({
projectConfig: {
databaseUrl: process.env.DATABASE_URL,
redisUrl: process.env.REDIS_URL,
http: {
storeCors: process.env.STORE_CORS,
adminCors: process.env.ADMIN_CORS,
authCors: process.env.AUTH_CORS,
jwtSecret: process.env.JWT_SECRET,
cookieSecret: process.env.COOKIE_SECRET,
},
},
modules: [
{
resolve: '@medusajs/medusa/fulfillment',
options: {
providers: [
{
resolve: '@medusajs/fulfillment-manual',
id: 'manual',
},
],
},
},
{
resolve: '@medusajs/medusa/payment',
options: {
providers: [
{
resolve: '@medusajs/payment-stripe',
id: 'stripe',
options: {
apiKey: process.env.STRIPE_API_KEY,
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
},
},
],
},
},
{
resolve: '@medusajs/medusa/file',
options: {
providers: [
{
resolve: '@medusajs/file-s3',
id: 's3',
options: {
file_url: process.env.S3_FILE_URL,
access_key_id: process.env.S3_ACCESS_KEY_ID,
secret_access_key: process.env.S3_SECRET_ACCESS_KEY,
region: process.env.S3_REGION,
bucket: process.env.S3_BUCKET,
},
},
],
},
},
],
});
Модели данных и работа с каталогом
Medusa 2.x использует MikroORM под капотом. Продукты строятся через Product → ProductVariant → Price иерархию:
// Создание продукта с вариантами через API или сервис
const productModuleService = container.resolve('product');
const product = await productModuleService.createProducts({
title: 'Куртка зимняя',
handle: 'kurtka-zimnyaya',
status: ProductStatus.PUBLISHED,
options: [
{ title: 'Размер', values: ['S', 'M', 'L', 'XL'] },
{ title: 'Цвет', values: ['Чёрный', 'Синий', 'Красный'] },
],
variants: [
{
title: 'S / Чёрный',
sku: 'JACKET-S-BLACK',
options: { Размер: 'S', Цвет: 'Чёрный' },
manage_inventory: true,
prices: [
{ amount: 299900, currency_code: 'rub' },
{ amount: 2999, currency_code: 'usd' },
],
},
// ...остальные варианты
],
images: [{ url: 'https://cdn.example.com/jacket-black.jpg' }],
collection_id: 'col_winter_2024',
});
Workflows и бизнес-логика
Workflows — ключевая концепция Medusa 2.x для сложной бизнес-логики. Это саги с компенсациями (rollback):
// src/workflows/custom-order-workflow.ts
import { createWorkflow, WorkflowResponse } from '@medusajs/framework/workflows-sdk';
import { createStep, StepResponse } from '@medusajs/framework/workflows-sdk';
const validateInventoryStep = createStep(
'validate-inventory',
async ({ variantId, quantity }: { variantId: string; quantity: number }, context) => {
const inventoryService = context.container.resolve('inventory');
const available = await inventoryService.retrieveAvailableQuantity(variantId, []);
if (available < quantity) {
throw new Error(`Недостаточно товара: доступно ${available}, запрошено ${quantity}`);
}
return new StepResponse({ available });
}
);
const reserveInventoryStep = createStep(
'reserve-inventory',
async ({ variantId, quantity, locationId }, context) => {
const inventoryService = context.container.resolve('inventory');
const reservation = await inventoryService.createReservationItems([{
inventory_item_id: variantId,
location_id: locationId,
quantity,
}]);
return new StepResponse(
{ reservationId: reservation[0].id },
// Компенсация — выполняется при откате
{ reservationId: reservation[0].id }
);
},
// Компенсирующая функция (rollback)
async ({ reservationId }, context) => {
const inventoryService = context.container.resolve('inventory');
await inventoryService.deleteReservationItems([reservationId]);
}
);
export const customOrderWorkflow = createWorkflow(
'custom-order-workflow',
function (input: { variantId: string; quantity: number; locationId: string }) {
const { available } = validateInventoryStep(input);
const { reservationId } = reserveInventoryStep(input);
return new WorkflowResponse({ reservationId, available });
}
);
Кастомные API-роуты
// src/api/store/recommendations/route.ts
import type { MedusaRequest, MedusaResponse } from '@medusajs/framework/http';
import { container } from '@medusajs/framework';
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
const productId = req.params.id;
const productService = container.resolve('product');
// Логика рекомендаций
const product = await productService.retrieveProduct(productId, {
relations: ['collection', 'tags'],
});
const related = await productService.listProducts({
collection_id: [product.collection_id],
id: { $ne: productId },
}, {
take: 6,
order: { created_at: 'DESC' },
});
res.json({ products: related });
};
Подписчики событий
// src/subscribers/order-placed.ts
import { IOrderModuleService } from '@medusajs/framework/types';
import { SubscriberArgs, SubscriberConfig } from '@medusajs/framework';
import { sendOrderConfirmationWorkflow } from '../workflows/send-order-confirmation';
export default async function orderPlacedHandler({
event: { data },
container,
}: SubscriberArgs<{ id: string }>) {
const orderId = data.id;
await sendOrderConfirmationWorkflow(container).run({
input: { orderId },
});
}
export const config: SubscriberConfig = {
event: 'order.placed',
};
Интеграция платёжных систем
Помимо Stripe, Medusa 2.x поддерживает кастомные Payment Provider:
// src/modules/payment-cloudpayments/service.ts
import {
AbstractPaymentProvider,
PaymentProviderContext,
PaymentSessionStatus,
} from '@medusajs/framework/utils';
class CloudPaymentsPaymentProvider extends AbstractPaymentProvider {
static identifier = 'cloudpayments';
async initiatePayment(context: PaymentProviderContext) {
const { amount, currency_code, context: { customer } } = context;
return {
id: `cp_${Date.now()}`,
data: {
publicId: process.env.CLOUDPAYMENTS_PUBLIC_ID,
description: 'Оплата заказа',
amount: amount / 100, // Medusa хранит в копейках
currency: currency_code.toUpperCase(),
accountId: customer?.email,
},
};
}
async getPaymentStatus(paymentSessionData: Record<string, unknown>) {
const response = await this.cloudpaymentsClient.checkPayment(
paymentSessionData.transactionId as string
);
return response.success ? PaymentSessionStatus.AUTHORIZED : PaymentSessionStatus.PENDING;
}
async capturePayment(paymentData: Record<string, unknown>) {
// CloudPayments — capture при confirm
return { ...paymentData, captured: true };
}
async refundPayment(paymentData: Record<string, unknown>, refundAmount: number) {
await this.cloudpaymentsClient.refund({
TransactionId: paymentData.transactionId,
Amount: refundAmount / 100,
});
return paymentData;
}
}
Деплой и production-конфигурация
# docker-compose.prod.yml
services:
medusa:
build: .
environment:
NODE_ENV: production
DATABASE_URL: postgresql://user:pass@postgres:5432/medusa
REDIS_URL: redis://redis:6379
JWT_SECRET: ${JWT_SECRET}
command: >
sh -c "npx medusa db:migrate && npx medusa start"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
medusa-worker:
build: .
environment:
NODE_ENV: production
MEDUSA_WORKER_MODE: worker # только обработка очереди
command: npx medusa start
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: medusa
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d medusa"]
interval: 5s
redis:
image: redis:7-alpine
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
Сроки разработки
- Базовый магазин: бэкенд Medusa + Next.js Storefront starter: 3–4 недели
- Магазин с кастомными модулями, интеграциями CRM/ERP, кастомными workflow: 8–14 недель
- Headless enterprise-решение с несколькими регионами, мультивалютностью, B2B-логикой: 16–24 недели







