A learning-focused ecommerce platform built with Next.js App Router. It demonstrates catalog and checkout flows, Clerk authentication, resilient API access, intentional rendering (SSG/ISR), and production-oriented patterns (tests, CI, observability).
Live: mini-ecommerce-nextjs-psi.vercel.app
Repo: github.com/alejosworkstuff/mini-ecommerce-nextjs
| Home | Product catalog | Cart | Clerk sign-in |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
I wanted a project that goes beyond static UI and reflects real commerce behavior: dynamic catalog pages, cart/session handling, multi-step checkout, authenticated account areas, and API-backed interactions.
The app showcases frontend-heavy product flows plus backend patterns (REST, GraphQL, Postgres + Prisma, Redis caching, WebSockets, Docker, and deployment scaffolding).
- Built the full application architecture across UI, API routes, and React contexts
- Implemented product, cart, checkout, favorites, collections, and order flows
- Added Clerk auth (proxy middleware, protected routes, per-user orders)
- Implemented a resilient HTTP client (timeout, retry, typed errors) and SSG/ISR catalog pages
- Integrated Sentry, error boundaries, and structured API logging
- Added Vitest + Playwright tests and GitHub Actions CI (including E2E on push)
- Set up Docker Compose and AWS ECS deployment scaffolding
- Next.js 16 (App Router), React 19, TypeScript 5 (strict)
- Tailwind CSS 3
- Clerk (
@clerk/nextjs) — OIDC sessions, protected routes - Sentry (
@sentry/nextjs) — optional error monitoring - Node.js route handlers (REST + GraphQL)
- PostgreSQL (Neon) + Prisma — orders (
Order,OrderItem) - Redis — cart sessions and product cache
- WebSockets (
ws) - Vitest 4, Testing Library, Playwright
- Docker / Docker Compose, GitHub Actions
mini-ecommerce/
├── src/app/ # App Router pages, API routes, error boundaries
├── src/app/context/ # Cart, orders, favorites, collections, messages, realtime
├── src/components/ # UI, ProductsCatalog, ProductDetailView, AccountMenu
├── src/hooks/ # useDebouncedValue, useAsyncResource, useApiMutation, …
├── src/lib/ # http-client, api-client, product-data, order-store, prisma
├── prisma/ # schema + migrations (orders)
├── src/proxy.ts # Clerk clerkMiddleware + protected routes
├── docs/ # Architecture, DoD, testing strategy
├── e2e/ # Playwright specs
├── realtime-server.mjs # WebSocket gateway (local/Docker)
├── aws/ # ECS task definition and deploy notes
└── .github/workflows/ # CI (+ optional AWS deploy)
See docs/frontend-architecture.md for route-level rendering decisions and the Server Action cart sync flow.
Cart writes use a Server Action instead of a client fetch to the REST route:
CartContext (client) ──► syncCartAction() ──► cart-store.ts ──► Redis
- Action:
src/app/actions/cart.ts—syncCartAction(sessionId, items) - Shared logic:
src/lib/cart-store.ts— also used byGET|PUT /api/cart/[sessionId]for tests and external callers - Reads: still
GET /api/cart/[sessionId]viafetchRemoteCarton mount (hydration from Redis)
| Path | Purpose | Access |
|---|---|---|
/ |
Home | Public |
/products |
Product listing (server data + client filters/search) | Public |
/product/[id] |
Product detail (SSG/ISR + client cart/favorites) | Public |
/cart, /checkout/* |
Cart and checkout flow | Public |
/sign-in, /sign-up |
Clerk auth UI | Public |
/my-purchases |
Order history | Signed in |
/favorites, /collections, /messages, /settings |
Account features | Signed in |
/admin/orders |
All orders (demo admin) | Signed in + role: admin in Clerk |
REST
GET /api/products,GET /api/products/[id]GET|POST|PATCH /api/orders— requires Clerk session; scoped byuserIdGET|PUT /api/cart/[sessionId]— REST surface; client cart sync uses Server Action (syncCartAction) instead ofPUT
GraphQL — POST /api/graphql (products public; orders require auth)
Realtime — realtime-server.mjs on port 4001. Override with NEXT_PUBLIC_WS_URL; otherwise the browser connects to ws://<page-host>:4001 (works with Docker Compose port mapping).
Orders and cart data use different stores on purpose — each fits the access pattern:
| Data | Store | Why |
|---|---|---|
Cart (/api/cart/[sessionId]) |
Redis (or in-memory Map without REDIS_URL) |
Session-scoped, high-churn key/value; no relational queries needed |
Product cache (/api/products) |
Redis | Short-lived catalog cache |
Orders (/api/orders, GraphQL createOrder) |
PostgreSQL via Prisma | Relational Order + OrderItem rows; per-user history and admin queries |
Migration story: v1 stored orders as Redis JSON blobs (orders:user:{userId}) for a fast demo. Phase 6 swapped the order-store.ts adapter to Prisma + Postgres on Neon without changing the public API — same REST/GraphQL surface, Clerk userId scoping unchanged. Cart stayed on Redis.
Production: Neon (neon-cerulean-curtain) via Vercel Marketplace; pooled DATABASE_URL with ?pgbouncer=true. Migrations run on deploy: prisma migrate deploy && next build.
Legacy data cutover: the app no longer writes orders to Redis. A one-off script handles any leftover orders:user:* blobs from the v1 Redis store — optionally backfilling them into Postgres, then deleting the stale keys. It is dry-run by default and idempotent:
npm run orders:migrate-legacy # dry run (report only)
npm run orders:migrate-legacy -- --apply # backfill to Postgres + delete Redis keys
npm run orders:migrate-legacy -- --apply --skip-backfill # delete keys only
npm run orders:migrate-legacy -- --apply --keep-redis # backfill onlyLocal options (pick one):
- A — Docker Compose:
docker compose up postgresstarts thepostgres:16-alpineservice; useDATABASE_URL=postgresql://postgres:postgres@localhost:5432/mini_ecommerce. - B — Prisma local Postgres (no Docker):
npx prisma devstarts a local Prisma Postgres instance and prints aprisma+postgres://...URL — copy it intoDATABASE_URLin.env.local, then runnpm run db:migratein a second terminal. - C — Neon dev branch: point
DATABASE_URLat a Neon branch connection string.
npx prisma dev # option B: start a local Prisma Postgres (keep running)
npm run db:migrate # apply migrations
npm run db:studio # Prisma Studio (local)- Product catalog with SSG/ISR detail pages, debounced search, and filters
- Cart with session-backed persistence and multi-step checkout
- Clerk authentication — sign-in/up,
UserButton, protected account routes - Favorites, collections, messages (demo), and per-user order history
- HTTP resilience — centralized client with timeout, retry,
AppError,ApiState<T> - Dark mode, Open Graph metadata, REST + GraphQL, Redis cart cache + Postgres orders, WebSocket demos
- Observability — Sentry (optional),
error.tsx,ErrorBoundary, structured logs
clerkMiddleware()insrc/proxy.ts(includes/__clerk/:path*matcher)<ClerkProvider>insrc/app/layout.tsx- Header:
<Show>,<SignInButton>,<SignUpButton>,<UserButton>+ account menu - Server routes use
auth()from@clerk/nextjs/serverfor orders - Admin: set
publicMetadata.roleto"admin"in Clerk for/admin/orders
Local setup: copy .env.example → .env.local and add keys from dashboard.clerk.com.
Vercel: add the same variables in Project → Settings → Environment Variables (never commit real keys).
| Variable | Required | Purpose |
|---|---|---|
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY |
Yes (auth) | Clerk publishable key |
CLERK_SECRET_KEY |
Yes (auth) | Clerk secret key (server only) |
NEXT_PUBLIC_SENTRY_DSN |
No | Sentry DSN (client + server) |
SENTRY_AUTH_TOKEN |
No | Sentry upload token (builds) |
NEXT_PUBLIC_API_BASE_URL |
No | API base override (empty = same origin) |
NEXT_PUBLIC_WS_URL |
No | WebSocket URL override; default is ws://<page-host>:4001 in the browser |
REDIS_URL |
No | Redis for cart + product cache (default redis://localhost:6379; in-memory fallback if unset) |
DATABASE_URL |
Yes (orders) | Postgres connection string — use Neon pooler URL in production (?pgbouncer=true) |
npm install
cp .env.example .env.local # then add Clerk keys
npm run devOpen http://localhost:3000.
Realtime (optional, second terminal): npm run dev:realtime
Full stack: docker compose up --build
GitHub Actions on PRs and main (.github/workflows/ci.yml):
- lint → type-check → Vitest → build → Playwright E2E
Local equivalents:
npm run ci # lint, type-check, test, build
npm run ci:full # above + Playwright E2E
npm run test:e2e # E2E only (starts production server)CI runs the full pipeline on GitHub Actions (lint, type-check, Vitest, build, and Playwright E2E) on every push and pull request, against a Postgres service container. It uses Clerk test keys from repo secrets (CLERK_PUBLISHABLE_KEY / CLERK_SECRET_KEY), with placeholder fallback — never production secrets. The same checks run locally with the commands above.
main is protected: the quality-checks job is a required status check, so a pull request cannot be merged until CI is green.
| Area | Files |
|---|---|
| Cart logic | src/lib/cart-logic.test.ts |
| Cart persistence | src/lib/cart-store.test.ts |
| Cart context | src/app/context/cart-context.integration.test.tsx |
| HTTP client | src/lib/http-client.test.ts |
| Hooks | src/hooks/useDebouncedValue.test.ts |
| E2E checkout | e2e/checkout-happy-path.spec.ts |
Details: docs/testing-strategy.md
docs/frontend-architecture.md— SSR/SSG/ISR/Client splitdocs/definition-of-done.mddocs/testing-strategy.md.github/pull_request_template.md
- Vercel: primary live URL (configure Clerk + optional Sentry env vars in the dashboard). Every pull request gets an automatic preview deployment via the Vercel ↔ GitHub integration; pushes to
maindeploy to production. The build runsprisma migrate deployon production/CI/local builds but skips migrations on Preview builds (VERCEL_ENV=preview) — seescripts/build.mjs. - AWS ECS:
.github/workflows/deploy-aws.yml,aws/task-definition.json,aws/deploy.md.
| Symptom | Check |
|---|---|
| Auth buttons do nothing / Clerk errors | Keys in .env.local; restart npm run dev |
| Redirect loop on account pages | Valid Clerk keys; routes /sign-in, /sign-up exist |
| Orders empty after login | Sign in before checkout; orders are per Clerk userId and stored in Postgres |
DATABASE_URL missing on Vercel |
Set pooled Neon URL on Preview + Production; build runs prisma migrate deploy |
| API timeout errors | Network tab; default 8s timeout in http-client |
| Build fails on Vercel | Clerk env vars in Vercel project settings |
| Sentry not receiving events | NEXT_PUBLIC_SENTRY_DSN set in env |
E2E fails with Clerk host_invalid |
Set real Clerk test keys in CLERK_PUBLISHABLE_KEY / CLERK_SECRET_KEY repo secrets |
- Challenge: Model realistic ecommerce with auth, resilient APIs, and clear rendering choices.
- Approach: Clerk for sessions; server-loaded catalog with client islands; typed HTTP layer; Prisma + Postgres for orders, Redis for cart; tests + CI + docs.
- Result: A demo that reads as "junior with SaaS-style mechanics," not only a UI exercise — including a live Postgres migration without breaking the API.
- Clerk auth E2E with test users
- Performance budgets and accessibility audit with before/after notes
- Portfolio case-study page with the Redis → Postgres migration narrative



