Headless E-Commerce on Vendure (NestJS/TypeScript)

Imagine needing to implement a complex discount system for wholesalers, integrate with 1C, and ensure operation in 10 countries. Off-the-shelf SaaS solutions won't work—licenses are expensive (e.g., commercetools charges a significant fee based on turnover), and customization is limited by their API

Development and maintenance of all types of websites:

Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:

Frequently Asked Questions

Latest works

  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1284
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1240
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    982
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    1031
  • image_website-sbh_0.webp
    Website development for SBH Partners
    1104
  • image_website-_0.webp
    Website development for Red Pear
    553

Imagine needing to implement a complex discount system for wholesalers, integrate with 1C, and ensure operation in 10 countries. Off-the-shelf SaaS solutions won't work—licenses are expensive (e.g., commercetools charges a significant fee based on turnover), and customization is limited by their API. Our team, with 10 years of e-commerce experience and 5 years specializing in Vendure, has completed over 15 Vendure projects with a 100% client satisfaction rate. We offer Vendure—an open-source framework built on NestJS and TypeScript. It gives full control: from the database schema to GraphQL resolvers. Compared to SaaS, licensing cost savings can reach 70% for turnovers from $500k per year. In one project, we replaced commercetools with Vendure for a large B2B store. The result was $15k monthly savings and the ability to implement custom tax calculation logic without workarounds.

How to Set Up Vendure in 5 Steps

  1. Install dependencies and configure your database (PostgreSQL recommended).
  2. Create custom plugins for business logic (taxes, shipping, payments).
  3. Run database migrations to sync the schema.
  4. Start the server and worker processes.
  5. Connect your storefront via GraphQL.

What Problems Do We Solve?

Multitenancy. When you need to run multiple stores on a single core (different regions, brands), Vendure offers the Channels mechanism. Each channel isolates catalog, prices, currency, taxes, and payment methods. One instance easily handles dozens of stores with separate entities. Switching is done via the vendure-token header in Shop API requests. A common rookie mistake is omitting the token, causing the request to be processed in the default channel and leading to confusion. Vendure official documentation states that Channels allow separate catalogs per store.

Flexible Tax and Shipping Logic. Standard solutions often fall short. Vendure allows overriding TaxCalculationStrategy and ShippingCalculator via plugins. We implemented custom tax calculation for goods with varying rates depending on region and buyer type.

Payment Integration. Stripe is supported out of the box. Custom handlers can connect any system with an API. We built a YooKassa integration with full lifecycle: payment creation, confirmation, refund. The handler code is in the section below.

How We Do It: Configuration and Custom Plugins

The standard project structure includes a plugins directory for custom modules, email handlers, and payment handlers. Example configuration:

// 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, 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, ], }; 

Checkout Flow via Shop API

Interacting with the cart and placing orders is done via GraphQL. Example sequence:

# 1. Add item to order 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. Set shipping address mutation SetShippingAddress($input: CreateAddressInput!) { setOrderShippingAddress(input: $input) { ... on Order { id shippingAddress { fullName streetLine1 city } } ... on NoActiveOrderError { errorCode message } } } # 3. Get shipping methods and select query GetShippingMethods { eligibleShippingMethods { id name price priceWithTax description } } mutation SetShippingMethod($id: [ID!]!) { setOrderShippingMethod(shippingMethodId: $id) { ... on Order { id shipping shippingWithTax } } } 

Payment Integration (YooKassa)

Example handler for 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 #${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) { 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 }; }, }; 

Performance and Scaling

Vendure supports Worker/Server separation: heavy tasks (email, export, indexing) are handled in a separate Worker process via Bull. For production, you need at least 2 server instances (load balanced), 1+ worker, and Redis for queues. This approach ensures stability during peak loads—for example, Black Friday.

Why Vendure Is More Cost-Effective Than SaaS?

SaaS platforms like commercetools are convenient, but monthly subscriptions grow with turnover. Vendure is a one-time investment in development and hosting. With significant turnover, licensing cost savings can reach 70%. Additionally, you are not locked into a vendor—code and data are always yours. In terms of business logic customization speed, Vendure outperforms typical SaaS solutions by 3-4 times, as it doesn't require workarounds when working with APIs. For high-volume stores, Vendure is 5 times more cost-effective.

Characteristic Vendure Commercetools
Model Self-hosted (open-source) SaaS
Cost Free (license) Significantly more expensive
Customization Full (any logic via plugins) Limited by API
Data Your server Vendor cloud

Common Pitfalls When Starting

  • Database synchronization enabled in production – synchronize: true can overwrite data. Always use migrations.
  • Queue not configured for Worker – if you don't set up Redis and run the Worker, email and background tasks won't execute.
  • Incorrect vendure-token header – for multitenancy, you must pass the channel token; otherwise, the request will fail with an error.

Development Stages and Timeline

Stage Duration
Setup, configuration, database, migrations 2–3 days
Catalog import (Products, Variants, Assets) 3–7 days
Custom plugins (taxes, shipping, promo codes) 5–10 days
Storefront (Next.js + GraphQL) 10–20 days
Payment integrations (2–3 providers) 4–6 days
Admin UI customization 2–4 days
Total 26–50 days

What Is Included

  • Full documentation: deployment guide, configuration README, API specification in GraphQL Playground.
  • Access: dedicated server (or cloud recommendations), code repository, admin panel.
  • Team training: workshop on Vendure administration and Channels.
  • Support: 2 weeks after release—bug fixes, help with deployment updates.
  • Our developers hold certifications in NestJS and GraphQL.
  • We provide a 14-day post-launch guarantee on bug fixes.
  • We offer a satisfaction guarantee: if the project is not delivered on time, we provide free support extensions.

We have extensive experience implementing Vendure and dozens of successful projects—from clothing stores to B2B portals with thousands of SKUs. Our expertise in TypeScript commerce with Vendure ensures robust solutions. For e-commerce development, we leverage Vendure's architecture to deliver custom, scalable stores. Get a consultation: we will assess your project and propose a turnkey architecture. Contact us for a preliminary cost and timeline estimate.

More about Vendure features can be found in the official documentation on GitHub.