
Next Js Tutorial #10 - Ecommerce App - My Orders Page & Api Route
We added new data models in our schema and made functionality to create cart and products.

You can also watch the YouTube video:
Next Js Tutorial #10 | Ecommerce App - My Orders Page & Api Route
Let's add a new data models to our schema file to make create cart and order functionality.
Edit schema.prisma
in prisma
folder:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mongodb"
url = env("DATABASE_URL")
}
enum UserRole {
USER
ADMIN
}
enum OrderStatus {
COMPLETED
PENDING
CANCELLED
}
model User {
id String @id @default(auto()) @map("_id") @db.ObjectId
email String @unique
password String
role UserRole @default(USER)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
orders Order[]
}
model Product {
id String @id @default(auto()) @map("_id") @db.ObjectId
title String
description String
price Int
image String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
cartProducts CartProduct[]
}
model CartProduct {
id String @id @default(auto()) @map("_id") @db.ObjectId
quantity Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
order Order @relation(fields: [orderId], references: [id])
orderId String @db.ObjectId
product Product @relation(fields: [productId], references: [id])
productId String @db.ObjectId
}
model Order {
id String @id @default(auto()) @map("_id") @db.ObjectId
numId Int @unique
status OrderStatus @default(PENDING)
total Float
cartProducts CartProduct[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdBy User @relation(fields: [createdById], references: [id])
createdById String @db.ObjectId()
}
Push the new schema to our database:
npx prisma db push
Create route.js
in app/api/orders
folder:
import { NextResponse } from "next/server";
import { PrismaClient } from "@prisma/client";
import { cookies } from "next/headers";
import jwt from "jsonwebtoken";
const prisma = new PrismaClient();
export async function POST(request) {
const body = await request.json();
const { cartProducts } = body;
if (!cartProducts) {
return NextResponse.json(
{ error: "An error occurred, please try again later!" },
{ status: 200 }
);
}
try {
const tokenCookie = await cookies();
const getToken = tokenCookie.get("token");
if (getToken) {
const token = jwt.verify(getToken.value, "appSecret");
const userId = token.id;
if (!userId) {
return NextResponse.json(
{ error: "Unauthorized request!" },
{ status: 200 }
);
}
const user = await prisma.user.findUnique({ where: { id: userId } });
if (!user) {
return NextResponse.json(
{ error: "Unauthorized request!" },
{ status: 200 }
);
}
let total = 0;
await Promise.all(
cartProducts.map(async (cartProduct) => {
const product = await prisma.product.findUnique({
where: { id: cartProduct.productId },
});
if (!product) {
return NextResponse.json(
{ error: "Product not found!" },
{ status: 200 }
);
}
total += product.price * cartProduct.quantity;
})
);
const numId =
Math.floor(Math.random() * (99999999999 - 10000000000 + 1)) +
10000000000;
const order = await prisma.order.create({
data: {
numId,
total: total + 1,
createdById: user.id,
status: "COMPLETED",
},
});
if (order) {
await Promise.all(
cartProducts.map(async (cartProduct) => {
await prisma.cartProduct.create({
data: {
productId: cartProduct.productId,
orderId: order.id,
quantity: cartProduct.quantity,
},
});
})
);
}
return NextResponse.json(order, { status: 201 });
}
return NextResponse.json(
{ error: "Unauthorized request!" },
{ status: 200 }
);
} catch (error) {
console.log(error.message);
return NextResponse.json(
{ error: "Failed to create order" },
{ status: 500 }
);
} finally {
await prisma.$disconnect();
}
}
export async function GET() {
try {
const tokenCookie = await cookies();
const getToken = tokenCookie.get("token");
if (getToken) {
const token = jwt.verify(getToken.value, "appSecret");
const userId = token.id;
if (!userId) {
return NextResponse.json(
{ error: "Unauthorized request!" },
{ status: 200 }
);
}
const user = await prisma.user.findUnique({ where: { id: userId } });
if (!user) {
return NextResponse.json(
{ error: "Unauthorized request!" },
{ status: 200 }
);
}
const orders = await prisma.order.findMany({
orderBy: { createdAt: "desc" },
where: { createdById: user.id },
include: { cartProducts: { include: { product: true } } },
});
return NextResponse.json({ orders });
} else {
return NextResponse.json(
{ error: "Unauthorized request!" },
{ status: 200 }
);
}
} catch (error) {
console.log(error);
return NextResponse.json(
{ error: "Failed to get orders" },
{ status: 500 }
);
} finally {
await prisma.$disconnect();
}
}
Create page.js
in app/auth/my-orders
folder:
import MyOrders from "@/components/Auth/MyOrders";
const AuthMyOrdersPage = () => {
return <MyOrders />;
};
export default AuthMyOrdersPage;
Create MyOrders.jsx
in components/Auth
folder:
"use client";
import { Loader2 } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { useEffect, useState } from "react";
const MyOrders = () => {
const [orders, setOrders] = useState([]);
const [loading, setLoading] = useState(false);
const getOrders = async () => {
try {
setLoading(true);
const response = await fetch("/api/orders");
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
setOrders(data.orders);
return data;
} catch (error) {
console.error("There was a problem with the fetch operation:", error);
} finally {
setLoading(false);
}
};
useEffect(() => {
getOrders();
}, []);
return (
<div className="mx-auto w-full max-w-screen-xl px-2.5 md:px-20 mt-16 space-y-8">
{loading ? (
<div className="flex items-center justify-center">
<Loader2 className="w-8 h-8 animate-spin" />
</div>
) : null}
{orders
? orders.map((order) => (
<div key={order.id}>
<div className="rounded-lg border">
{/* Card Header */}
<div className="flex flex-col space-y-1.5 p-6">
{/* Card Title */}
<h1 className="text-2xl font-semibold leading-none tracking-tight">
Order #{order.numId}
</h1>
{/* Card Description */}
<div className="text-sm text-gray-900 dark:text-gray-50">
<p>{order.createdAt}</p>
<p>
Total:{" "}
<span className="font-semibold">${order.total} USD</span>
</p>
</div>
</div>
{/* Card Content */}
<div className="p-6 pt-0">
<ul
className={
"divide-y divide-gray-200 border-b border-t border-gray-200"
}
>
{order.cartProducts
? order.cartProducts.map((cartProduct) => {
return (
<li
key={cartProduct.id}
className="flex py-6 sm:py-10"
>
<div className="flex-shrink-0">
<div className="relative h-24 w-24">
<Image
fill
src={cartProduct.product.image}
alt="product image"
className="h-full w-full rounded-md object-cover object-center sm:h-48 sm:w-48"
/>
</div>
</div>
<div className="ml-4 flex flex-1 flex-col justify-between sm:ml-6">
<div className="relative pr-9 sm:grid sm:grid-cols-2 sm:gap-x-6 sm:pr-0">
<div>
<div className="flex justify-between">
<h3 className="text-sm">
<Link
href={`/${cartProduct.product.id}`}
className="font-medium text-gray-700 dark:text-gray-50 dark:hover:text-gray-200 hover:text-gray-800"
>
{cartProduct.product.title}{" "}
{cartProduct.quantity > 1
? `(${cartProduct.quantity})`
: null}
</Link>
</h3>
</div>
<p className="mt-1 text-sm font-medium text-gray-900 dark:text-gray-50">
${cartProduct.product.price} USD
</p>
</div>
</div>
</div>
</li>
);
})
: null}
</ul>
</div>
</div>
</div>
))
: null}
</div>
);
};
export default MyOrders;
Create layout.js
in app/auth
folder:
"use client";
import { useCurrentUser } from "@/contexts/CurrentUserContext";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
export default function AuthLayout({ children }) {
const { currentUser } = useCurrentUser();
const router = useRouter();
useEffect(() => {
if (!currentUser) {
router.push("/");
}
}, [currentUser]);
return children;
}
Edit SidenavSheet.jsx
in app/components
folder:
// Find the Home link
<Link href="/">
<li className="sidenav-li">
<ArrowBigRight className="sidenav-li--icon" />
<span className="ml-3">Home</span>
</li>
</Link>
// Add My Orders link
<Link href="/auth/my-orders">
<li className="sidenav-li">
<ArrowBigRight className="sidenav-li--icon" />
<span className="ml-3">My Orders</span>
</li>
</Link>
Edit page.js
in app/cart
folder:
// Find this line
import { Minus, Plus, X } from "lucide-react";
// Edit with this line
import { Loader2, Minus, Plus, X } from "lucide-react";
// Add new imports
import { useRouter } from "next/navigation";
import { useState } from "react";
// Find this line
const { items, removeItem, increaseQuantity, decreaseQuantity, clearCart } =
useCart();
// Add these lines above it
const [errorMsg, setErrorMsg] = useState("");
const [loading, setLoading] = useState(false);
const router = useRouter();
// Find and delete this line
const productIDs = items.map(({ product }) => product.id);
// Find this line
const cartTotal = items.reduce(
(total, { product, quantity }) => total + product.price * quantity,
0
);
// Add handleCheckout function below it
const handleCheckout = async () => {
let cartProducts = [];
items.map((item) => {
cartProducts.push({
productId: item.product.id,
quantity: item.quantity,
});
});
try {
setLoading(true);
const response = await fetch("/api/orders", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
cartProducts,
}),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.error) {
setErrorMsg(data.error);
setTimeout(() => {
setErrorMsg("");
}, 4000);
} else {
clearCart();
router.push("/auth/my-orders");
}
return data;
} catch (error) {
console.error("There was a problem with the fetch operation:", error);
} finally {
setLoading(false);
}
};
// Find H1 Tag
<h1 className="text-3xl font-bold tracking-tight sm:text-4xl">
Shopping Cart
</h1>;
// Add these lines below it
{
errorMsg ? (
<div
className="p-4 mt-4 text-sm text-red-800 rounded-lg bg-red-50"
role="alert"
>
<span className="font-medium">Error:</span>
{errorMsg}
</div>
) : undefined;
}
// And finally go to end of page and edit the button
<Button
onClick={handleCheckout}
disabled={items.length === 0 || loading}
className="w-full"
size="lg"
>
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : "Checkout"}
</Button>;
That's it for this tutorial, we added new data models in our schema and made functionality to create cart and products.
Next tutorial we'll create admin orders page and api route.