skip to content

In a Next.js project migrating a pages/ route that used getServerSideProps, what replaces it in the App Router, and what changes about where the data is fetched?

level: middleimportance: must knowfreq 72%

answer

  1. the function disappears, it does not move
  2. the component itself becomes async
  3. no props object crosses to the client
  4. context.req becomes cookies() and headers()
  5. per-request is opt-in now, not automatic

basics

~20 s

The App Router has no one-for-one replacement: the page component itself becomes an async Server Component that awaits its data inline. There is no exported data function and no props payload, and per-request freshness becomes something you opt into.

solid answer

~50 s

In the Pages Router, `getServerSideProps` was a separate exported function that ran on the server, returned a serializable `props` object, and had that object shipped to the client to hydrate the page. The App Router deletes that indirection entirely — `page.tsx` is itself an async Server Component, so it just `await`s the query or the `fetch` and renders the result. The pieces of the old `context` argument map onto explicit APIs: `context.req`/`context.res` become `cookies()` and `headers()` from `next/headers`, and returning `{ notFound: true }` or `{ redirect }` becomes calling `notFound()` or `redirect()` from `next/navigation`. Two things bite people: `getServerSideProps` exported from a file inside `app/` is simply dead code, Next never calls it; and the old function guaranteed a per-request run, whereas an App Router page renders statically unless something in it opts into dynamic rendering.

code

tsx · 10 lines
tsx
// pages/products/[id].tsx — Pages Router
export async function getServerSideProps(context) {
  const product = await getProduct(context.params.id);
  if (!product) return { notFound: true };
  return { props: { product } };
}

export default function ProductPage({ product }) {
  return <h1>{product.name}</h1>;
}

go deeper

for a junior

Be able to say that in the App Router the page component is itself async and fetches its own data, and that getServerSideProps belongs to the older pages/ directory.

for a middle

Explain the mapping precisely — the data function disappears rather than being renamed, context.req becomes cookies() and headers(), and no serialized props object is shipped to the client any more.

for a senior

Show that you know the per-request guarantee does not carry over: name what forces a route to render dynamically, and describe how you would audit a migrated route to confirm it is not being prerendered stale.

for a principal

Own the migration mechanics at scale: a lint rule or codemod that flags Pages Router data functions surviving inside app/, and a convention for where data fetching lives now that any component in the tree can do it.

## What getServerSideProps actually was In the Pages Router, a route file exported the component *and*, optionally, one of a small set of magic data functions. `getServerSideProps` was the per-request one: Next called it on the server for every request to that route, passed it a `context` object holding `params`, `query`, `req`, `res`, and expected back an object of the shape `{ props }`, `{ notFound: true }`, or `{ redirect: { destination, permanent } }`. The key structural fact is the boundary. The function ran on the server, the component ran on the server *and* on the client, and the only thing that crossed between them was the `props` object — serialized to JSON, embedded in the HTML as part of the Next data payload, and re-read by the client during hydration. That serialization step is why `props` could not contain a `Date`, a class instance, or a function without special handling, and why a large result set inflated the HTML twice (once as rendered markup, once as JSON). ## What the App Router does instead In `app/`, `page.tsx` is a Server Component by default, and a Server Component may be `async`. So the data fetch moves *into* the component: ```tsx // app/products/[id]/page.tsx export default async function Page({ params }) { const { id } = await params; const product = await db.product.findUnique({ where: { id } }); return <h1>{product.name}</h1>; } ``` There is no exported data function to replace `getServerSideProps` with, and there is no props payload: the component's output is streamed as rendered UI, so the raw data never has to be serialized to the browser unless you explicitly hand it to a client component. The second structural change is *granularity*. `getServerSideProps` was one function per route, so every piece of data a page needed had to be gathered at the top and drilled down. In the App Router any Server Component anywhere in the tree can fetch what it needs, so data fetching co-locates with the component that renders it. ## The mapping, function by function - `getServerSideProps` → `async` Server Component, plus an explicit opt-in to dynamic rendering where per-request freshness matters. - `getStaticProps` → `async` Server Component; the page is prerendered at build time when nothing in it forces dynamic rendering. - `getStaticPaths` → `generateStaticParams`. - `getInitialProps` → nothing. It was already legacy in the Pages Router and has no App Router form. - `context.req` / `context.res` → `cookies()` and `headers()` from `next/headers`. - `{ notFound: true }` → `notFound()` from `next/navigation`. - `{ redirect: { destination } }` → `redirect()` from `next/navigation`. ## The two mistakes that actually happen **Leaving the old function in place.** Copy a route file from `pages/` into `app/`, keep the `export async function getServerSideProps`, and nothing complains loudly — Next simply never calls it in the App Router, so the page renders with undefined data. The export is dead code and should be deleted, not adapted. **Assuming freshness carries over.** `getServerSideProps` had one behaviour: run on every request. An App Router page has two possible behaviours, and the default leans static. If the route must reflect the current request — a personalised dashboard, a page reading a session cookie — reading `cookies()` or `headers()` already forces dynamic rendering; a page that fetches from an API with no such signal may be prerendered once and served from that build output. Migrations that translate the code faithfully but ignore this ship stale pages. Related: the caching behaviour of `fetch()` itself has moved between Next major versions — in Next 15 and later, `fetch` results are not cached unless you ask for caching. Do not port a Pages Router mental model of "the server always refetches" and assume it holds; state the caching you want explicitly with the `cache` or `next.revalidate` options rather than relying on whatever the default is on the version you happen to be on. One more version detail worth knowing during a migration: from Next 15 onward the `params` and `searchParams` props handed to a page are Promises and must be awaited, where the Pages Router handed you plain objects on `context`.

  • If someone leaves getServerSideProps exported from a file under app/, what actually happens at runtime?
    Nothing calls it. The App Router does not recognise the Pages Router data functions, so the export is inert dead code and the component renders without whatever it was meant to supply. There is no loud runtime error to catch it, which is why route-by-route migration needs a review step that deletes those exports rather than porting them.
  • getServerSideProps could return { redirect } or { notFound: true }. What replaces those two return shapes?
    Calling `redirect()` or `notFound()` from `next/navigation` inside the Server Component. They are called rather than returned, and they work by throwing, so code after the call does not run — which also means you should not wrap them in a try/catch that swallows everything.
  • Why did the props object of getServerSideProps have to be JSON-serializable, and does that constraint survive in the App Router?
    Because the object was embedded in the HTML and re-read by the client during hydration, so it had to survive a JSON round trip. In the App Router the constraint does not apply to data a Server Component merely renders — that never leaves the server. It reappears the moment you pass a value as a prop to a client component, since that prop does cross the boundary.

saying these in an interview costs you the question

  • Says getServerSideProps still works if you put it in app/
  • Claims every App Router page runs on every request like getServerSideProps did
  • Thinks getStaticProps maps to a different exported function in app/
  • Assumes page data is still serialized into the HTML as a props payload
  • Believes you must fetch everything in the top-level page and drill it down

context