---
title: "Wire Next.js to FastAPI"
description: "Convert the Next.js page component from a synchronous mock-data render to an async server component that fetches from the FastAPI API on the same origin, using Vercel's auto-injected VERCEL_URL so the code works locally and in production without any manual env vars."
canonical_url: "https://vercel.com/academy/python-on-vercel/wire-nextjs-to-fastapi"
md_url: "https://vercel.com/academy/python-on-vercel/wire-nextjs-to-fastapi.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-05-11T01:24:32.370Z"
content_type: "lesson"
course: "python-on-vercel"
course_title: "Python on Vercel"
prerequisites:  []
---

<agent-instructions>
Vercel Academy — structured learning, not reference docs.
Lessons are sequenced.
Adapt commands to the human's actual environment (OS, package manager, shell, editor) — detect from project context or ask, don't assume.
The lesson shows one path; if the human's project diverges, adapt concepts to their setup.
Preserve the learning goal over literal steps.
Quizzes are pedagogical — engage, don't spoil.
Quiz answers are included for your reference.
</agent-instructions>

# Wire Next.js to FastAPI

# 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

1. Replace `mockItems` with an async `getItems()` function
2. Build the fetch URL from `process.env.VERCEL_URL` with a localhost fallback
3. Make `Home` an `async` component

## 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:

```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>
    </>
  );
}
```

Three things changed from the starter:

1. The `mockItems` array is gone
2. `getItems()` builds an absolute URL from `VERCEL_URL` (in production) or `localhost:3000` (locally) and fetches from `/api/items`
3. `Home` is now `async` so it can `await` the fetch

The component markup is identical. Same grid, same cards, same badges. Only the data source changed.

\*\*Note: No NEXT\_PUBLIC\_ prefix needed\*\*

`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:

```bash
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 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:

1. Open `http://localhost:3000`. All 8 items appear.
2. Open `http://localhost:3000/api/items` in a second tab. Same data.
3. 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

\*\*Note: Use the companion skill for quick checks\*\*

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.tsx` uses `async function getItems()` with no `mockItems` array
- [ ] 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

```tsx
// 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>
    </>
  );
}
```


---

[Full course index](/academy/llms.txt) · [Sitemap](/academy/sitemap.md)
