Как создать мультивендорный маркетплейс на Next.js, Express и Stripe Connect

Материал основан на разборе freecodecamp.org. Ниже — главное и практические шаги, которые можно быстро применить в работе.


Мультивендорный маркетплейс — это платформа, позволяющая продавцам регистрироваться и напрямую продавать свои товары или услуги покупателям.

В этом руководстве мы создадим полноценный маркетплейс, используя TypeScript. Вам не понадобится традиционная база данных — вместо этого мы будем использовать Stripe для управления товарами и процессами платежей.

Именно так работают многие реальные маркетплейсы: Stripe хранит товары, цены и данные покупателей, а ваше приложение отвечает за пользовательский опыт.

Вот что вы создадите:

  • Процесс подключения продавцов, в котором они создают аккаунты и привязываются к Stripe
  • Систему управления товарами, где продавцы добавляют и размещают товары напрямую через Stripe
  • Процесс оформления заказа с поддержкой как единовременных платежей, так и повторяющихся подписок
  • Вебхуки (webhooks), которые в реальном времени отслеживают события оплаты
  • Полноценный магазин, где покупатели просматривают и приобретают товары

Полный исходный код можно найти в репозитории на GitHub — ссылка приведена в конце.

В этом пошаговом руководстве по созданию базы данных для интернет-магазина на Node.js вы найдете все необходимые инструкции для внедрения своей системы хранения данных, которая прекрасно трасформирует ваш маркетплейс. Прочитайте здесь.

Динамические формы в React и Next.js — это ключевой момент для улучшения взаимодействия пользователей с вашим приложением. Изучите, как реализовать это в вашем маркетплейсе! Узнайте больше.

Содержание
  1. Предварительные требования
  2. Что такое Stripe Connect и как он работает
  3. Как настроить проект
  4. Как настроить бэкенд
  5. Бэкенд на Express и TypeScript: настройка сервера
  6. Stripe Connect onboarding: подключение продавцов
  7. Как создать подключённый аккаунт
  8. Как создать ссылку для онбординга
  9. Как проверить статус аккаунта
  10. Как создавать товары через Stripe
  11. Как получать список товаров
  12. Как реализовать оформление заказа через Stripe Checkout
  13. Как обрабатывать вебхуки
  14. Как настроить вебхуки в панели управления Stripe
  15. Как тестировать вебхуки локально
  16. Как добавить биллинговый портал
  17. Как построить фронтенд на Next.js
  18. Как создать контекст аккаунта
  19. Как создать хук статуса аккаунта
  20. Как создать компонент онбординга продавца
  21. Как создать форму товара, список товаров и оформление заказа
  22. Как создать форму товара
  23. Как создать главную страницу
  24. Как протестировать полный сценарий
  25. Как работает разделение платежа
  26. Следующие шаги
  27. Ответы на эти вопросы могут быть для вас полезными

Предварительные требования

Прежде чем начать, убедитесь, что у вас есть следующее:

  • Node.js (версия 18 или выше), установленный на вашем компьютере
  • Базовое понимание React, TypeScript и REST API
  • Аккаунт Stripe (бесплатная регистрация на stripe.com)
  • Редактор кода, например VS Code

В этом проекте обойдёмся без базы данных, так как Stripe будет использоваться в роли основного хранилища для ваших товаров, цен и информации о покупателях, учитывая современные тенденции в разработке маркетплейсов.

Что такое Stripe Connect и как он работает

Stripe Connect — это набор API, который упрощает управление платёжными процессами на платформах и маркетплейсах, позволяя создавать и управлять аккаунтами продавцов.

В данном руководстве мы используем Stripe V2 Accounts API, который является современным и рекомендуемым способом создания подключённых аккаунтов, позволяя настраивать процессы приёма платежей и осуществления выплат.

Вот как работает процесс оплаты:

  1. Покупатель выбирает товар и нажимает кнопку оформления заказа на вашем маркетплейсе
  2. Ваш сервер создаёт Stripe Checkout Session, привязанную к подключённому аккаунту продавца
  3. Покупатель оплачивает заказ на размещённой странице оформления заказа Stripe
  4. Stripe автоматически разделяет платёж: продавец получает свою долю, а ваша платформа удерживает комиссию приложения
  5. Stripe отправляет событие вебхука на ваш сервер, подтверждая оплату
  6. Продавец просматривает свои доходы и выводит средства из своей панели управления Stripe

Как настроить проект

Создайте папку проекта с отдельными директориями для бэкенда и фронтенда.

Практически это означает, что вам с самого начала стоит разделить ответственность между слоями. Бэкенд будет общаться со Stripe API, создавать аккаунты продавцов, товары, цены, checkout-сессии и принимать вебхуки. Фронтенд на Next.js отвечает за интерфейс продавца и покупателя: формы, карточки товаров, статус онбординга и редиректы.

Такое разделение избавляет проект от хаоса уже на старте. Когда вы дойдёте до деплоя, масштабирования или подключения авторизации, у вас не будет монолитного приложения, где платежи, UI и логика продавца перемешаны в одном месте.

Как настроить бэкенд

Перейдите в директорию server и инициализируйте проект TypeScript:

cd server
npm init -y
npm install express cors dotenv stripe
npm install -D typescript ts-node @types/express @types/cors @types/node
npx tsc --init
mkdir src

Откройте tsconfig.json и обновите его следующими настройками:

{ "compilerOptions": { "target": "ES2020", "module": "commonjs", "lib": ["ES2020"], "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true }, "include": ["src/**/*"]
}

Затем создайте файл .env в корне директории server:

STRIPE_SECRET_KEY=sk_test_your_key_here
DOMAIN=http://localhost:3000

Ваш тестовый секретный ключ Stripe можно найти в панели управления Stripe в разделе Developers > API Keys. Переменная DOMAIN указывает вашему серверу, куда перенаправлять покупателей после оформления заказа.

Добавьте следующие скрипты в ваш package.json:

{ "scripts": { "dev": "ts-node src/index.ts", "build": "tsc", "start": "node dist/index.js" }
}

Бэкенд на Express и TypeScript: настройка сервера

Создайте файл src/index.ts — это будет весь ваш бэкенд. Начнём с настройки и импортов:

import express, { Request, Response, Router } from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import Stripe from 'stripe';
dotenv.config();
const app = express();
const router = Router();
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string);
app.use(cors({ origin: process.env.DOMAIN }));
app.use(express.static('public'));

Обратите внимание, что мы не импортируем никакой клиент базы данных. Stripe — это наш уровень данных. Каждый товар, цена, покупатель и транзакция хранятся в Stripe. Ваш сервер Express — это тонкий оркестрационный слой, который взаимодействует с Stripe API от имени вашего фронтенда.

Мы также подключаем express.static("public"), чтобы при необходимости можно было раздавать статические файлы. Эндпоинт вебхука требует необработанного тела запроса, поэтому мы зарегистрируем его до JSON-парсера — добавим это сейчас.

Stripe Connect onboarding: подключение продавцов

Первое, что нужно сделать продавцу — создать аккаунт на вашей платформе и привязать его к Stripe. Это включает два шага: создание подключённого аккаунта и перенаправление продавца на размещённую форму онбординга Stripe.

Как создать подключённый аккаунт

Добавьте следующий маршрут в ваш файл src/index.ts:

// Определения типов для тел запросов
interface CreateAccountBody { email: string;
}
interface AccountIdBody { accountId: string;
}
// Создание подключённого аккаунта с использованием Stripe V2 API
router.post( '/create-connect-account', async (req: Request<{}, {}, CreateAccountBody>, res: Response) => { try { const account = await stripe.v2.core.accounts.create({ display_name: req.body.email, contact_email: req.body.email, dashboard: 'full', defaults: { responsibilities: { fees_collector: 'stripe', losses_collector: 'stripe', }, }, identity: { country: 'GB', entity_type: 'company', }, configuration: { customer: {}, merchant: { capabilities: { card_payments: { requested: true }, }, }, }, }); res.json({ accountId: account.id }); } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; res.status(500).json({ error: message }); } },
);

Разберём, что делает этот код. Метод stripe.v2.core.accounts.create() создаёт новый подключённый аккаунт с использованием Stripe V2 API. Вот ключевые параметры конфигурации:

  • dashboard: "full" даёт продавцу доступ к собственной панели управления Stripe, где он может просматривать платежи, управлять выплатами и обрабатывать споры
  • responsibilities сообщает Stripe, кто собирает комиссии и кто несёт ответственность за убытки. Установка обоих значений в "stripe" означает, что Stripe берёт это на себя — наиболее простая конфигурация
  • identity задаёт страну и тип юридического лица. Замените "GB" на код страны ваших продавцов (например, "US" для Соединённых Штатов)
  • configuration.merchant.capabilities запрашивает возможность card_payments, которая позволяет продавцу принимать платежи по кредитным картам

Как создать ссылку для онбординга

После создания аккаунта необходимо перенаправить продавца на размещённую форму онбординга Stripe. Добавьте этот маршрут:

// Создание ссылки для онбординга
router.post('/create-account-link', async (req: Request<{}, {}, AccountIdBody>, res: Response) => { const { accountId } = req.body; try { const accountLink = await stripe.v2.core.accountLinks.create({ account: accountId, use_case: { type: 'account_onboarding', account_onboarding: { configurations: ['merchant', 'customer'], refresh_url: `${process.env.DOMAIN}`, return_url: `${process.env.DOMAIN}?accountId=${accountId}`, }, }, }); res.json({ url: accountLink.url }); } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; res.status(500).json({ error: message }); }
});

Метод accountLinks.create() генерирует временный URL, который ведёт продавца на форму онбординга Stripe. На этой форме Stripe собирает документы, удостоверяющие личность продавца, реквизиты банковского счёта и налоговую информацию — вам не нужно ничего из этого строить самостоятельно.

return_url — это адрес, на который Stripe перенаправляет продавца после завершения онбординга. Обратите внимание, что вы добавляете accountId в качестве параметра запроса, чтобы ваш фронтенд мог его получить и сохранить.

Как проверить статус аккаунта

Вам нужен способ проверить, завершил ли продавец онбординг и готов ли он принимать платежи. Добавьте этот маршрут:

// Получение статуса подключённого аккаунта
router.get( '/account-status/:accountId', async (req: Request<{ accountId: string }>, res: Response) => { try { const account = await stripe.v2.core.accounts.retrieve(req.params.accountId, { include: ['requirements', 'configuration.merchant'], });
const payoutsEnabled = account.configuration?.merchant?.capabilities?.stripe_balance?.payouts?.status === 'active'; const chargesEnabled = account.configuration?.merchant?.capabilities?.card_payments?.status === 'active'; const summaryStatus = account.requirements?.summary?.minimum_deadline?.status; const detailsSubmitted = !summaryStatus || summaryStatus === 'eventually_due';
res.json({ id: account.id, payoutsEnabled, chargesEnabled, detailsSubmitted, requirements: account.requirements?.entries, }); } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; res.status(500).json({ error: message }); } },
);

Этот маршрут получает подключённый аккаунт и проверяет три важных статуса:

  • chargesEnabled — может ли продавец принимать платежи
  • payoutsEnabled — может ли он получать выплаты на свой банковский счёт
  • detailsSubmitted — заполнил ли он форму онбординга

Ваш фронтенд будет использовать эти флаги для отображения или скрытия функций.

Как создавать товары через Stripe

Вместо хранения товаров в базе данных вы будете создавать их непосредственно в Stripe. Каждый товар создаётся на подключённом аккаунте продавца с использованием заголовка stripeAccount. Это означает, что у каждого продавца есть собственный изолированный каталог товаров внутри Stripe.

// Определение типа для создания товара
interface CreateProductBody { productName: string; productDescription: string; productPrice: number; accountId: string;
}
// Создание товара на подключённом аккаунте
router.post('/create-product', async (req: Request<{}, {}, CreateProductBody>, res: Response) => { const { productName, productDescription, productPrice, accountId } = req.body; try { // Создание товара на подключённом аккаунте const product = await stripe.products.create( { name: productName, description: productDescription, }, { stripeAccount: accountId }, );
// Создание цены для товара const price = await stripe.prices.create( { product: product.id, unit_amount: productPrice, currency: 'usd', }, { stripeAccount: accountId }, );
res.json({ productName, productDescription, productPrice, priceId: price.id, }); } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; res.status(500).json({ error: message }); }
});

Здесь происходят два вызова Stripe API. Сначала stripe.products.create() создаёт товар (название и описание). Затем stripe.prices.create() создаёт цену для этого товара (сумму и валюту).

Stripe разделяет товары и цены, потому что у одного товара может быть несколько цен — например, ежемесячный план и годовой план.

Параметр { stripeAccount: accountId } в обоих вызовах указывает Stripe создавать эти ресурсы на подключённом аккаунте продавца, а не на аккаунте вашей платформы. Это критически важная деталь: без него товары будут созданы на аккаунте вашей платформы, и продавец никогда их не увидит.

Как получать список товаров

Добавьте маршрут для получения всех товаров конкретного продавца:

// Получение товаров для конкретного аккаунта
router.get('/products/:accountId', async (req: Request<{ accountId: string }>, res: Response) => { const { accountId } = req.params; try { const options: Stripe.RequestOptions = {}; if (accountId !== 'platform') { options.stripeAccount = accountId; }
const prices = await stripe.prices.list( { expand: ['data.product'], active: true, limit: 100, }, options, );
const products = prices.data.map((price) => { const product = price.product as Stripe.Product; return { id: product.id, name: product.name, description: product.description, price: price.unit_amount, priceId: price.id, period: price.recurring ? price.recurring.interval : null, }; });
res.json(products); } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; res.status(500).json({ error: message }); }
});

Этот маршрут получает все активные цены из аккаунта продавца в Stripe и раскрывает данные о товаре (используя expand: ["data.product"]), чтобы вы получили название и описание товара в одном вызове API. Поле period будет равно null для разовых товаров и "month" или "year" для подписок.

Как реализовать оформление заказа через Stripe Checkout

Процесс оформления заказа должен обрабатывать два сценария: разовые платежи за отдельные продукты и повторяющиеся подписки. Checkout Sessions в Stripe поддерживают оба варианта — нужно лишь задать режим в зависимости от типа цены.

// Определение типа для оформления заказа
interface CheckoutBody { priceId: string; accountId: string;
}
// Создание сессии оформления заказа
router.post( '/create-checkout-session', async (req: Request<{}, {}, CheckoutBody>, res: Response) => { const { priceId, accountId } = req.body; try { // Получаем цену, чтобы определить, является ли она // разовой или повторяющейся const price = await stripe.prices.retrieve(priceId, { stripeAccount: accountId, }); const isSubscription = price.type === 'recurring'; const mode = isSubscription ? 'subscription' : 'payment';
const session = await stripe.checkout.sessions.create( { line_items: [ { price: priceId, quantity: 1, }, ], mode, success_url: `${process.env.DOMAIN}/done?session_id={CHECKOUT_SESSION_ID}`, cancel_url: `${process.env.DOMAIN}`, ...(isSubscription ? { subscription_data: { application_fee_percent: 10, }, } : { payment_intent_data: { application_fee_amount: 123, }, }), }, { stripeAccount: accountId }, );
res.redirect(303, session.url as string); } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; res.status(500).json({ error: message }); } },
);

Вот что делает этот маршрут шаг за шагом. Сначала он получает цену из подключённого аккаунта продавца, чтобы определить, является ли она разовой или повторяющейся подпиской. Затем создаёт Checkout Session с соответствующим режимом — либо "payment", либо "subscription".

application_fee_amount — это доля платформы от транзакции, указанная в наименьшей единице валюты (центах для USD). В данном примере вы берёте $1.23 или 10% с каждой транзакции. В реальном маркетплейсе это значение, скорее всего, рассчитывалось бы как процент от цены продукта.

Обратите внимание: application_fee_amount помещается внутрь subscription_data для подписок, но внутрь payment_intent_data для разовых платежей. Это требование Stripe — два режима используют разные объекты конфигурации.

Наконец, маршрут использует res.redirect(303, session.url), чтобы отправить покупателя напрямую на размещённую Stripe страницу оформления заказа.

Как обрабатывать вебхуки

Вебхуки — это способ, которым Stripe уведомляет ваш сервер о событиях, происходящих асинхронно: успешный платёж, неудавшееся списание или отмена подписки.

В продакшн-маркетплейсе никогда не следует полагаться исключительно на URL-адреса перенаправления для подтверждения платежей. Покупатель может закрыть браузер до завершения перенаправления. Вебхуки — ваш источник истины.

Добавьте эндпоинт вебхука до JSON body parser. Stripe отправляет полезные нагрузки вебхуков в виде сырых байтов, и вам нужно необработанное тело для проверки подписи:

// ВАЖНО: Зарегистрируйте это ДО app.use(express.json())
app.post( '/api/webhook', express.raw({ type: 'application/json' }), (req: Request, res: Response) => { let event: Stripe.Event = JSON.parse(req.body.toString());
// Если у вас есть секрет эндпоинта, проверьте // подпись для безопасности const endpointSecret = process.env.WEBHOOK_SECRET; if (endpointSecret) { const signature = req.headers['stripe-signature'] as string; try { event = stripe.webhooks.constructEvent( req.body, signature, endpointSecret, ) as Stripe.Event; } catch (err) { const message = err instanceof Error ? err.message : 'Unknown error'; console.log('Webhook signature verification failed:', message); res.sendStatus(400); return; } }
// Обрабатываем событие switch (event.type) { case 'checkout.session.completed': { const session = event.data.object as Stripe.Checkout.Session; console.log('Payment successful for session:', session.id); // Выполняем заказ: отправляем email, предоставляем доступ, // обновляем записи и т.д. break; } case 'checkout.session.expired': { const session = event.data.object as Stripe.Checkout.Session; console.log('Session expired:', session.id); break; } case 'checkout.session.async_payment_succeeded': { const session = event.data.object as Stripe.Checkout.Session; console.log('Delayed payment succeeded for session:', session.id); break; } case 'checkout.session.async_payment_failed': { const session = event.data.object as Stripe.Checkout.Session; console.log('Payment failed for session:', session.id); break; } case 'customer.subscription.deleted': { const subscription = event.data.object as Stripe.Subscription; console.log('Subscription cancelled:', subscription.id); break; } default: console.log('Unhandled event type:', event.type); }
res.send(); },
);

Обработчик вебхуков отслеживает пять ключевых событий:

  • checkout.session.completed срабатывает при успешном платеже — здесь вы выполняете заказ, отправляете письмо с подтверждением или предоставляете доступ
  • checkout.session.expired срабатывает, когда сессия истекает до того, как покупатель завершил оплату
  • checkout.session.async_payment_succeeded срабатывает, когда отложенный способ оплаты (например, банковский перевод) наконец проходит
  • checkout.session.async_payment_failed срабатывает, когда отложенный способ оплаты не проходит
  • customer.subscription.deleted срабатывает при отмене подписки

Как настроить вебхуки в панели управления Stripe

Прежде чем получать события вебхуков, нужно сообщить Stripe, куда их отправлять и какие события вас интересуют. Выполните следующие шаги:

  1. Перейдите в панель управления Stripe и откройте раздел Developers > Webhooks
  2. Нажмите "Add destination"
  3. В разделе типа аккаунта выберите "Connected and V2 accounts", поскольку ваши платежи проходят через подключённые аккаунты продавцов
  4. В разделе "Events to listen for" нажмите "All events" и выберите следующие пять событий:
  • checkout.session.async_payment_succeeded — происходит, когда платёжное намерение с использованием отложенного способа оплаты наконец успешно выполняется
  • checkout.session.completed — происходит, когда Checkout Session успешно завершена
  • checkout.session.expired — происходит, когда Checkout Session истекает до завершения
  • checkout.session.async_payment_failed — происходит, когда платёжное намерение с использованием отложенного способа оплаты не выполняется
  1. Введите URL вашего эндпоинта вебхука. Для продакшна это будет что-то вроде https://yourdomain.com/api/webhook. Для локальной разработки вместо этого используется Stripe CLI (рассматривается далее)
  2. Нажмите "Add destination" для сохранения

Как тестировать вебхуки локально

Для локальной разработки не нужно открывать сервер в интернет. Установите Stripe CLI и выполните:

brew install stripe/stripe-cli/stripe
stripe login
stripe listen --forward-to localhost:4242/webhook

CLI выведет секрет подписи вебхука, начинающийся с whsec_. Добавьте его в файл .env как WEBHOOK_SECRET. CLI перехватывает все события вебхуков от Stripe и перенаправляет их на ваш локальный сервер, так что вы можете тестировать полный платёжный процесс без развёртывания чего-либо.

Как добавить биллинговый портал

// Создание сессии биллингового портала
router.post('/create-portal-session', async (req: Request, res: Response) => { const { session_id } = req.body as { session_id: string }; try { const session = await stripe.checkout.sessions.retrieve(session_id); const portalSession = await stripe.billingPortal.sessions.create({ customer_account: session.customer_account as string, return_url: `${process.env.DOMAIN}?session_id=${session_id}`, }); res.redirect(303, portalSession.url); } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; res.status(500).json({ error: message }); }
});

Этот маршрут принимает session_id из предыдущей сессии оформления заказа, получает связанного покупателя и создаёт сессию биллингового портала. Поле customer_account связывает портал с правильным подключённым аккаунтом, чтобы покупатель видел только свои подписки у конкретного продавца.

Теперь добавьте JSON parser и подключите роутер — это должно идти после маршрута вебхука:

app.use(express.json());
app.use('/api', router);
app.listen(4242, () => console.log('Server running on port 4242'));

Как построить фронтенд на Next.js

Перейдите в директорию client и создайте новый проект Next.js с TypeScript:

cd client
npx create-next-app@latest . --typescript --tailwind --app

Как создать контекст аккаунта

Вам нужен способ передавать ID аккаунта продавца во все компоненты. Создайте провайдер контекста (context provider) по пути contexts/AccountContext.tsx:

'use client';
import { createContext, useContext, useState, ReactNode } from 'react';
import { useSearchParams } from 'next/navigation';
interface AccountContextType { accountId: string | null; setAccountId: (id: string | null) => void;
}
const AccountContext = createContext<AccountContextType | undefined>(undefined);
export function useAccount(): AccountContextType { const context = useContext(AccountContext); if (!context) { throw new Error('useAccount must be used within AccountProvider'); } return context;
}
export function AccountProvider({ children }: { children: ReactNode }) { const searchParams = useSearchParams(); const [accountId, setAccountId] = useState<string | null>( searchParams.get('accountId'), ); return ( <AccountContext.Provider value={{ accountId, setAccountId }}> {children} </AccountContext.Provider> );
}

Этот контекст хранит ID аккаунта текущего продавца и делает его доступным во всём приложении. При первоначальной загрузке он проверяет URL на наличие параметра запроса accountId — именно так редирект после онбординга Stripe передаёт ID аккаунта обратно в ваше приложение.

Как создать хук статуса аккаунта

Создайте пользовательский хук (custom hook) по пути hooks/useAccountStatus.ts, который опрашивает статус аккаунта:

'use client';
import { useState, useEffect } from 'react';
import { useAccount } from '@/contexts/AccountContext';
interface AccountStatus { id: string; payoutsEnabled: boolean; chargesEnabled: boolean; detailsSubmitted: boolean;
}
export default function useAccountStatus() { const [accountStatus, setAccountStatus] = useState<AccountStatus | null>(null); const { accountId, setAccountId } = useAccount();
useEffect(() => { if (!accountId) return;
const fetchStatus = async () => { try { const res = await fetch(`http://localhost:4242/api/account-status/${accountId}`); if (!res.ok) throw new Error('Failed to fetch'); const data: AccountStatus = await res.json(); setAccountStatus(data); } catch { setAccountId(null); } };
fetchStatus(); const interval = setInterval(fetchStatus, 5000); return () => clearInterval(interval); }, [accountId, setAccountId]);
return { accountStatus, needsOnboarding: !accountStatus?.chargesEnabled && !accountStatus?.detailsSubmitted, };
}

Этот хук опрашивает статус аккаунта каждые 5 секунд. Это важно, потому что онбординг Stripe является асинхронным — продавец может заполнить форму, но Stripe может потребоваться некоторое время для проверки его данных и активации аккаунта. Флаг needsOnboarding сообщает вашему UI, нужно ли показывать кнопку онбординга или панель управления продавца.

Как создать компонент онбординга продавца

Создайте components/ConnectOnboarding.tsx:

'use client';
import { useState } from 'react';
import { useAccount } from '@/contexts/AccountContext';
import useAccountStatus from '@/hooks/useAccountStatus';
const API_URL = 'http://localhost:4242/api';
export default function ConnectOnboarding() {
  const [email, setEmail] = useState('');
  const { accountId, setAccountId } = useAccount();
  const { accountStatus, needsOnboarding } = useAccountStatus();
  async function handleCreateAccount() {
    const res = await fetch(`${API_URL}/create-connect-account`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email }),
    });
    const data = await res.json();
    setAccountId(data.accountId);
  }
  if (!accountId) {
    return <SellerSignupForm email={email} onChange={setEmail} onSubmit={handleCreateAccount} />;
  }
  return <SellerStatusCard accountId={accountId} status={accountStatus} needsOnboarding={needsOnboarding} />;
}

В боевом приложении лучше разнести этот компонент на две части: SellerSignupForm и SellerStatusCard. Тогда ветка создания аккаунта отвечает только за email и вызов /create-connect-account, а ветка статуса занимается показом chargesEnabled, payoutsEnabled и кнопкой запуска онбординга.

Важно и то, что в production-коде интерфейс должен быть дружелюбнее к продавцу. Вместо длинных JSX-блоков с вшитыми строками лучше использовать отдельные компоненты формы, явные подписи полей и статусы верификации, чтобы Stripe Connect выглядел как часть вашего продукта, а не как техническая заглушка.

Как создать форму товара, список товаров и оформление заказа

Создайте components/Products.tsx:

'use client';
import { useState, useEffect } from 'react';
import { useAccount } from '@/contexts/AccountContext';
import useAccountStatus from '@/hooks/useAccountStatus';
interface Product {
  id: string;
  name: string;
  description: string | null;
  price: number | null;
  priceId: string;
  period: string | null;
}
export default function Products() {
  const { accountId } = useAccount();
  const { needsOnboarding } = useAccountStatus();
  const [products, setProducts] = useState<Product[]>([]);
  useEffect(() => {
    if (!accountId || needsOnboarding) return;
    fetchProductsForSeller(accountId).then(setProducts);
  }, [accountId, needsOnboarding]);
  return <ProductGrid products={products} accountId={accountId} />;
}

Здесь снова полезно отделить инфраструктурную часть от UI. Компонент Products должен лишь подгружать данные и следить за тем, что продавец прошёл онбординг. Отрисовку сетки карточек и переход к create-checkout-session лучше вынести в отдельный ProductGrid, чтобы в коде не смешивались запросы, состояние и верстка.

Такой разнос даёт практическую пользу: при добавлении фильтров, пагинации или разных типов предложений вам не придётся переписывать весь экран. Более того, можно отдельно тестировать получение списка товаров и отдельно компонент карточки, что особенно важно для маркетплейса со множеством сценариев продажи.

Как создать форму товара

Продавцам нужен способ добавлять продукты через фронтенд. Создайте components/ProductForm.tsx:

type ProductFormData = {
  productName: string;
  productDescription: string;
  productPrice: number;
};
async function handleSubmit(formData: ProductFormData, accountId: string) {
  await fetch(`${API_URL}/create-product`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ ...formData, accountId }),
  });
}

Самое важное правило здесь — не показывать форму, пока продавец не завершил онбординг. Это избавляет от лишних ошибок и не позволяет создавать товары на аккаунт, который ещё не может принимать платежи. В UI лучше явно сообщать, на каком шаге находится продавец: создание аккаунта, проверка Stripe или уже активные продажи.

Цена передаётся в центах, потому что так Stripe хранит денежные значения. На практике стоит сделать поле с обычной денежной маской и конвертировать сумму в минимальные единицы уже перед отправкой. Тогда продавец вводит привычные 25.00, а ваш код сохраняет корректное значение 2500.

Как создать главную страницу

Соберите всё вместе в app/page.tsx:

import { Suspense } from 'react';
import { AccountProvider } from '@/contexts/AccountContext';
import ConnectOnboarding from '@/components/ConnectOnboarding';
import ProductForm from '@/components/ProductForm';
import Products from '@/components/Products';
export default function Home() { return ( <Suspense fallback={<div>Loading...</div>}> <AccountProvider> <main className="max-w-4xl mx-auto p-8"> <h1 className="text-3xl font-bold mb-8">Marketplace</h1> <ConnectOnboarding /> <ProductForm /> <Products /> </main> </AccountProvider> </Suspense> );
}

Как протестировать полный сценарий

Запустите оба сервера:

cd server
npm run dev
cd ../client
npm run dev

Затем пройдите полный сценарий:

  1. Откройте http://localhost:3000 и введите email для создания аккаунта продавца
  2. Нажмите "Complete Onboarding" и заполните форму онбординга Stripe (используйте тестовые данные)
  3. После возврата на ваш сайт добавьте товар через форму
  4. Используйте тестовую карту Stripe 4242 4242 4242 4242 для завершения платежа

Как работает разделение платежа

Вот что именно происходит, когда покупатель платит $25.00 за товар:

  1. Покупатель платит $25.00 на странице оформления заказа Stripe
  2. Stripe вычитает свою комиссию за обработку (приблизительно 2.9% + $0.30 для карт США)
  3. Ваша платформа берёт установленную вами комиссию приложения ($1.23 в нашем примере)
  4. Оставшаяся сумма переводится на подключённый аккаунт Stripe продавца
  5. Продавец может вывести свои средства на банковский счёт из Stripe Dashboard

Вы управляете комиссией приложения в маршруте оформления заказа. В производственном маркетплейсе вы бы рассчитывали её как процент от транзакции. Например, чтобы взять комиссию 10%:

payment_intent_data: { application_fee_amount: Math.round(productPrice * 0.1),
}

Мне нравится именно этот подход: вся логика разделения платежей остаётся на стороне Stripe, а ваш код остаётся чистым и предсказуемым.

Следующие шаги

Теперь у вас есть работающий маркетплейс. Вот улучшения, которые стоит рассмотреть для продакшена:

  • Добавьте аутентификацию с NextAuth.js, чтобы продавцы могли безопасно входить в систему и управлять своими аккаунтами между сессиями
  • Добавьте валидацию во время выполнения с Zod для проверки всех тел запросов до того, как они достигнут Stripe
  • Добавьте загрузку изображений для товаров с помощью Cloudinary или AWS S3, затем передайте URL изображения в метаданные продукта Stripe
  • Создайте отдельные представления для продавцов и покупателей — сейчас приложение объединяет оба сценария на одной странице
  • Разверните бэкенд на Railway или Render, а фронтенд на Vercel. Обновите URL вебхука в Stripe Dashboard, чтобы он указывал на ваш производственный сервер

Полный исходный код этого руководства можно найти на GitHub: https://github.com/michaelokolo/marketplace

Благодарности

Некоторые паттерны использования API в этом руководстве вдохновлены примерами из официальной документации Stripe. Эти примеры были адаптированы для демонстрации того, как создать полноценную архитектуру маркетплейса с несколькими продавцами.

В этом руководстве вы создали полноценный онлайн-маркетплейс, где продавцы могут проходить онбординг через Stripe Connect, создавать товары, хранящиеся непосредственно в Stripe, и получать платежи от покупателей — всё это без традиционной базы данных.

Ключевое понимание состоит в том, что Stripe Connect берёт на себя самые сложные части управления маркетплейсом — разделение платежей, соответствие налоговым требованиям, верификацию личности и переводы средств. Ваша задача — создать отличный пользовательский опыт поверх этого.

Ответы на эти вопросы могут быть для вас полезными

Можно ли использовать этот подход без базы данных в продакшне?

Да, для многих маркетплейсов Stripe полностью заменяет базу данных для хранения товаров, цен и данных покупателей. Если вам нужно хранить дополнительные метаданные (например, профили продавцов или историю заказов), базу данных можно добавить позже, не переписывая платёжную логику.

Что произойдёт, если покупатель закроет браузер во время оплаты?

Именно для этого нужны вебхуки. Событие checkout.session.completed придёт на ваш сервер независимо от того, дождался ли покупатель редиректа. Никогда не полагайтесь только на success_url для подтверждения платежа.

Как изменить размер комиссии платформы?

Для разовых платежей измените значение application_fee_amount в payment_intent_data — оно указывается в центах. Для подписок измените application_fee_percent в subscription_data — это процент от каждого списания. Оба параметра задаются в маршруте /create-checkout-session.

Как продавец выводит заработанные деньги?

Продавец входит в свою панель управления Stripe (доступ к ней открывается параметром dashboard: "full" при создании аккаунта) и инициирует выплату на привязанный банковский счёт. Stripe автоматически рассчитывает доступный баланс после вычета комиссии платформы.

Поддерживает ли этот маркетплейс продавцов из разных стран?

Да, но при создании аккаунта нужно указать правильный код страны в поле identity.country. Stripe поддерживает подключённые аккаунты в большинстве стран, однако доступные способы оплаты и требования к верификации могут отличаться в зависимости от региона.

Оцените статью
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x