Розробка інтернет-магазину на Vendure
Vendure — headless commerce фреймворк на NestJS та TypeScript з GraphQL API. На відміну від commercetools, це open-source рішення з self-hosted розгортанням: ви контролюєте інфраструктуру, дані та можете модифікувати ядро через плагіни без форків. Архітектура базується на модулях NestJS, що робить розширення передбачуваним.
Коли Vendure — правильний вибір
- Потрібен self-hosted: дані не покидають вашу інфраструктуру
- Вимагається глибока кастомізація бізнес-логіки (податки, доставка, промокоди)
- TypeScript — основна мова команди
- Бюджет не дозволяє SaaS-платформи ($500+/міс на commercetools)
- Потрібен контроль над схемою БД (PostgreSQL/MySQL)
Архітектура проекту
vendure-project/
├── src/
│ ├── vendure-config.ts # Головна конфігурація
│ ├── plugins/ # Кастомні плагіни
│ │ ├── loyalty/
│ │ ├── b2b-pricing/
│ │ └── erp-sync/
│ ├── email-handlers/ # Шаблони листів
│ └── payment-handlers/ # Обробники оплати
├── storefront/ # Next.js / Nuxt
└── docker-compose.yml
Конфігурація Vendure
// src/vendure-config.ts
import { VendureConfig } from "@vendure/core";
import { defaultEmailHandlers, EmailPlugin } from "@vendure/email-plugin";
import { AssetServerPlugin } from "@vendure/asset-server-plugin";
import { AdminUiPlugin } from "@vendure/admin-ui-plugin";
export const config: VendureConfig = {
apiOptions: {
port: 3000,
adminApiPath: "admin-api",
shopApiPath: "shop-api",
adminApiPlayground: process.env.NODE_ENV === "development",
},
authOptions: {
tokenMethod: ["bearer", "cookie"],
superadminCredentials: {
identifier: process.env.SUPERADMIN_USERNAME!,
password: process.env.SUPERADMIN_PASSWORD!,
},
cookieOptions: {
secret: process.env.COOKIE_SECRET!,
},
},
dbConnectionOptions: {
type: "postgres",
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
database: process.env.DB_NAME,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
synchronize: false, // лише міграції в production
migrations: ["dist/migrations/*.js"],
},
paymentOptions: {
paymentMethodHandlers: [stripePaymentHandler, yookassaPaymentHandler],
},
taxOptions: {
taxCalculationStrategy: new CustomTaxCalculationStrategy(),
},
shippingOptions: {
shippingCalculators: [defaultShippingCalculator, tieredShippingCalculator],
shippingEligibilityCheckers: [defaultShippingEligibilityChecker],
fulfillmentHandlers: [manualFulfillmentHandler],
},
plugins: [
AssetServerPlugin.init({
route: "assets",
assetUploadDir: path.join(__dirname, "../static/assets"),
}),
EmailPlugin.init({
devMode: process.env.NODE_ENV === "development",
handlers: defaultEmailHandlers,
templatePath: path.join(__dirname, "../email/templates"),
transport: {
type: "smtp",
host: process.env.SMTP_HOST!,
port: 587,
auth: {
user: process.env.SMTP_USER!,
pass: process.env.SMTP_PASS!,
},
},
}),
AdminUiPlugin.init({
route: "admin",
port: 3002,
}),
LoyaltyPlugin,
B2bPricingPlugin,
ErpSyncPlugin,
],
};
Модель каналів та мультитенантність
Vendure підтримує Channels — аналог мультиарендності. Один інстанс обслуговує кілька магазинів з окремим каталогом, цінами та замовленнями:
// Канал створюється через Admin API або скрипт заповнення
await channelService.create(ctx, {
code: "ru-channel",
token: "ru-token-abc123",
defaultCurrencyCode: CurrencyCode.RUB,
defaultLanguageCode: LanguageCode.ru,
defaultTaxZone: taxZoneRU,
defaultShippingZone: shippingZoneRU,
pricesIncludeTax: false,
});
Кожен запит до Shop API повинен містити заголовок vendure-token: <channel-token>.
Checkout flow через Shop API
# 1. Додати товар до замовлення
mutation AddToOrder($productVariantId: ID!, $quantity: Int!) {
addItemToOrder(productVariantId: $productVariantId, quantity: $quantity) {
... on Order {
id
code
totalWithTax
lines {
id
quantity
productVariant { name sku }
unitPriceWithTax
}
}
... on ErrorResult {
errorCode
message
}
}
}
# 2. Встановити адресу доставки
mutation SetShippingAddress($input: CreateAddressInput!) {
setOrderShippingAddress(input: $input) {
... on Order { id shippingAddress { fullName streetLine1 city } }
... on NoActiveOrderError { errorCode message }
}
}
# 3. Отримати методи доставки та вибрати
query GetShippingMethods {
eligibleShippingMethods {
id
name
price
priceWithTax
description
}
}
mutation SetShippingMethod($id: [ID!]!) {
setOrderShippingMethod(shippingMethodId: $id) {
... on Order { id shipping shippingWithTax }
}
}
Платіжна інтеграція (YooKassa)
// src/payment-handlers/yookassa.handler.ts
import { CreatePaymentResult, PaymentMethodHandler, LanguageCode } from "@vendure/core";
export const yookassaPaymentHandler = new PaymentMethodHandler({
code: "yookassa",
description: [{ languageCode: LanguageCode.ru, value: "YooKassa" }],
args: {
shopId: { type: "string" },
secretKey: { type: "string", ui: { component: "password-form-input" } },
},
async createPayment(ctx, order, amount, args, metadata): Promise<CreatePaymentResult> {
const yookassa = new YooKassa({
shopId: args.shopId,
secretKey: args.secretKey,
});
const payment = await yookassa.createPayment({
amount: {
value: (amount / 100).toFixed(2),
currency: order.currencyCode,
},
capture: true,
confirmation: {
type: "redirect",
return_url: `${process.env.SHOP_URL}/checkout/confirmation`,
},
description: `Замовлення #${order.code}`,
metadata: { vendure_order_id: order.id },
});
return {
amount,
state: "Authorized",
transactionId: payment.id,
metadata: { confirmationUrl: payment.confirmation.confirmation_url },
};
},
async settlePayment(ctx, order, payment, args) {
// YooKassa з capture=true — оплата знімається автоматично
return { success: true };
},
async refundPayment(ctx, order, payment, args, lines, adjustment) {
const yookassa = new YooKassa({ shopId: args.shopId, secretKey: args.secretKey });
const refund = await yookassa.createRefund(payment.transactionId, {
amount: { value: (adjustment / 100).toFixed(2), currency: order.currencyCode },
});
return { state: "Settled", transactionId: refund.id };
},
});
Продуктивність та масштабування
Vendure підтримує Worker/Server розділення: важкі завдання (email, експорт, indexing) обробляються в окремому Worker-процесі через Bull:
// server.ts
import { bootstrap } from "@vendure/core";
bootstrap(config);
// worker.ts — запускається окремо
import { bootstrapWorker } from "@vendure/core";
bootstrapWorker(config);
Для Production: 2+ інстанси сервера (load balanced), 1+ воркер, Redis для черг.
Етапи розробки та часові рамки
| Етап | Часова рамка |
|---|---|
| Встановлення, конфігурація, БД, міграції | 2–3 дні |
| Імпорт каталогу (Products, Variants, Assets) | 3–7 днів |
| Кастомні плагіни (податки, доставка, промокоди) | 5–10 днів |
| Storefront (Next.js + GraphQL) | 10–20 днів |
| Платіжні інтеграції (2–3 провайдери) | 4–6 днів |
| Admin UI кастомізація | 2–4 дні |
| Всього | 26–50 днів |







