Demo, all content is generated
Question

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

Solved · 472 views · asked by nkechi_o · edited

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 · edited

3 answers

Marked as helpful by the asker
dev_ana · edited

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 · edited
femi_o · edited

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 · edited
sofia_gr · edited

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