
Outcomes
Project scope
NovaShop began as a small storefront, and I kept extending it until it behaved like a store someone could actually run. Money moves through Stripe, catalog and order data live in PostgreSQL, and staff have their own admin application for daily operations, because payments, authentication, and back-office work are where engineering decisions start to carry real consequences. A wrong webhook handler here is not a cosmetic bug; it is a customer charged twice.
All three applications live in one pnpm workspace. A feature like order fulfilment touches the storefront, the API, and the admin dashboard at once, and the monorepo lets that land as a single reviewed change while each app keeps its own build and deployment pipeline.
Architecture
The storefront and the admin dashboard both talk to the NestJS API directly; there is no general proxy layer in between. The only routes living inside Next.js are the ones that genuinely need to run there: NextAuth, the AI chat endpoint, and the Stripe checkout and webhook handlers. Business rules, persistence, and payment state all belong to the API. On the storefront, authenticated requests go through a single authFetch helper that attaches the access token and refreshes it when needed.
The API exposes REST endpoints documented with Swagger alongside a GraphQL schema served through Apollo. Cart and review flows use GraphQL, where the client benefits from controlling the shape of the response; the remaining request-response flows stay on REST.
- Storefront: Next.js 15.5 App Router, React 19, Tailwind CSS, deployed on Vercel.
- API: NestJS 11, TypeORM over PostgreSQL (Supabase), REST + GraphQL.
- Admin: React 19, Vite, TanStack Query, and Cloudinary uploads.
- Workspace: three applications managed with pnpm and checked by GitHub Actions CI.
The storefront
The Next.js app covers search, filtering, product pages, cart, wishlist, checkout, and order history. Catalog pages render as React Server Components, so the main product content arrives in the initial HTML instead of being fetched afterwards. Checkout works with or without an account: a guest can buy a product directly and Stripe collects their shipping address during payment, while signed-in customers check out a full cart against their saved addresses.
The storefront also generates sitemap, robots.txt, JSON-LD, Open Graph, and Twitter metadata. In a desktop Lighthouse run it scored 98 for performance, 96 for accessibility, and 100 for both best practices and SEO, with a 1.1-second LCP and zero CLS. Google Analytics and Sentry are wired in, so traffic and production errors are visible without digging through server logs.
The API
The NestJS API is split by domain: auth, users, products, cart, wishlist, orders, addresses, reviews, notifications, analytics, and storefront posters. Filtering and sorting happen in PostgreSQL rather than in application memory, and TypeORM synchronize is disabled so every schema change is applied deliberately.
Order creation runs inside a single database transaction. Stock is decremented with a conditional update that only succeeds while enough units remain, so two buyers racing for the last item cannot both win. Each order also stores its own snapshot of the shipping address, which means editing an address later never rewrites the history of orders already placed.
The admin dashboard
Admin and staff accounts use a separate React and Vite app to manage products, orders, customers, posters, and sales data. TanStack Query manages server state, and product and poster images are uploaded through Cloudinary.
Payments
Stripe Checkout sessions cover both buy-now and full-cart purchases, and no order is ever confirmed from the browser redirect. Stripe's webhook lands on a Next.js route that verifies the event signature, and that route then calls an internal confirmation endpoint on the API, protected by a separate shared secret. Two independent checks stand between the internet and a confirmed order.
Confirmation is idempotent: the handler looks up the Stripe session id inside the transaction, so a retried webhook returns the existing order instead of creating it twice. Guest orders keep the buyer's email, and when that email belongs to a registered account the order is attached to it and appears in their history. A confirmation email goes out once the order is recorded.
Authentication and security
Sign-in works with email and password or Google. NestJS issues separate access and refresh JWTs, which the storefront stores in httpOnly cookies through NextAuth v5. Refresh tokens are bcrypt-hashed in the database and rotated after use, so a stolen token cannot be replayed. The API also applies request throttling, role guards, and ownership checks, which keep one account from touching another account's cart, orders, or addresses.
AI shopping assistant
The shopping assistant uses xAI Grok through the Vercel AI SDK. Its searchProducts tool queries the current catalog by keyword, category, and price, so product suggestions come from live store data.
Caching and publishing
Public catalog pages use ISR, account-specific requests bypass shared caching, and mutations invalidate tagged data. The cache helper supports the relevant Next.js 15 and 16 APIs so the behavior stays consistent during the framework upgrade.
Testing
Each application carries its own test suite: Jest on the API, Vitest on the storefront and the admin dashboard. Coverage is concentrated where a regression costs real money or data, such as order status transitions and stock restoration when an order is cancelled, the validation schemas behind admin forms, and the catalog filter parsing that drives storefront URLs. Every push runs the full ladder in GitHub Actions: lint, the three unit suites, a Cypress end-to-end pass that boots the storefront and walks through catalog navigation, search, filters, and the cart, SonarQube static analysis for code quality and security issues, and finally the production build.
Honest limits
Two trade-offs are worth naming. Stripe's webhook lands on the storefront and is relayed to the API; the relay is verified twice, but if I rebuilt the flow I would point Stripe straight at a NestJS endpoint and drop the extra hop. Confirmation emails are also sent within the order flow itself, which is fine at this scale and belongs on a queue at real volume. Both were conscious choices for a system this size, and both are the first things I would revisit if it grew.