Wire Next.js to FastAPI
The mock data has done its job. It proved the UI works, and now it's in the way. Time to pull it out and replace it with a real fetch.
Because both apps run under the same origin (thanks to vercel dev in the previous lesson, and thanks to Vercel's routing in production), the fetch URL is simple: just /api/items. No environment variables. No CORS. No absolute URLs for local vs. production.
Well, almost. One wrinkle with server components we'll handle in a second.
Outcome
Replace the mockItems array with an async fetch to /api/items that works both under vercel dev locally and in production.
Fast Track
- Replace
mockItemswith an asyncgetItems()function - Build the fetch URL from
process.env.VERCEL_URLwith a localhost fallback - Make
Homeanasynccomponent
Hands-On
Why we can't just write /api/items
In a client component, fetch("/api/items") works perfectly. The browser knows the origin and fills in the rest. In a server component, there is no browser. The fetch runs on the server, and Node's fetch needs an absolute URL.
On Vercel, the platform auto-injects VERCEL_URL in every deployment. That's the hostname of the current deployment, e.g., hazel-home-abc123.vercel.app. For local dev, we fall back to http://localhost:3000.
Update the page component
Open starter/app/page.tsx and replace the file with this:
type Item = {
id: number;
name: string;
category: string;
price: number;
in_stock: boolean;
};
async function getItems(): Promise<Item[]> {
const base = process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}`
: "http://localhost:3000";
const res = await fetch(`${base}/api/items`);
if (!res.ok) throw new Error("Failed to fetch items from Hazel Home API");
return res.json();
}
export default async function Home() {
const items = await getItems();
return (
<>
<h2 className="text-2xl font-semibold mb-8">All Furniture</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{items.map((item) => (
<div
key={item.id}
className="bg-white border border-stone-200 rounded-lg p-6"
>
<p className="text-xs uppercase tracking-widest text-stone-400 mb-1">
{item.category}
</p>
<h3 className="text-lg font-medium mb-3">{item.name}</h3>
<div className="flex items-center justify-between">
<span className="text-lg font-semibold">
${item.price.toLocaleString()}
</span>
<span
className={`text-xs px-2 py-1 rounded-full ${
item.in_stock
? "bg-emerald-50 text-emerald-700"
: "bg-stone-100 text-stone-400"
}`}
>
{item.in_stock ? "In stock" : "Out of stock"}
</span>
</div>
</div>
))}
</div>
</>
);
}Three things changed from the starter:
- The
mockItemsarray is gone getItems()builds an absolute URL fromVERCEL_URL(in production) orlocalhost:3000(locally) and fetches from/api/itemsHomeis nowasyncso it canawaitthe fetch
The component markup is identical. Same grid, same cards, same badges. Only the data source changed.
VERCEL_URL is a server-side variable. The fetch happens in a server
component, not the browser, so we don't need to expose it as NEXT_PUBLIC_.
If this fetch lived in a client component, we'd use a different pattern
entirely, but server components keep things simple.
Run vercel dev and refresh
If vercel dev is still running from the last lesson, you should see the updated page after saving. If not, restart it:
cd starter
vercel devOpen http://localhost:3000. The furniture listing appears, this time fetched live from the FastAPI server at http://localhost:3000/api/items.
If you stop vercel dev and reload, Next.js throws an error because getItems() can't reach the backend. That's expected. The two apps are genuinely connected now.
Try It
With vercel dev running, confirm the data is coming from FastAPI and not the mock:
- Open
http://localhost:3000. All 8 items appear. - Open
http://localhost:3000/api/itemsin a second tab. Same data. - Edit one item's name in
api/index.py, save, and reload the frontend. The name updates.
The third check is the real proof. The frontend is now a window into the backend, not a static page.
Troubleshooting
If you're working in academy-python-course, ask Cursor:
"Use the python-on-vercel skill and check my starter/app/page.tsx and starter/api/index.py wiring."
It can quickly spot route prefix mistakes, VERCEL_URL issues, and fetch URL problems.
TypeError: Failed to parse URL in the server logs: This happens if process.env.VERCEL_URL is unset and the fallback didn't kick in. Confirm the ternary is written correctly and that the fallback value starts with http://.
Failed to fetch items from Hazel Home API: vercel dev isn't running, or the FastAPI routes don't match. Check that api/index.py has @app.get("/api/items") (with the /api prefix) and that vercel dev is active on port 3000.
Done-When
page.tsxusesasync function getItems()with nomockItemsarray- The fetch URL resolves to
http://localhost:3000/api/itemsin local dev http://localhost:3000loads real data from FastAPI throughvercel dev- Editing
api/index.pyand reloading updates what the frontend shows
Solution
// starter/app/page.tsx
type Item = {
id: number;
name: string;
category: string;
price: number;
in_stock: boolean;
};
async function getItems(): Promise<Item[]> {
const base = process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}`
: "http://localhost:3000";
const res = await fetch(`${base}/api/items`);
if (!res.ok) throw new Error("Failed to fetch items from Hazel Home API");
return res.json();
}
export default async function Home() {
const items = await getItems();
return (
<>
<h2 className="text-2xl font-semibold mb-8">All Furniture</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{items.map((item) => (
<div key={item.id} className="bg-white border border-stone-200 rounded-lg p-6">
<p className="text-xs uppercase tracking-widest text-stone-400 mb-1">{item.category}</p>
<h3 className="text-lg font-medium mb-3">{item.name}</h3>
<div className="flex items-center justify-between">
<span className="text-lg font-semibold">${item.price.toLocaleString()}</span>
<span className={`text-xs px-2 py-1 rounded-full ${item.in_stock ? "bg-emerald-50 text-emerald-700" : "bg-stone-100 text-stone-400"}`}>
{item.in_stock ? "In stock" : "Out of stock"}
</span>
</div>
</div>
))}
</div>
</>
);
}Was this helpful?