A full-stack e-commerce React application refactored to be production-ready, fully type-safe, and 100% tested with Vitest and React Testing Library.
Live demo: vscrm.github.io/React-Shop Backend repository: React-Shop-Backend
- Pages
- Tech Stack
- Quick Start
- Environment Variables
- Running the Test Suite
- Architectural Changes
- Zod Integration β Runtime Validation
- Component & Module Breakdown
- Project Structure
- Added & Changed Dependencies
- Type Safety Guidelines
- Test Coverage Map
- Acknowledgements
- License
| Page | Route | Description |
|---|---|---|
| Home | / |
Product catalog with search and add-to-cart |
| Checkout | /checkout |
Cart review, delivery options, order placement |
| Orders | /orders |
Order history with buy-again functionality |
| Tracking | /tracking/:orderId/:productId? |
Real-time delivery tracking progress |
| Technology | Version | Purpose |
|---|---|---|
| React | 19 | UI library with the latest concurrent features |
| TypeScript | 6 | Static typing with strict mode enabled |
| React Router | 7 | Client-side routing with BrowserRouter and basename support |
| Vite | 8 | Build tool and dev server with HMR |
| Library | Version | Purpose |
|---|---|---|
| Axios | 1.x | HTTP client for all API requests via a shared api.ts instance |
| Zod | 3.x | Runtime schema validation and TypeScript type inference β¨ New |
| Day.js | 1.x | Lightweight date formatting for delivery and order dates |
| Library | Purpose |
|---|---|
| Vitest | Unit test runner (Vite-native, Jest-compatible) |
| @testing-library/react | Component rendering and querying in tests |
| @testing-library/user-event | Simulates real user interactions |
| @testing-library/jest-dom | Custom DOM matchers (toBeInTheDocument, etc.) |
| jsdom | Browser environment simulation for Node.js |
| Tool | Purpose |
|---|---|
| ESLint | Code linting with React Hooks and React Refresh plugins |
| gh-pages | Automated deployment to GitHub Pages from the dist folder |
# Install project dependencies
npm install
# Run linter to check for code style and errors
npm run lint
# Start development server (proxies /api and /images to localhost:3000)
npm run dev
# Run tests once
npm run test
# Run tests in watch mode for development
npm run test:watch
# Run tests and generate code coverage report
npm run test:coverage
# Build production-ready assets into the dist folder
npm run build
# Locally preview the production build
npm run preview
# Build and deploy the application to GitHub Pages
npm run deployCreate a .env file in the project root for local development:
VITE_API_URL=http://localhost:3000.env.production is committed and used automatically during npm run build / npm run deploy:
VITE_API_URL=https://react-shop-backend-jg62.onrender.com| Command | Description |
|---|---|
npm test |
Run all tests once (CI mode) |
npm run test:watch |
Run in watch mode (development) |
npm run test:coverage |
Run with V8 coverage report |
β src/schemas/index.test.ts (56 tests)
β src/hooks/useProducts.test.ts (14 tests)
β src/hooks/useCartActions.test.tsx (12 tests)
β src/services/api.test.ts (12 tests)
β src/hooks/useDeliveryOptions.test.ts (11 tests)
β src/hooks/useBuyAgain.test.ts (11 tests)
β src/pages/home/ProductCard.test.tsx (11 tests)
β src/components/ErrorBoundary.test.tsx (8 tests)
β src/hooks/useCartItemHandlers.test.ts (8 tests)
β src/hooks/useCartData.test.ts (8 tests)
β src/hooks/useHeaderSearch.test.tsx (10 tests)
β src/context/CartProvider.test.tsx (3 tests)
β src/hooks/useFlashMessage.test.ts (6 tests)
β src/utils/getTrackedProducts.test.ts (6 tests)
β src/hooks/useCartContext.test.tsx (3 tests)
β src/hooks/useDebounce.test.ts (5 tests)
β src/utils/computeStatus.test.ts (7 tests)
β src/utils/searchUtils.test.ts (6 tests)
β src/utils/money.test.ts (6 tests)
Test Files 19 passed (19)
Tests 203 passed (203)
Before: TypeScript interfaces in src/types/index.ts were manually defined and duplicated runtime shape expectations.
After: All domain types are derived from Zod schemas via z.infer:
src/schemas/index.ts β Zod schemas (runtime validation)
β z.infer
src/types/index.ts β TypeScript types (static typing)
Change a schema once β both runtime validation and static types update automatically.
Every data-fetching function now calls Schema.parse(response.data) before returning. If the server sends unexpected data (missing fields, wrong types, out-of-range values), a ZodError is thrown immediately and the hook's catch block converts it to a user-facing error message.
Affected services:
| Service | Schema used |
|---|---|
loadToCart |
CartItemListSchema |
createOrder |
CreatedOrderSchema |
Affected hooks:
| Hook | Schema used |
|---|---|
useProducts |
ProductListSchema |
useDeliveryOptions |
DeliveryOptionListSchema |
useOrders |
OrderListSchema |
useOrder |
OrderSchema |
usePaymentSummary |
PaymentSummarySchema |
tsconfig.json enforces:
{
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
}All function arguments, return types, React component props, custom hook return types, and event handlers are explicitly typed. any and unknown are banned from application code β Zod handles the unknown β validated type boundary.
useProducts (and all other data-fetching hooks) now return an explicit result interface, making the contract clear to consumers without reading the implementation.
Every exported function, hook, schema, and type has a JSDoc comment documenting its purpose, parameters, return value, and possible thrown errors (@throws).
RatingSchema β { stars: 0β5, count: int β₯ 0 }
ProductSchema β a single product
ProductListSchema β z.array(ProductSchema)
DeliveryOptionSchema β a delivery option
DeliveryOptionListSchema β z.array(DeliveryOptionSchema)
CartItemSchema β a cart item (embeds ProductSchema)
CartItemListSchema β z.array(CartItemSchema)
OrderProductSchema β a product inside an order
OrderSchema β a full order (embeds OrderProductSchema[])
OrderListSchema β z.array(OrderSchema)
CreatedOrderSchema β POST /api/orders response
PaymentSummarySchema β checkout cost breakdown
// Before β no runtime validation
const response = await api.get<CartItem[]>("/api/cart-items?expand=product");
return response.data; // trusted blindly
// After β Zod validates before returning
const response = await api.get<unknown>("/api/cart-items?expand=product");
return CartItemListSchema.parse(response.data); // throws ZodError on bad dataAll data-fetching hooks catch both network errors and Zod parse errors in the same catch block:
try {
const validated = SomeSchema.parse(response.data);
setData(validated);
} catch {
// Catches ZodError (bad API data) AND AxiosError (network failure)
setError("Could not load data. Please try again.");
}| File | Responsibility |
|---|---|
index.ts |
All Zod validation schemas β single source of truth |
index.test.ts |
56 tests: valid data, invalid data, edge cases for every schema |
| File | Responsibility |
|---|---|
index.ts |
TypeScript types derived from schemas via z.infer + UI-only types |
| File | Method | Endpoint | Zod validation |
|---|---|---|---|
api.ts |
β | Axios instance factory | β |
addToCart.ts |
POST | /api/cart-items |
No (mutation) |
loadToCart.ts |
GET | /api/cart-items?expand=product |
β
CartItemListSchema |
updateCartQuantity.ts |
PUT | /api/cart-items/:id |
No (mutation) |
updateCartDelivery.ts |
PUT | /api/cart-items/:id |
No (mutation) |
deleteCartItem.ts |
DELETE | /api/cart-items/:id |
No (mutation) |
createOrder.ts |
POST | /api/orders |
β
CreatedOrderSchema |
| Hook | Responsibility |
|---|---|
useCart |
Composes useCartData + useCartActions into the full cart API |
useCartData |
Raw cart state: initial load, setCart, manual loadCart |
useCartActions |
All cart mutation operations (add, update, remove, order) |
useCartContext |
Type-safe access to CartContext; throws if used outside provider |
useCartItemHandlers |
UI state for a single checkout row (editing, delete confirm) |
useProducts |
Products list with search, loading & error states + Zod validation |
useDeliveryOptions |
Delivery options fetch + Zod validation |
useOrders |
Orders list fetch + Zod validation |
useOrder |
Single order fetch by ID + Zod validation |
usePaymentSummary |
Payment summary fetch triggered by cart changes + Zod validation |
useBuyAgain |
"Buy again" picker state for the orders page |
useFlashMessage |
Timed boolean flag (e.g. "Added to cart" flash) |
useDebounce |
Generic debounce for search input |
useHeaderSearch |
Header search bar: input, submit, clear, URL sync |
| File | Responsibility |
|---|---|
CartContext.tsx |
Creates the CartContext with CartContextValue | undefined |
CartProvider.tsx |
Wraps children with the cart context populated from useCart |
| File | Responsibility |
|---|---|
ErrorBoundary.tsx |
Class component error boundary; supports custom fallback render-prop |
HeaderLogo.tsx |
Site logo with responsive images |
HeaderNav.tsx |
Navigation links + cart item count |
HeaderSearch.tsx |
Search bar composition (SearchInput + SearchButton) |
SearchInput.tsx |
Controlled input element |
SearchButton.tsx |
Submit / clear toggle button |
| File | Responsibility |
|---|---|
constants.ts |
App-wide constants (search key, debounce delay, tracking steps) |
computeStatus.ts |
Derives TrackingStatus from timestamps |
getTrackedProducts.ts |
Filters order products by optional productId |
imageUrl.ts |
Builds absolute URLs for API images and static assets |
money.ts |
formatMoney(cents) β "$X.XX" |
searchUtils.ts |
buildSearchPath(value) β URL string |
React-Shop/
βββ public/ # Static assets served as-is by Vite (not bundled).
β β # Contains UI icons and logo images used directly
β β # in JSX via staticImage() helper.
β β # Product images and ratings are served by the
β β # backend (Render.com) and are not included here.
βββ src/
β βββ components/
β β βββ ErrorBoundary.test.tsx
β β βββ ErrorBoundary.tsx
β β βββ HeaderLogo.tsx
β β βββ HeaderNav.tsx
β β βββ HeaderSearch.tsx
β β βββ SearchButton.tsx
β β βββ SearchInput.tsx
β βββ context/
β β βββ CartContext.tsx
β β βββ CartProvider.test.tsx
β β βββ CartProvider.tsx
β βββ hooks/
β β βββ useBuyAgain.test.ts
β β βββ useBuyAgain.ts
β β βββ useCart.ts
β β βββ useCartActions.test.tsx # REFACTORED
β β βββ useCartActions.ts
β β βββ useCartContext.test.tsx
β β βββ useCartData.test.ts # REFACTORED
β β βββ useCartData.ts
β β βββ useCartItemHandlers.test.ts
β β βββ useCartItemHandlers.ts
β β βββ useDebounce.test.ts
β β βββ useDebounce.ts
β β βββ useDeliveryOptions.test.ts # REFACTORED
β β βββ useDeliveryOptions.ts # now validates with Zod β¨ REFACTORED
β β βββ useFlashMessage.test.ts
β β βββ useFlashMessage.ts
β β βββ useHeaderSearch.test.tsx
β β βββ useHeaderSearch.ts
β β βββ useOrder.ts # now validates with Zod β¨ REFACTORED
β β βββ useOrders.ts # now validates with Zod β¨ REFACTORED
β β βββ usePaymentSummary.ts # now validates with Zod β¨ REFACTORED
β β βββ useProducts.test.ts # REFACTORED
β β βββ useProducts.ts # now validates with Zod β¨ REFACTORED
β βββ layout/
β β βββ Header.css
β β βββ Header.tsx
β βββ pages/
β β βββ checkout/
β β β βββ CartItemActions.tsx
β β β βββ CartItemDetails.tsx
β β β βββ CheckoutHeader.css
β β β βββ CheckoutHeader.tsx
β β β βββ CheckoutPage.css
β β β βββ CheckoutPage.tsx
β β β βββ DeleteConfirm.tsx
β β β βββ DeliveryDate.tsx
β β β βββ DeliveryOptions.tsx
β β β βββ OrderSummary.tsx
β β β βββ PaymentSummary.tsx
β β β βββ QuantityDisplay.tsx
β β β βββ QuantityEditor.tsx
β β βββ home/
β β β βββ AddedMessage.tsx
β β β βββ HomePage.css
β β β βββ HomePage.tsx
β β β βββ NoSearchResults.tsx
β β β βββ ProductCard.test.tsx
β β β βββ ProductCard.tsx
β β β βββ ProductQuantity.tsx
β β β βββ ProductRating.tsx
β β β βββ ProductsGrid.tsx
β β βββ order/
β β β βββ BuyAgainPicker.tsx
β β β βββ OrderContainer.tsx
β β β βββ OrderDetail.tsx
β β β βββ OrderHeader.tsx
β β β βββ OrdersPage.css
β β β βββ OrdersPage.tsx
β β β βββ ProductActions.tsx
β β β βββ ProductDetails.tsx
β β β βββ ProductInfo.tsx
β β βββ tracking/
β β βββ TrackingDeliveryHeader.tsx
β β βββ TrackingItem.tsx
β β βββ TrackingPage.css
β β βββ TrackingPage.tsx
β β βββ TrackingProductInfo.tsx
β β βββ TrackingProgress.tsx
β β βββ TrackingStatusMessage.tsx
β βββ schemas/
β β βββ index.test.ts # 56 schema tests β¨ NEW
β β βββ index.ts # Zod schemas (single source of truth) β¨ NEW
β βββ services/
β β βββ addToCart.ts
β β βββ api.test.ts # includes Zod rejection tests β¨ REFACTORED
β β βββ api.ts
β β βββ createOrder.ts # now validates with CreatedOrderSchema β¨ REFACTORED
β β βββ deleteCartItem.ts
β β βββ loadToCart.ts # now validates with CartItemListSchema β¨ REFACTORED
β β βββ updateCartDelivery.ts
β β βββ updateCartQuantity.ts
β βββ types/
β β βββ index.ts # z.infer-derived types β¨ REFACTORED
β βββ utils/
β β βββ computeStatus.test.ts
β β βββ computeStatus.ts
β β βββ constants.ts
β β βββ getTrackedProducts.test.ts
β β βββ getTrackedProducts.ts
β β βββ imageUrl.ts
β β βββ money.test.ts
β β βββ money.ts
β β βββ searchUtils.test.ts
β β βββ searchUtils.ts
β βββ App.tsx
β βββ index.css
β βββ main.tsx
β βββ vite-env.d.ts # <reference types="vite/client" />
βββ .editorconfig
βββ .env # Local development API URL (git-ignored)
βββ .env.example # Template for environment variables
βββ .env.production # Production API URL (committed)
βββ .gitignore
βββ eslint.config.ts
βββ index.html
βββ jsconfig.json
βββ LICENSE
βββ package-lock.json
βββ package.json
βββ README.md
βββ setupTests.ts # Imports jest-dom matchers for all tests
βββ tsconfig.json # TypeScript config with strict mode and path aliases
βββ tsconfig.node.json
βββ vite.config.ts # Build config, proxy, base path, test env
| Package | Version | Reason |
|---|---|---|
zod |
^3.24.2 |
Runtime schema validation and TypeScript type inference |
{
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage"
}All existing dependencies (axios, dayjs, react-router, etc.) are unchanged.
// Derive types from schemas
import type { z } from 'zod';
import { ProductSchema } from '@/schemas';
type Product = z.infer<typeof ProductSchema>;
// Explicitly type all function signatures
function formatMoney(amountCents: number): string { ... }
// Use schema.parse() at API boundaries
const data = ProductListSchema.parse(response.data);
// Use schema.safeParse() when you need to handle errors gracefully
const result = CartItemSchema.safeParse(raw);
if (!result.success) console.error(result.error.format());// Never use `any`
const data: any = response.data;
// Never trust raw API responses without parsing
return response.data as CartItem[];
// Never duplicate type definitions manually when a schema already exists
interface Product {
id: string; /* ... */
}| Test File | What it covers |
|---|---|
schemas/index.test.ts |
β Valid data accepted Β· β Invalid data rejected Β· π² Edge cases (zero prices, boundary stars, empty arrays) |
services/api.test.ts |
HTTP methods & URLs Β· Zod rejection from loadToCart Β· Zod rejection from createOrder |
hooks/useProducts.test.ts |
Valid fetch Β· Zod rejects malformed data Β· Loading states Β· Re-fetch on query change |
hooks/useDeliveryOptions.test.ts |
Valid fetch Β· Zod rejects malformed data Β· Loading/error states |
hooks/useCartData.test.ts |
Initial load Β· Zod/network error β cartError Β· Manual reload Β· Optimistic setCart |
hooks/useCartActions.test.tsx |
addCart Β· updateDeliveryOption Β· updateQuantity Β· removeItem Β· placeOrder Β· error swallowing |
hooks/useCartItemHandlers.test.ts |
isEditing toggle Β· handleSaveQuantity Β· handleDeleteOne (qty=1 vs qty>1) Β· handleDeleteAll |
hooks/useCartContext.test.tsx |
Value returned Β· Same reference Β· Throws outside provider |
hooks/useBuyAgain.test.ts |
Initial state Β· Single-qty direct add Β· Multi-qty picker flow Β· confirm Β· cancel |
hooks/useFlashMessage.test.ts |
Inactive initially Β· Active after trigger Β· Auto-deactivates Β· Timer reset |
hooks/useDebounce.test.ts |
Initial value Β· No update before delay Β· Updates after delay Β· Rapid change debounce |
hooks/useHeaderSearch.test.tsx |
Initial state Β· Home page URL sync Β· Input change Β· Clear Β· Form submit |
context/CartProvider.test.tsx |
Cart data provided Β· Context shape Β· Children rendered |
components/ErrorBoundary.test.tsx |
Normal render Β· Default fallback UI Β· Custom fallback prop Β· Try-again reset Β· logging |
pages/home/ProductCard.test.tsx |
Name/price/image render Β· Add to cart Β· Quantity selector Β· Flash message |
utils/computeStatus.test.ts |
Delivered (at/past time) Β· Preparing (<40%) Β· Shipped (40β99%) |
utils/getTrackedProducts.test.ts |
null order Β· no products Β· filter by id Β· all products |
utils/money.test.ts |
Various cent values Β· Dollar sign Β· 2 decimal places |
utils/searchUtils.test.ts |
Empty/whitespace β / Β· Query path Β· URL encoding Β· Trim |
This project was built while following the SuperSimpleDev React course β a fantastic, hands-on resource for learning modern React from scratch.
- πΊ Course video: youtube.com/watch?v=TtPXvEcE11E
- π» GitHub: github.com/SuperSimpleDev/react-course