Question

Server component fetching my own /api route: 'fetch failed' on Vercel, fine locally

Solved · 474 viewsasked by nkechi_o

My dashboard page (server component) loads data like this:

const res = await fetch("http://localhost:3000/api/stats");
const stats = await res.json();

Works locally. On Vercel the build fails with:

TypeError: fetch failed
  [cause]: Error: connect ECONNREFUSED 127.0.0.1:3000

Cursor wants to replace localhost with an env var for the site URL. Is that the fix?

What I’ve tried

Tried a relative URL (/api/stats), then it fails with 'Failed to parse URL' instead.

Comment
Are you on Vercel or self-hosting? The answer changes a bit. dmitri_v

3 answers

Marked as helpful by the asker
dev_ana

Don't fetch your own API from a server component at all. Both run on the server, so you are making an HTTP request to yourself, and during next build there is nobody listening, hence ECONNREFUSED.

Move the logic into a plain function and call it from both places:

// lib/stats.ts
export async function getStats(userId: string) {
  const supabase = await createClient();
  const { data } = await supabase.from("orders").select("total").eq("user_id", userId);
  return summarize(data ?? []);
}
// app/dashboard/page.tsx
const stats = await getStats(user.id);

Keep /api/stats only if something outside your app (a mobile app, a client component) needs it; it can call getStats too. One less network hop, no URL juggling, and it works in every environment.

Comment
Moved 3 of these to lib/ functions, build passes and the dashboard is faster too. Deleted two API routes nothing else used. nkechi_o
femi_o

Yes, use an absolute URL based on the environment:

const base = process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : "http://localhost:3000";
const res = await fetch(`${base}/api/stats`);
Comment
This works, but on previews with Deployment Protection turned on you'll get a 401 back, and during the build there is no server to call. See my answer. dev_ana
sofia_gr

Worth adding: put import "server-only"; at the top of lib/stats.ts. If someone (or Cursor) later imports it into a client component, the build fails loudly instead of shipping your query logic to the browser.

Comment