"fetch failed" (ECONNREFUSED) and "Failed to parse URL" in Next.js: Fetching Your Own API, Tested
A common pattern carried over from client-side React is a page that fetches its own API route: fetch("/api/products"). In a Server Component that breaks in two different ways, and both usually show up for the first time in next build.
I reproduced both on Next.js 16.4.4 (Node 26), then measured the fix.
1. A relative URL: "Failed to parse URL"
Error occurred prerendering page "/products".
TypeError: Failed to parse URL from /api/data
[cause]: TypeError: Invalid URL
code: 'ERR_INVALID_URL',
input: '/api/data'In the browser, "/api/data" is resolved against the page's address. On the server there is no page address, and Node's fetch needs a full URL, so it throws before any request is made.
2. An absolute URL to localhost: "fetch failed"
The usual next attempt is a full URL, often from an environment variable like http://localhost:3000. That gets past the parsing, then fails differently:
Error occurred prerendering page "/products".
[TypeError: fetch failed] {
[cause]: AggregateError:
code: 'ECONNREFUSED',
Error: connect ECONNREFUSED ::1:1468
Error: connect ECONNREFUSED 127.0.0.1:1468During next build, static pages are rendered before any server is running, so nothing is listening at that address. (My test used port 1468; you'll see 3000 or whatever your URL says.) It also explains the classic "works in dev, fails in build": in dev your server is running, so the request succeeds.
Using your production URL instead doesn't really fix it. During a build on a host like Vercel, that URL still points to the previous deployment, so the build gets the old version's data, or a 404 for a route that's new in this deploy, and the first deploy of a new site has nothing to call at all.
The fix: call the code directly
// lib/products.ts: the logic, in one place
export async function getProducts() {
return db.product.findMany();
}
// app/api/products/route.ts: still there for client components and other apps
import { getProducts } from "@/lib/products";
export async function GET() {
return Response.json(await getProducts());
}
// app/products/page.tsx: Server Component calls the function, no HTTP
import { getProducts } from "@/lib/products";
export default async function Page() {
const products = await getProducts();
// ...
}A Server Component already runs on the server, so it can do whatever the route handler does. Move the logic into a function, call it from both the route and the page. The page built with no server running.
It's also less work at runtime. With the server running, I made the page dynamic and timed 30 requests each: going through the API route averaged 3.8 ms per page, calling the function directly 2.0 ms. That's on one machine with no database. On serverless hosting the API call can be a whole second function invocation, so the gap is usually larger.
If you must call a URL
- Fetching an external API (a CMS, Shopify, a payment provider) is fine: those servers are up during your build. These errors are only about fetching your own app.
- If the page can't be built ahead of time, make it dynamic (call connection() or read cookies), so the fetch happens at request time when the server is running. The absolute-URL version worked that way in my test. It's still slower than calling the function.
- "fetch failed" against an external API during build usually means a network or DNS problem on the build machine, or the API blocking it. Check the [cause] line: ECONNREFUSED, ENOTFOUND and certificate errors each point to a different problem.
All results come from one Next.js 16.4.4 project built with next build and run with next start on Node 26.
Related
- Dynamic server usage: couldn't be rendered statically
- Next.js environment variables undefined
- Next.js revalidate not working
- Next.js development