Build an Online Shop with Next.js #5: Order Confirmation and Deployment
Last time we wired up the checkout form, a Server Action, and the payment simulation — completing the path that turns a cart into an order. In this final post, we close the user flow with an order confirmation page, deploy to Vercel to put the shop on the real internet, and wrap up with a look back at the whole series and the question “what would need fixing before this could be a production service?”
/orders/[orderId] — the order confirmation page
#
This is where the client lands right after checkout, via router.push('/orders/${orderId}'). It looks up the order in the in-memory store by ID and displays it.
src/app/orders/[orderId]/page.js:
import Link from 'next/link';
import { notFound } from 'next/navigation';
import { getOrder } from '../../lib/orders';
import { formatPrice } from '../../lib/format';
export const metadata = {
title: 'Order Complete',
description: 'Your order has been received.',
robots: { index: false, follow: false },
};
export default async function OrderConfirmationPage({ params }) {
const { orderId } = await params;
const order = getOrder(orderId);
if (!order) notFound();
return (
<main className="mx-auto max-w-2xl px-4 py-8">
<section className="rounded-lg bg-green-50 p-6 text-center">
<h1 className="text-2xl font-bold text-green-800">Your order is complete</h1>
<p className="mt-2 text-sm text-green-700">
Order number <code className="font-mono">{order.id}</code>
</p>
</section>
<section className="mt-6">
<h2 className="text-lg font-semibold">Order details</h2>
<ul className="mt-2 divide-y border-y text-sm">
{order.items.map((it) => (
<li key={it.slug} className="flex justify-between py-2">
<span>{it.name} × {it.quantity}</span>
<span>{formatPrice(it.price * it.quantity)}</span>
</li>
))}
</ul>
<div className="mt-3 flex justify-between text-lg font-semibold">
<span>Total</span>
<span>{formatPrice(order.total)}</span>
</div>
</section>
<section className="mt-6">
<h2 className="text-lg font-semibold">Shipping info</h2>
<dl className="mt-2 grid grid-cols-3 gap-y-1 text-sm">
<dt className="text-gray-500">Recipient</dt>
<dd className="col-span-2">{order.shipping.name}</dd>
<dt className="text-gray-500">Phone</dt>
<dd className="col-span-2">{order.shipping.phone}</dd>
<dt className="text-gray-500">Address</dt>
<dd className="col-span-2">{order.shipping.address}</dd>
</dl>
</section>
<div className="mt-8 flex gap-3">
<Link
href="/products"
className="rounded-md border px-4 py-2 text-sm"
>
Continue shopping
</Link>
<Link
href="/"
className="rounded-md bg-black px-4 py-2 text-sm text-white"
>
Back to home
</Link>
</div>
</main>
);
}Design details:
robots: { index: false, follow: false }— there’s no reason for search engines to index an order confirmation page. It contains personal information (name, phone, address), so we block any chance of it showing up in search from the start.- Invalid IDs hit
notFound()— if someone guesses another user’s order ID and the order isn’t in memory (server restart, etc.), they get a 404. - Two next actions — “Continue shopping” and “Back to home.” Right after payment, users shouldn’t be stuck in a dead end; we point them to where to go next.
A quick note on order page security #
The current implementation means anyone who knows an order ID can view that order. Fine for a learning demo, but a production service needs these reinforcements:
- Make order IDs long, unguessable random strings (the current timestamp + 6 characters is partially guessable)
- For guest checkout, issue a separate one-time token for order lookup (attached to an email link)
- For logged-in users, match orders against the session so users can only see their own
We’ll come back to this in the final section.
Tidying up site metadata #
Finally, let’s polish the metadata to cover the SEO basics. The pattern is similar to #5 of the blog series, adjusted for an e-commerce site.
src/app/layout.js (metadata section):
export const metadata = {
metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL ?? 'http://localhost:3000'),
title: {
default: 'My Shop',
template: '%s | My Shop',
},
description: 'A learning e-commerce build — discover great products.',
openGraph: {
type: 'website',
locale: 'en_US',
siteName: 'My Shop',
},
};Refining the product detail page’s OG as well makes social previews richer.
src/app/products/[slug]/page.js (enhanced generateMetadata):
export async function generateMetadata({ params }) {
const { slug } = await params;
const product = getProductBySlug(slug);
if (!product) return {};
return {
title: product.name,
description: product.description,
openGraph: {
title: product.name,
description: product.description,
type: 'website',
images: [{ url: product.image }],
},
};
}We won’t go as far as automatic OG image generation (ImageResponse). Exposing a static image as the OG image is enough here; fancier OG setups are a topic for another day.
Environment variables and deploy prep #
The domain changes in the deployed environment, so we extract the site URL into an environment variable.
.env.local:
NEXT_PUBLIC_SITE_URL=http://localhost:3000In the Vercel project’s Environment Variables, register the real domain:
NEXT_PUBLIC_SITE_URL=https://your-shop.vercel.appmetadataBase uses this value to build absolute URLs for OG images. Dev, preview, and production each automatically get their own domain.
Deploying to Vercel #
Same flow as the blog series.
- Push the code to a GitHub repository
- On vercel.com, “New Project” → pick the repo
- Framework Preset: Next.js (auto-detected)
- Add
NEXT_PUBLIC_SITE_URLunder Environment Variables - Deploy
When the build finishes, you get a domain like https://your-shop.vercel.app. Go in, browse the catalog, add something to the cart, and run a checkout end to end.
The limits of in-memory storage show up after deploy #
Once you actually use the deployed shop, two problems that were invisible during learning surface.
1. 404 on the order page right after checkout #
Checkout succeeds, but /orders/[orderId] sometimes returns a 404. The cause is Vercel’s separate serverless function instances.
- The checkout request (
checkoutAction) runs on instance A, so the order lands in A’s in-memoryMap - The follow-up order lookup (the Server Component for
/orders/[orderId]) runs on instance B - B’s memory doesn’t have that order →
notFound()
In-memory storage only means anything within a single process. To share data across multiple instances, that data has to live somewhere external — and a database is the standard answer.
2. Redeploying wipes all orders #
When a new deploy goes live, the serverless functions start as fresh instances. The module-scope Map starts out empty again. Every past order evaporates.
A fine trade-off for a learning demo — and a great teaching moment, because it shows precisely where a production service would have to break.
What needs fixing for a production service #
Let’s revisit the areas this series deliberately simplified.
| Area | Learning implementation | Production recommendation |
|---|---|---|
| Product storage | JSON file | DB (PostgreSQL, MySQL, SQLite) or headless CMS |
| Order storage | Module-scope in-memory Map | DB. Can’t assume a single instance |
| Price validation | Sum prices sent by the client | Server re-fetches prices from the product master, then sums |
| Inventory | Static stock field, no decrement | Decrement stock in a transaction at checkout + concurrency handling |
| Payment | Simulation (Math.random) | Integrate a PG like Toss Payments / Stripe / Iamport + webhooks |
| Authentication | None; anyone can check out | NextAuth.js / Clerk / Lucia, etc. |
| Order access control | Anyone with the order ID | Logged-in users see only their own orders / one-time token for guests |
| Order status | Fixed paid | A pending / paid / preparing / shipped / delivered / canceled state machine |
| Email notifications | None | Mail at each payment/shipping step via Resend / SendGrid, etc. |
| Admin pages | None | Order list / inventory management / refund handling |
| Security | HTML5 required + server guard | + CSRF tokens / rate limiting / input sanitization |
Each row could fill a series of its own. But the skeleton this series built — catalog → cart → checkout → order confirmation — survives intact. The items above are flesh you add to that skeleton over time.
Series retrospective — Build an Online Shop with Next.js #
Across this series, we went from an empty project to a demo shop with a closed user flow.
| # | Added feature | Key patterns/tools |
|---|---|---|
| 1 | Design, data model, setup | App Router, JSON seed, lib/ separation |
| 2 | Catalog (list / detail / filters) | Server Components, searchParams, generateStaticParams, notFound, next/image |
| 3 | Shopping cart | Client Context, localStorage persistence, hydration-safe pattern |
| 4 | Checkout + payment | Server Actions, useActionState, useFormStatus, payment simulation |
| 5 | Order confirmation + deployment | metadata API, robots, Vercel, limits of in-memory storage |
Same five-post length as the blog series, but different territory. The blog centered on reading and comments; the shop centered on user state (cart) → transactions (orders). Following both builds gives you practical patterns for both read-centric sites and state/transaction-centric apps.
Where to go from here #
You now have an even sturdier base for starting your own projects. Possible directions:
If you made this shop real #
- Add a DB — start with Prisma + PostgreSQL or Drizzle + SQLite. Keep the call signatures in
products.js/orders.jsand swap only the internals, and the page code barely needs to change - Integrate a real payment provider — open the payment window and receive webhooks with Toss Payments (Korea) or Stripe (global)
- Add authentication — email/social login with NextAuth.js, users viewing their own orders
- Admin pages — order list / adding products / inventory management
Other builds #
- Social app — boards, follows, notifications — real-time and many-to-many relationships
- Dashboard — charts, filters, data visualization — external data source integration
- A tool for your own domain — that small tool you’ve always wanted. The choice with the biggest learning payoff
Wrap-up #
Thanks for following along this far. We started from a single small catalog page and tied cart, payment, and order confirmation into one continuous flow. There are learning-grade simplifications throughout, but the exact points where those simplifications break are the signals that tell you which tools a production service needs — the moment in-memory storage splits across instances, you need a DB; the moment client-sent prices start to worry you, you need stronger server-side validation.
The React ecosystem moves fast, but the mental model this series covered — “where does this code run?” — doesn’t change. Once the division of labor between Server Components, Client Components, and Server Actions is in your hands, the same intuition carries over to any other tool. DB, auth, and payment providers just stack on top over time. Knowing the essence, every new tool that comes along quickly finds its place.