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

- 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


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
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
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
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
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.