Fetching own API endpoint in React Server Components
As part of data-fetching, I'm fetching the data from my own API routes or route handlers with a
fetch
inside a server component like so:While this works fine in dev mode, it fails to build. Also I had to use an absolute URL instead of a nicer relative URL. Why is that? Is this the right way to fetch data?
No, this is not the right way to fetch data in server components. In short, do not fetch your own API routes or route handlers (from now on, just "route handlers") in server components, instead just call the server-side logic directly. Imagine server components not as normal React components, but as the good old getStaticProps
and getServerSideProps
. Instead of the code above, you should do something like this:
If you find yourself having to fetch from localhost:3000
in your own Next.js app (assuming this Next.js app runs on port 3000) and the fetch requires an absolute URL, you are very likely doing something wrong.
If you worry about authentication or similar stuff, cookies
and headers
are helpful.
Route handlers should be instead used for client-side fetching, interactions with other services/applications, etc. If you find yourself repeating code in both the server component and the route handler, you can extract the common code into a separate utility function (say, getUser(id: number): User | null
) and import it in both places.
Of course, this only applies to server components. In client components, you can fetch your own route handlers normally with swr
or react-query
(see also Ā Ā When is a component a client component, in the app router?). That being said, we recommend you to use server actions for mutations, because it integrates very nicely with the React router used by the app
directory.
Now read on to see why fetch
-ing route handlers in server components is a bad idea.
Why does it require absolute URLs?
fetch
in client-side does not require absolute URLs, because the browser knows the URL of the current page and hence can understand what host/domain to use. But, in server components, the fetch is run on the server, which is like running a fetch
from a Node.js script inside your own computer. It doesn't know what host/domain to use (you don't know the domain of your computer do you?), so you have to specify it explicitly.
Why does it not work during build? Why does it work in dev mode?
It requires the server to be running at (in the example above) localhost:3000
. That is fulfilled during development mode and during runtime (next start
), so the fetch works fine there. But during build, the server is not running, so the fetch fails.
Why does the documentation describe fetch
inside server components then? For what?
For communicating with a separate backend service (api.yourapp.com
) or a third-party API (api.thirdparty.com
).
What are the drawbacks of the approach?
The most evident drawback of the approach is that it doesn't work (see above). But what if it did work, or what if you only use it in the limited scenarios where it does work? What are the drawbacks then?
-
The fetch does not have access to headers and cookies automatically. You need to manually extract the relevant informations with
headers
andcookies
and pass them to thefetch
call. -
It is not type-safe. In the examples above, if you use
fetch
, since the server component knows nothing about what would be responded by the route handler, the type has to beany
and you have to manually type-check/validate the response. But if you use something like Prisma with server components directly as above, the type is available automatically (so you know if something can benull
or you didn't typouser.name
touser.nmae
). -
If you self-host, it is slower, albeit only very very slightly. It is essentially your server fetching data from itself, while pretending itself as a separate server. The overhead added by that pretense is small but it is there.
-
If you host the Next.js app on Vercel or a similar serverless platform, chances are it can be significantly slower. At least for Vercel's case, your route handlers and dynamic server components are deployed to AWS Lambda, which can require a cold start if the lambda is not used for a while. We don't know how Vercel bundles the route handlers and server components to different lambdas, but if your route handler happens to need a cold start when you request the server component, you are taking that cold start penalty that could've been avoided if you didn't use a route handler in the first place.
-
It brings zero benefits. Server components are run exclusively on the server anyway, you need not worry about server-side logic/variables leaking to the browser.
-
You would be shooting yourself and other future maintainers in the foot. That is generally a bad idea.