Vercel Logo

Wire Next.js to FastAPI

The Hazel Home product grid has been showing the same eight items no matter what the API returns. We are ready to remove that disguise.

The page is a Server Component, so its fetch runs in Node rather than in the browser. Node needs an absolute URL. We'll build one from Vercel's deployment hostname in production and use localhost:3000 during local development.

Outcome

Replace the mockItems array with an async fetch to /api/items that works both under vercel dev locally and in production.

Hands-on exercise 2.2

Build the request URL

In a Client Component, the browser can resolve fetch("/api/items") against the current page. A Server Component runs without that browser context, and Node's fetch requires an absolute URL.

When system environment variables are exposed to a deployment, Vercel provides VERCEL_URL as the deployment hostname, such as hazel-home-abc123.vercel.app. Because the value has no protocol, the production branch adds https://. The local branch uses 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`, { cache: "no-store" });
  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>
    </>
  );
}

The mockItems array is gone. getItems() builds an absolute URL, checks the response, and returns the decoded inventory. The no-store option keeps this route request-time rendered, so next build does not try to contact a deployment that is still being built. Making Home asynchronous lets the component wait for the data before rendering the existing card markup.

No NEXT_PUBLIC_ prefix needed

VERCEL_URL stays on the server because this request runs in a Server Component. Variables only need the NEXT_PUBLIC_ prefix when browser code must read them. In Project Settings, the Automatically expose System Environment Variables option controls whether VERCEL_URL is available.

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 dev

Open 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 reports a fetch error because the API is unavailable. That failure confirms the page now depends on FastAPI.

Try It

With vercel dev running, confirm the data is coming from FastAPI and not the mock:

  1. Open http://localhost:3000. All 8 items appear.
  2. Open http://localhost:3000/api/items in a second tab and compare the response.
  3. Edit one item's name in api/index.py, save, and hard-refresh the frontend. The name updates.

The edited product name is the useful test: the storefront is reading FastAPI's response rather than its old local array.

Commit

Save the connection between the storefront and API:

git add app/page.tsx
git commit -m "feat(storefront): fetch inventory from FastAPI"

Troubleshooting

Use the companion skill for quick checks

If you're working in academy-python-course, ask your coding agent: "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.tsx uses async function getItems() with no mockItems array
  • The request uses { cache: "no-store" } so the page renders at request time
  • The fetch URL resolves to http://localhost:3000/api/items in local dev
  • http://localhost:3000 loads real data from FastAPI through vercel dev
  • Editing api/index.py and 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`, { cache: "no-store" });
  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?

supported.