B2B E-Commerce Platform
Full-stack B2B platform for construction materials built with DDD, Clean Architecture, NestJS, and React.
- TypeScript
- React
- Node.js
- NestJS
- PostgreSQL
- AWS
- Tailwind CSS
- Period
- 2023 – 2024
A construction materials distributor was running their B2B sales entirely through WhatsApp and spreadsheets. Buyers had no visibility into stock, pricing varied by region with no systematic rules, and the sales team spent hours a day manually processing orders and generating quotes. The goal was to build a proper B2B e-commerce platform from scratch — one that could serve multiple buyer profiles, handle regional pricing, and eventually replace the manual workflow entirely.
I was the sole engineer responsible for the full system design and implementation, from database schema to deployment.
System design
The backend is a NestJS monolith split into bounded contexts: catalog (products, categories, stock), pricing (regional price tables, buyer tiers), orders (cart, checkout, order lifecycle), and identity (buyer accounts, ACL roles). Each context owns its own database tables and exposes a well-defined application layer with use cases and port interfaces. Infrastructure concerns — Prisma repositories, S3 adapters, email senders — are isolated behind those ports.
The API follows REST conventions with a consistent envelope: every response is { data, meta, error }. Validation uses class-validator decorators at the DTO layer; domain invariants are enforced inside aggregates and return domain errors rather than throwing.
| Bounded context | Responsibility |
|---|---|
| catalog | Products, categories, stock levels |
| pricing | Regional price tables, buyer tiers |
| orders | Cart, checkout, order lifecycle |
| identity | Buyer accounts, ACL roles |
Frontend
The customer-facing portal is a React + Vite SPA that consumes the API via TanStack Query. Cart operations use optimistic updates so the UI feels instant even on slower connections. Product listing pages have server-side filtering via query params so URLs are shareable and back-navigation works correctly.
The backoffice portal (for the sales team and admins) shares the same Tailwind design system but has a completely different layout and permission model. Role-based access is enforced both in the API (NestJS guards reading JWT claims) and in the UI (route guards and conditional rendering based on the decoded token).
Pricing engine
One of the trickier parts was the regional pricing engine. Prices are defined in tiers: a base national price, state-level overrides, and buyer-specific negotiated prices. The resolution order is buyer-specific → state → national. This logic lives in a dedicated PriceResolver domain service that the order and cart use cases call — the frontend never calculates prices, it always fetches the resolved price from the API.
// Resolution chain: buyer override → state override → national base
const resolved = priceResolver.resolve({
productId,
buyerId,
stateCode: buyer.address.state,
});
Infrastructure
Deployed on AWS with EC2 for the API, RDS PostgreSQL for the database, and S3 for product images. Images go through a pre-signed URL upload flow — the client gets a signed S3 URL from the API, uploads directly to S3, and then confirms the upload by sending the key back to the API. This kept media traffic off the API server entirely.
Database migrations are managed per environment with Prisma Migrate. A GitHub Actions pipeline runs migrations and deploys the new API build on every merge to main.
Other projects
Personal Portfolio
A full-stack portfolio built with Next.js, DDD, and Clean Architecture in a Turborepo monorepo.
- TypeScript
- React