MoosanClub
Production e-commerce platform with server-side rendering, authentication, Stripe payments, and a hand-rolled request cache.
- Role
- Lead frontend engineer
- Year
- 2024
- Next.js
- TypeScript
- Stripe
- SSR
- PostgreSQL
Problem
MoosanClub needed a storefront that felt instant on first load and stayed correct under real traffic: authenticated carts, live inventory, and Stripe checkout that could never double-charge. The earlier build leaned entirely on client-side fetching, so the first paint was a spinner and search engines saw an empty shell.
- First contentful paint gated behind a client-side data waterfall
- Cart and inventory state drifting out of sync across tabs
- No server-rendered product pages for SEO or social previews
Approach
I rebuilt the product and category pages as server-rendered routes so the meaningful content ships in the first HTML response, then hydrated only the interactive islands — cart, quantity steppers, checkout.
Rather than pull in a heavy data-fetching dependency, I wrote a small SWR-style cache keyed by request, with revalidation on focus and mutation. It kept the client bundle lean while giving carts optimistic updates that reconcile against the server.
- SSR product/category pages; interactive islands hydrated selectively
- Hand-rolled stale-while-revalidate cache — no extra runtime dependency
- Stripe checkout with idempotency keys to make retries safe
Tradeoffs
Writing the cache by hand meant owning invalidation logic that a library would hand you for free. The payoff was a materially smaller bundle and full control over revalidation timing; the cost was more tests and a higher bar for onboarding new contributors.
Server-rendering everything shifted load onto the origin, so I had to be deliberate about caching headers and which routes could be statically generated versus rendered per request.
Outcome
Product pages went from a spinner-first experience to server-rendered content on first byte, and checkout reliability improved once retries became idempotent. The lean cache kept the JavaScript budget well under the target while still feeling live.
- Server-rendered content on first response across the catalogue
- Idempotent checkout — retries no longer risk double charges
- Client bundle kept small by avoiding a data-fetching dependency