Skip to content
Rahul Nainala
ShippedOn my own · 2024–2609 / 15

Folio

A full-stack bookstore where checkout can't oversell: React, Express and Postgres in one typed monorepo.

Folio's home page: a serif headline, buttons to browse the catalog or try a demo account, three book covers and genre filters with counts
Home: browse the catalog, or sign in as a demo customer or admin with one click.
books, searchable by title, author and ISBN
66
API tests against a real Postgres, not mocks
37
web tests, plus 5 Playwright end-to-end flows
11
origin: /api is proxied, so no CORS and an httpOnly cookie
1

Overview

Folio is an online bookstore I built to practise the whole stack end to end: a React 19 storefront on Vite, an Express 5 API on Drizzle and Postgres, and Zod schemas shared by both in an npm workspaces monorepo. It covers the full flow: a searchable catalog, a cart that works signed out and merges when you sign in, checkout, order history and an admin dashboard. The API is tested against a real Postgres rather than mocks.

More screens

Folio's catalog filtered to Science Fiction: a genre and price sidebar, a sort menu and a grid of books with ratings and prices
Catalog: search, genre, price and stock filters live in the URL, so a search can be shared.
Folio's checkout as the demo customer: a shipping form, an order summary with two books, and a test-mode payment notice
Checkout: one transaction locks the rows, checks stock and places the order.

Problem → built → result

Problem
A shop's worst bug is selling the last copy twice.
Built
React + Vite storefront, Express 5 API on Drizzle and Postgres, Zod schemas shared by both.
Result
37 API tests on a real Postgres · checkout can't oversell

What I built

  • Checkout in one transaction with row locks, and a test that races three buyers for the last copy
  • A cart that works before sign-in and merges into the account on sign-in
  • Request schemas shared by the API, the forms and the generated OpenAPI docs
  • Admin dashboard with a revenue chart, top sellers and low stock, plus editing books, authors, genres and order status
  • Vitest + Supertest against a real Postgres, Testing Library on the web app and Playwright end to end, in GitHub Actions

Decisions

  1. 1

    Lock the rows, not the code path.

    Placing an order locks the book rows, checks stock, copies the prices into the order, decrements stock and clears the cart in one transaction. A test sends three buyers at the last copy at once and checks that exactly one gets it.

  2. 2

    Define validation once.

    The request schemas live in a shared package: the API validates with them, the forms use them and the OpenAPI docs are generated from them, so the three can't disagree.

  3. 3

    One origin instead of CORS.

    Vercel proxies /api to the API, and Vite does the same in development, so the session is a normal first-party httpOnly cookie: no token in localStorage, no CORS setup.

  4. 4

    Make the demo hard to wreck.

    The demo admin can edit anything but can't delete the original catalog, login is rate limited, and a GitHub Action reseeds the database every night.

Why it's shaped this way

Correctness lives in the database: the transaction and its row locks decide who gets the last copy, so no amount of UI timing can sell it twice.