DEV Community

Ajith
Ajith

Posted on

Fashion E-Commerce Store

When I started this project, the brief was simple to state and deceptively large to build: a full fashion e-commerce platform — product catalog, size and color variants, filters, an "AI-style" size recommender, cart, checkout, JWT auth, order tracking, and a wishlist. Here's a walkthrough of how it came together, the decisions behind the architecture, and a few lessons learned along the way.

Why FastAPI + MySQL

FastAPI was the obvious choice for a REST backend like this. Automatic OpenAPI docs (you get /docs for free), native Pydantic validation, and async support made it easy to move fast without sacrificing structure. MySQL paired with SQLAlchemy's ORM gave a relational model that fits e-commerce data naturally — products, variants, orders, and users all have clean foreign-key relationships that a document store would make awkward.

The stack ended up being:

Backend: FastAPI + Pydantic v2
Database: MySQL 8.x via SQLAlchemy ORM, with Alembic for migrations
Auth: JWT (OAuth2 password flow) + bcrypt password hashing
Frontend: Server-rendered Bootstrap 5 pages with vanilla JS calling the same REST API a mobile app would use
Designing the catalog: the size/color variant problem

The trickiest early design decision was how to model a product that comes in multiple sizes and colors, each with its own stock count. The naive approach — a single stock field on the product — falls apart the moment you need "Medium, Black" to sell out while "Large, Black" is still available.

The fix was a separate ProductVariant table: one row per size+color combination, each with its own SKU and stock quantity. The Product table holds shared data (name, description, price, category), while ProductVariant handles everything that varies by selection. This is the same pattern most real fashion retailers use under the hood, and it made the cart and checkout logic much simpler — every cart line item points to a specific variant, not just a product.

Product (1) ──< ProductVariant (many)
"Classic Crew Neck T-Shirt" S/Black, M/Black, L/Black,
S/White, M/White, ...
The size recommendation engine

The brief called for "AI-based size recommendations." Rather than reaching for a trained ML model on day one (which needs a lot of labeled fit data most fashion startups don't have yet), I built a rule-based measurement-matching engine instead — one that's explainable, debuggable, and immediately useful with just a size chart.

Here's the core idea: each product category has a SizeChart with min/max chest, waist, hip, and height ranges per size. When a user submits their measurements, the engine scores every size in the chart:

A perfect fit inside the range scores 1.0
A near-miss just outside the range gets partial credit that decays with distance
Missing measurements are skipped, not penalized

Then it layers on two nice touches: a fit preference (snug/regular/loose) nudges the result up or down a size, and if the user tells us a previous size "ran small," that feedback becomes a strong prior for the next recommendation.

The result isn't a black box — it comes back with a confidence score and a plain-language explanation ("Your chest and waist measurements best matched size M"). That transparency matters more than raw accuracy for a first version: customers trust a system they can question. And because it's isolated in one function, swapping in a real ML model later is a drop-in replacement, not a rewrite.

Checkout and the payment gateway abstraction

Checkout needed to feel realistic without wiring up real payment credentials for a demo project. The solution was a small PaymentGateway interface with a mock implementation underneath:

python
class PaymentGateway(ABC):
@abstractmethod
def charge(self, amount, method, token, order_number) -> PaymentResult:
...

Cash-on-delivery orders confirm instantly. Card and UPI payments simulate authorization with realistic latency and even include a deterministic failure hook for testing declined payments. Because every router depends on the interface rather than a specific provider, adding a real Stripe or Razorpay integration later means writing one new class and flipping a config value — no changes to the checkout endpoint itself.

The checkout flow also had to guard against a subtle race condition: a user adds an item to their cart, someone else buys the last unit, and now the original user tries to check out. The endpoint re-validates stock for every line item at checkout time, not just when items were added to the cart, and only decrements stock after payment succeeds.

Auth, and a real bug worth mentioning

Password hashing went through bcrypt via passlib initially — the standard, well-documented choice. But testing surfaced a real compatibility issue: newer versions of the bcrypt package dropped an internal attribute that older passlib versions probe for during version detection, causing spurious failures even though the underlying hashing worked fine. The fix was to call bcrypt directly instead of routing through passlib's compatibility layer — fewer moving parts, and one less place for library version drift to cause silent breakage. It's a good reminder that "well-established library" doesn't mean "immune to version mismatches," and that testing the actual auth flow end-to-end (not just reading the docs) catches things a code review alone wouldn't.

What's next

A few things I'd tackle if this were headed to production rather than a portfolio piece: rate limiting on the login/register endpoints, moving product images to real object storage instead of placeholder URLs, and swapping the mock payment gateway for a real processor. The architecture is set up to make all three straightforward additions rather than rewrites — which, in the end, was the real goal of this project: not just shipping features, but shipping them in a shape that can grow.

Built as part of a Python full-stack internship project. Full source, setup instructions, and API docs available in the project README.

Top comments (0)