In the Next.js App Router, `app/orders/page.tsx` queries the database directly and awaits the result. Which pieces of the classic browser-fetch setup does that remove, and what does the browser receive instead?
answer
- one less hop in the middle
- the endpoint existed only for the browser
- spinner state becomes a file convention
- driver and credentials never bundled
- markup and RSC payload, not JSON
basics
~20 sapp/orders/page.tsx runs only on the server, so it awaits the query and renders the rows itself. The API route, the browser fetch, and the useEffect loading and error state all disappear. The browser receives rendered markup plus the RSC payload, never the query.
solid answer
~40 sIn the App Router every file under `app/` is a Server Component unless the module graph is entered through `'use client'`, so `page.tsx` can be an `async function` that awaits the ORM call and renders the rows in the same file. That deletes three layers: the `app/api/orders` route that existed only to expose the data to the browser, the client-side `fetch` of it, and the `useEffect` plus `isLoading`/`error` state that drove the spinner. Pending UI moves to the `loading` file convention instead of component state. The browser gets streamed HTML plus the RSC payload describing the rendered tree; on a client navigation it fetches only that payload. The SQL, the driver and the connection string stay on the server because that module is never part of a client entry point.
code
typescript · 14 lines// app/orders/page.tsx — a Server Component by default
import { db } from '@/lib/db'
export default async function OrdersPage() {
const orders = await db.order.findMany({ take: 20 })
return (
<ul>
{orders.map((o) => (
<li key={o.id}>{o.total}</li>
))}
</ul>
)
}go deeper
Be ready to say that a page file in the App Router runs on the server by default, can be async, and can await data directly — so no API endpoint, no fetch and no loading flag are needed for its own data.
Explain what actually travels: streamed HTML plus the RSC payload on first load, payload only on client navigation, with the query and driver staying server-side because that module is never in a client entry point.
Show the judgment about when the removed layer should come back — a stable HTTP surface for external consumers — and call out the self-fetch anti-pattern where a server component calls its own route handler over the network.
Own the architectural consequence: data access is no longer funnelled through one endpoint layer, so the contract discipline, authorization and query budgeting that the API layer used to enforce have to be relocated deliberately rather than assumed.
## The shape this replaces The pattern most engineers arrive with looks like this: a component mounts, an effect fires, the browser calls `/api/orders`, that endpoint opens a database connection, runs the query, serializes rows to JSON, and sends them back; the component calls `setState` and finally paints. Every one of those steps is a thing you had to build and maintain — an endpoint URL, a JSON contract, an `isLoading` flag, an `error` flag, and a second copy of the row type on the client. And the data only starts loading *after* the JavaScript bundle has downloaded, parsed and hydrated. ## What the App Router does instead In the App Router the default is inverted. A module under `app/` is server-side unless something pulls it into a client entry point, which is what `'use client'` creates. Because the component renders only on the server, it may be declared `async` and `await` anything the server can do — an ORM call, a database driver, an internal SDK, the filesystem. ```tsx // app/orders/page.tsx — no 'use client' anywhere above it import { db } from '@/lib/db' export default async function OrdersPage() { const orders = await db.order.findMany({ take: 20 }) return <ul>{orders.map(o => <li key={o.id}>{o.total}</li>)}</ul> } ``` There is no `/api/orders`. There is no `fetch`. The data and the markup that displays it are produced in one pass, in one place. ## What actually reaches the browser Two things, and neither is your query. On a first load the response is streamed HTML — the finished list — plus the RSC payload, a serialized description of the rendered tree that React uses to reconcile on the client. On a subsequent client-side navigation to that route, Next requests only the RSC payload for the new segment; no HTML document is fetched and no JSON API is involved. What does *not* reach the browser is the interesting half: the query text, the ORM, the database driver, and the credential the driver used. Those modules are imported by a server module graph only, so they are never part of a client bundle. This is why importing a Node-only package in `page.tsx` does not break the browser build. ## Where loading and error state went They became file conventions rather than component state. A `loading` file in the segment supplies pending UI while the segment's server work is in flight, and an `error` file catches a throw. You are no longer writing `if (isLoading) return <Spinner/>` inside the component that also owns the data, because the component never exists in a pending state on the client — it either rendered or it did not. ## Per-component, not per-page The removal is not only at the page level. Any Server Component in the tree can fetch what it alone needs, so a sidebar component queries its own counts and a table component queries its own rows, with no prop drilling from a single top-level loader and no `getServerSideProps`-style single entry point for the whole route. Data access sits next to the markup that consumes it. ## What it does not remove Route handlers are still needed for consumers that are not this app: webhooks, third-party clients, mobile apps, and anything that needs a stable HTTP surface. Mutations are not covered by this at all — reading is what a Server Component does; writing goes through a Server Action or a route handler. And data that must change in response to interaction without a navigation still needs a client-side path. ## The two traps The first is adding `'use client'` to `page.tsx` because something inside it needs interactivity — that pulls the whole page into the browser graph and the direct `await` stops being legal. The fix is to keep the interactive part in its own client component and leave the page on the server. The second is calling your own API from the server: `await fetch('http://localhost:3000/api/orders')` inside a Server Component. That works, but it pays for an extra HTTP hop out of the process and back in, forces you to know an absolute URL in every environment, and gives up the direct database access that was the point. If the component is already on the server, call the data source, not your own endpoint.
- If there is no API route any more, what does the browser request when the user navigates client-side to that page?Only the RSC payload for the new route segment. Next fetches a serialized description of the server-rendered tree and reconciles it into the existing page; no HTML document is requested and no JSON endpoint is hit. The database query still ran on the server as part of producing that payload.
- When is writing a route handler still the right call in an App Router app?When the consumer is not this app's own rendering pass — webhooks, third-party integrations, a mobile client, an OAuth callback, or anything that needs a stable versioned HTTP surface. A route handler that exists purely so your own Server Components can fetch from it is an extra network hop for nothing.
- A colleague adds 'use client' to page.tsx so a button in it can have an onClick. What breaks?The page can no longer be an async component awaiting the database — that module is now a client entry point, and everything it imports is destined for the browser bundle, including the ORM. Keep the page on the server and extract only the button into its own client component.
It is the difference between a cook shouting an order through a hatch to a second cook and waiting for the plate to come back, and simply having the ingredients on the same counter.
saying these in an interview costs you the question
- Thinks the database query runs in the browser after hydration
- Says the page still needs an API route for its own data
- Believes the ORM and connection string ship in the client bundle
- Assumes useEffect is required to fetch data in page.tsx
- Thinks the browser receives JSON rows and renders them itself