AiTechWorlds
AiTechWorlds
University learning companion
Web Development Bootcamp 2026 · Project: E-Commerce Store
A feature looks correct on a developer’s large monitor but fails on a slow connection and a small screen. Use this topic to inspect structure, state, loading/error behaviour, accessibility and the user journey—not just the happy path.
Use this course-level map to place “Project: E-Commerce Store” in the larger system. It complements—not replaces—a topic-specific worked diagram.
This is the capstone project of the bootcamp a fully functional e commerce store with a product catalog, shopping cart, real Stripe checkout, and an order management system. By the end you'll have a production ready store you can show in your portfolio.
npx create-next-app@latest ecommerce-store --typescript --tailwind --app
cd ecommerce-store
npm install prisma @prisma/client stripe @stripe/stripe-js
npm install bcryptjs jsonwebtoken zod
npm install -D @types/bcryptjs @types/jsonwebtoken
npx prisma init// prisma/schema.prisma
model Product {
id String @id @default(cuid())
slug String @unique
name String
description String @db.Text
price Float
images String[]
category String
stock Int @default(0)
published Boolean @default(true)
orderItems OrderItem[]
@@map("products")
}
model User {
id String @id @default(cuid())
email String @unique
name String
password String
createdAt DateTime @default(now())
orders Order[]
@@map("users")
}
model Order {
id String @id @default(cuid())
status OrderStatus @default(PENDING)
total Float
stripeSessionId String? @unique
createdAt DateTime @default(now())
userId String?
user User? @relation(fields: [userId], references: [id])
items OrderItem[]
shipping ShippingAddress?
@@map("orders")
}
enum OrderStatus {
PENDING
PAID
SHIPPED
DELIVERED
CANCELLED
}
model OrderItem {
id String @id @default(cuid())
quantity Int
price Float
orderId String
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
productId String
product Product @relation(fields: [productId], references: [id])
@@map("order_items")
}
model ShippingAddress {
id String @id @default(cuid())
name String
line1 String
line2 String?
city String
state String
zip String
country String @default("US")
orderId String @unique
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
@@map("shipping_addresses")
}Keep the cart in localStorage for guest users and sync to the database for logged in users:
// src/store/cart.ts
"use client";
import { create } from "zustand";
import { persist } from "zustand/middleware";
interface CartItem {
id: string;
name: string;
price: number;
image: string;
quantity: number;
}
interface CartStore {
items: CartItem[];
addItem: (product: CartItem) => void;
removeItem: (id: string) => void;
updateQuantity: (id: string, quantity: number) => void;
clearCart: () => void;
total: number;
count: number;
}
// npm install zustand
export const useCart = create<CartStore>()(
persist(
(set, get) => ({
items: [],
addItem: (product) => set(state => {
const existing = state.items.find(i => i.id === product.id);
if (existing) {
return {
items: state.items.map(i =>
i.id === product.id
? { ...i, quantity: i.quantity + 1 }
: i
),
};
}
return { items: [...state.items, { ...product, quantity: 1 }] };
}),
removeItem: (id) => set(state => ({
items: state.items.filter(i => i.id !== id),
})),
updateQuantity: (id, quantity) => set(state => ({
items: quantity === 0
? state.items.filter(i => i.id !== id)
: state.items.map(i => i.id === id ? { ...i, quantity } : i),
})),
clearCart: () => set({ items: [] }),
get total() {
return get().items.reduce((sum, i) => sum + i.price * i.quantity, 0);
},
get count() {
return get().items.reduce((sum, i) => sum + i.quantity, 0);
},
}),
{ name: "cart" }
)
);// src/app/api/checkout/route.ts
import { NextRequest, NextResponse } from "next/server";
import Stripe from "stripe";
import { db } from "@/lib/db";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(request: NextRequest) {
const { items } = await request.json() as { items: Array<{ id: string; quantity: number }> };
if (!items?.length) {
return NextResponse.json({ error: "Cart is empty" }, { status: 400 });
}
// Fetch product details from database (never trust client-sent prices)
const productIds = items.map(i => i.id);
const products = await db.product.findMany({
where: { id: { in: productIds }, published: true },
});
if (products.length !== items.length) {
return NextResponse.json({ error: "Some products not available" }, { status: 400 });
}
// Create Stripe line items
const lineItems: Stripe.Checkout.SessionCreateParams.LineItem[] = items.map(item => {
const product = products.find(p => p.id === item.id)!;
return {
price_data: {
currency: "usd",
unit_amount: Math.round(product.price * 100), // cents
product_data: {
name: product.name,
images: product.images.slice(0, 1),
},
},
quantity: item.quantity,
};
});
// Create a pending order in the database
const order = await db.order.create({
data: {
total: products.reduce((sum, p) => {
const item = items.find(i => i.id === p.id)!;
return sum + p.price * item.quantity;
}, 0),
items: {
create: items.map(item => {
const product = products.find(p => p.id === item.id)!;
return {
productId: item.id,
quantity: item.quantity,
price: product.price,
};
}),
},
},
});
// Create Stripe checkout session
const session = await stripe.checkout.sessions.create({
payment_method_types: ["card"],
line_items: lineItems,
mode: "payment",
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/orders/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/cart`,
metadata: {
orderId: order.id,
},
shipping_address_collection: {
allowed_countries: ["US", "CA", "GB"],
},
});
// Save Stripe session ID
await db.order.update({
where: { id: order.id },
data: { stripeSessionId: session.id },
});
return NextResponse.json({ url: session.url });
}// src/app/api/webhooks/stripe/route.ts
import { NextRequest, NextResponse } from "next/server";
import Stripe from "stripe";
import { db } from "@/lib/db";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;
export async function POST(request: NextRequest) {
const body = await request.text();
const signature = request.headers.get("stripe-signature")!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
} catch (err) {
return NextResponse.json({ error: "Invalid signature" }, { status: 400 });
}
if (event.type === "checkout.session.completed") {
const session = event.data.object as Stripe.Checkout.Session;
const orderId = session.metadata?.orderId;
if (orderId) {
await db.order.update({
where: { stripeSessionId: session.id },
data: {
status: "PAID",
shipping: session.shipping_details ? {
create: {
name: session.shipping_details.name ?? "",
line1: session.shipping_details.address?.line1 ?? "",
line2: session.shipping_details.address?.line2 ?? undefined,
city: session.shipping_details.address?.city ?? "",
state: session.shipping_details.address?.state ?? "",
zip: session.shipping_details.address?.postal_code ?? "",
country: session.shipping_details.address?.country ?? "US",
}
} : undefined,
},
});
}
}
return NextResponse.json({ received: true });
}// src/components/Cart.tsx
"use client";
import { useCart } from "@/store/cart";
export function Cart() {
const { items, removeItem, updateQuantity, total } = useCart();
async function handleCheckout() {
const res = await fetch("/api/checkout", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ items: items.map(i => ({ id: i.id, quantity: i.quantity })) }),
});
const { url } = await res.json();
window.location.href = url; // redirect to Stripe hosted checkout
}
if (items.length === 0) {
return <p className="text-gray-500 text-center py-12">Your cart is empty.</p>;
}
return (
<div>
{items.map(item => (
<div key={item.id} className="flex items-center gap-4 py-4 border-b">
<img src={item.image} alt={item.name} className="w-16 h-16 object-cover rounded-lg"/>
<div className="flex-1">
<p className="font-medium">{item.name}</p>
<p className="text-gray-500 text-sm">${item.price.toFixed(2)}</p>
</div>
<div className="flex items-center gap-2">
<button onClick={() => updateQuantity(item.id, item.quantity - 1)} className="w-8 h-8 border rounded flex items-center justify-center">-</button>
<span className="w-6 text-center">{item.quantity}</span>
<button onClick={() => updateQuantity(item.id, item.quantity + 1)} className="w-8 h-8 border rounded flex items-center justify-center">+</button>
</div>
<button onClick={() => removeItem(item.id)} className="text-red-500 hover:text-red-600 text-sm">Remove</button>
</div>
))}
<div className="mt-6 border-t pt-4 flex items-center justify-between">
<span className="text-xl font-bold">Total: ${total.toFixed(2)}</span>
<button
onClick={handleCheckout}
className="bg-blue-600 hover:bg-blue-700 text-white font-semibold px-6 py-3 rounded-xl transition-colors"
>
Checkout with Stripe
</button>
</div>
</div>
);
}Card number: 4242 4242 4242 4242
Expiry: Any future date (e.g., 12/27)
CVC: Any 3 digits (e.g., 123)
ZIP: Any 5 digitsDeclined cards:
4000 0000 0000 0002 → Declined
4000 0000 0000 9995 → Insufficient fundsRun the Stripe webhook listener locally:
npm install -g stripe
stripe login
stripe listen --forward-to localhost:3000/api/webhooks/stripeSet environment variables on Vercel:
DATABASE_URL production databaseSTRIPE_SECRET_KEY live key from Stripe dashboardSTRIPE_WEBHOOK_SECRET from Stripe → Webhooks → Signing secretNEXT_PUBLIC_APP_URL your production URLNEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY live publishable keyAdd your production URL to Stripe webhook endpoints (Dashboard → Developers → Webhooks)
Deploy via git push Vercel builds and deploys automatically
You now have a production ready e commerce store. Congratulations on completing the Web Development Bootcamp.
University learning companion
Web Development Bootcamp 2026 · Project: E-Commerce Store
Complete the steps without looking at the answer first; record evidence rather than only a final result.
These are primary or established learning references for this subject. Check the relevant chapter or specification for the exact topic and version.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises