notAHacker
notAHacker

Reputation: 31

Fetching data from Next.js API route in Next.js frontend

I'm working on a web application with both the frontend and backend written using Next.js APP Rouer. The frontend is responsible for rendering the user interface, and the backend consists of API routes. I need assistance with making fetch requests from the Next.js frontend to the Next.js backend.

app directory consists of

 ---/(consists of all the frontend routes folder with page.ts)

 ---/api(consists of all the backend routes folder route.js)

I have a Helper function


export const UseFetchFromNext = async (endpoint) =>{
    console.log(process.env.NEXT_PUBLIC_VERSION)
    const res = await fetch(`${endpoint}`)
    const data = await res.json()
    console.log(data)
    return data;
}

Use of helper function inside server component just work fine

For example

This components uses

'use client'

useEffect(() => {
        fetchData();
        console.log(product)
        console.log(products)
    }, [slug])
    const fetchData = async () => {
        const productRes = await UseFetchFromNext(`/api/product/${slug}`);
        const productsRes = await UseFetchFromNext(`/api/products`);
        setProduct(productRes.data);
        setProducts(productsRes.data);
}

But when using the helper function on a server component it doesn't grab the default host URL so i tried using fetch separately on client component like this

This is server component

export default async function Home() {
    const res = await fetch(`/api/products`);
    const products = await res.json();
}

fetching like this throws an error as the component is server side and cannot get the host url by default

 Internal error: TypeError: Failed to parse URL from /api/products

So i made a workaround and create a .env file with

.env files

NEXT_PUBLIC_API_URL=http://localhost:3000

and made fetch request like this

server component (with env fetch)

export default async function Home() {
    const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/products`);
    const products = await res.json();
}

and it is working perfectly fine on developement on my machine
and also the build command builds it without no error
But when i'm trying to host the application on vercel it throws an error as i checks all the backend routes and optimizes it for the production

Vercel error

TypeError: fetch failed
    at Object.fetch (node:internal/deps/undici/undici:11576:11)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
  cause: Error: connect ECONNREFUSED 127.0.0.1:3000
      at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1495:16)
      at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:130:17) {
    errno: -111,
    code: 'ECONNREFUSED',
    syscall: 'connect',
    address: '127.0.0.1',
    port: 3000
  }

the Error makes sense the env variable is set to localhost and it is supposed to throw error but how can i get the hostURL in buildtime in vercel when deploying the application as the application is still building.

Is there any work around to get the enviornment variable which is equivalent to the the URL which will be provided by vercel on deployment
Any help will be highly appreciated

Upvotes: 3

Views: 3444

Answers (1)

Christian Ivicevic
Christian Ivicevic

Reputation: 10885

The problem that you are running into is that you are attempting to fetch your own route handlers from server-side code (in that case server components) which isn't the right way to do that. Instead call the server-side logic directly. Or more informal:

Don't order from the McDonalds drive through when you are already inside the restaurant.

In code this can be almost as trivial as copy-pasting the logic into your component.

Before

// app/api/products/route.ts
export async function GET() {
  const data = async db.select(...).from(...)
  return Response.json(data)
}

// app/page.tsx
export default async function Page() {
  const data = await fetch(...)
  return ...
}

After

// app/page.tsx
export default async function Page() {
  const data = async db.select(...).from(...)
  return ...
}

The whole point of server components is that you don't need to fetch your own route handlers and can perform the logic such as querying directly.

Reasons why this is idiomatic and causes less problems are the following:

  • You don't need to encode the current API URL which might be really annoying when dealing with multiple environments such as preview. Generally speaking if you feel like you need to deal with URLs pointing to your own backend something may be off.
  • Auth can get tricky as requests from the client to the server do pass on cookies such as JWTs, but fetching your route handlers from your server-side code doesn't pass on any cookies unless manually done. Thus the route handlers would erroneously think that incoming requests are unauthorized.
  • Static builds will break and not work at all. The reason is that the build process would attempt to fetch the server itself which isn't running during build time at all.
  • Fetching instead of calling the underlying logic is pretty much not type-safe.
  • As described in the McDonalds metaphor above, issuing a request while you are already on the server will incur additional round-trip times, make things slower and incur more costs.

In general route handlers are only designed for two very specific use cases:

  • Client-side fetching within your app in cases you can't or don't want to leverage the power of server components.
  • Third parties interacting with a publicly exposed version of your API. Especially in this case you can extract the logic of a route handler into a reusable function and call it from server components and route handlers.

Upvotes: 4

Related Questions