Skip to main content
ASoc
Tutorial

getServerSideProps in Next.js 16: Where Each Piece Went in the App Router

The Pages Router function has no App Router export. Here is the mapping, using the dashboard, auth layout and notFound pages of a real Next.js 16 app.

The ASoc Team7 min read

getServerSideProps is the Pages Router function that runs on the server for every request to a page, then hands its return value to the component as props. The App Router has no such export. You write the page as an async component, read params and searchParams directly, and call redirect() or notFound() instead of returning them. This site, on Next.js 16, has zero getServerSideProps calls.

What it does, and where each piece went

getServerSideProps runs on the server only, on every request, before the page renders. Whatever it returns under props is serialized into the page for the browser. It can also return redirect: { destination, permanent } or notFound: true.

Each of those has a direct App Router replacement, and this repo uses all of them:

getServerSidePropsApp Router equivalentWhere this repo does it
context.paramsparams prop (a Promise)src/app/templates/[slug]/page.tsx
context.querysearchParams prop (a Promise)src/app/dashboard/page.tsx
context.req.cookies / headerscookies() / headers() from next/headerssrc/lib/supabase/server.ts
return { redirect: {...} }redirect() from next/navigationsrc/app/dashboard/layout.tsx
return { notFound: true }notFound() from next/navigationsrc/app/templates/[slug]/page.tsx
return { props }The page's own JSX; there is no prop hand-offAny page.tsx
A shared check on every pageA layout.tsx that runs the check oncesrc/app/dashboard/layout.tsx
Custom response headersA Route Handler or proxy.tssrc/app/api/download/route.ts

Two of the rows changed shape, not just spelling. params and searchParams are Promises in Next 16, so you await them. And props disappears: the page fetches and renders in one function, so there is nothing to serialize between them.

Query, then render

The dashboard is this site's clearest getServerSideProps case: per-user data that must be fresh on every request.

// src/app/dashboard/page.tsx
export default async function DashboardPage({
  searchParams,
}: {
  searchParams: Promise<{ verification?: string; tab?: string }>;
}) {
  const { verification, tab } = await searchParams;
  // ... supabase.auth.getUser() ...
  const [profile, orders, slots, deliveries, refunds] = await Promise.all([
    supabase.from("profiles") /* ... */,
    supabase.from("orders") /* ... */,
    supabase.from("entitlement_slots") /* ... */,
    supabase.from("download_deliveries") /* ... */,
    supabase.from("refund_requests") /* ... */,
  ]);

In the Pages Router that body would sit inside getServerSideProps and the component would take five props. Here the five queries run in parallel in one Promise.all and the data goes straight into the JSX that uses it. Reading searchParams is also what makes the route dynamic: it renders per request, which is the whole behaviour you were asking getServerSideProps for.

Redirects belong in a layout

getServerSideProps made you repeat the auth check on every page. The App Router lets you put it in the layout once:

// src/app/dashboard/layout.tsx
export default async function DashboardLayout({ children }: { children: ReactNode }) {
  const supabase = await createClient();
  const { data } = await supabase.auth.getClaims();
  if (!data?.claims) {
    redirect("/login?next=/dashboard");
  }
  // ...
}

Every /dashboard/* route inherits the guard, so a new page cannot forget it. redirect() throws a special error, so nothing below it runs and you do not return it. Use getClaims(), which verifies the JWT; the unverified getSession() would let a forged cookie through.

One honest caveat: layouts do not re-render on every client-side navigation between their children, so a layout guard is not a substitute for checking authorization where the data is read. The dashboard page calls getUser() itself for that reason.

notFound for a missing record

// src/app/templates/[slug]/page.tsx
export default async function TemplateDetailPage({ params }: { params: Promise<Params> }) {
  const { slug } = await params;
  const product = getProduct(slug);
  if (!product) notFound();

This is return { notFound: true } without the return. This route also exports generateStaticParams, so it is prerendered at build time, which is the other half of the Pages Router pair (getStaticProps). You pick static or dynamic per route by what the route reads, not by which function you export.

Static or per-request: the decision

DataPages RouterApp Router
Same for everyone, known at buildgetStaticPropsPlain async component (prerendered)
Same for everyone, changes sometimesgetStaticProps + revalidatefetch caching or revalidate
Depends on the request (cookie, query)getServerSidePropsRead cookies() / searchParams
Mostly static, one live widgetAwkwardStatic page with a client component for the live part

For a measured example of how much of a real app ends up static versus dynamic, see React server-side rendering. For the full router comparison, see App Router vs Pages Router.

Migrating a page

  1. Delete the getServerSideProps export and make the component async.
  2. Replace context.params with await params and context.query with await searchParams.
  3. Move the data call into the component body.
  4. Swap return { redirect } for redirect() and return { notFound: true } for notFound().
  5. Replace req.headers / req.cookies with await headers() / await cookies().
  6. If it set Cache-Control, move that to a Route Handler or the segment's caching config.

Redirects that do not depend on the request are better in next.config.ts; the Next.js redirects walkthrough covers where each kind belongs.

Troubleshooting

SymptomCauseFix
getServerSideProps does nothingExported from a file under app/It is ignored there. Fetch in the component
params.slug is undefinedNot awaited in Next 15 or laterconst { slug } = await params
Page is served staleNothing in it reads request data, so it was prerenderedRead cookies() / searchParams, or export dynamic = "force-dynamic"
redirect() seems to do nothingCalled inside a try / catch that swallows itCall it outside the try, or rethrow
Data duplicated in the page payloadPassing server data to a client component as propsPass only what the client needs
Waterfall: slow pageSequential awaitsBatch with Promise.all, as the dashboard does

Frequently asked questions

Is getServerSideProps removed from Next.js? No. It still works in the Pages Router. It simply does not exist in the App Router, where a page.tsx that reads request data becomes dynamic on its own.

Can I use getServerSideProps and the App Router together? Yes, in one project. Pages under pages/ keep their data functions and routes under app/ use async components. Do not map one URL in both.

How do I set a Cache-Control header like I did in getServerSideProps? A page cannot set response headers. Use a Route Handler, like src/app/api/download/route.ts here, or proxy.ts for headers on many routes.

Does the secret stay on the server? Yes, and more strictly than before. With getServerSideProps anything under props reaches the browser. An async server component only sends rendered output, so a database row never serializes unless you pass it to a client component.

Templates in this post

ASoc Zenith (a growth marketing agency site), ASoc Aegis (a risk management landing page) and ASoc Ally (an AI support chatbot site) each have a Next.js edition, so you can read the App Router approach above against a full landing page.

Browse the full sets: Next.js landing page templates, Tailwind landing page templates.

Keep reading

Tutorial13 min read

Next.js Image Optimization Without next/image: 255 KiB to 33 KiB

A closed image set does not need a request-time optimiser. The sharp build script, the plain-img markup, and the lazy LCP image that cost us 1.67s of load delay.

Read more
Tutorial8 min read

Next.js Intercepting Routes: Why This Codebase Has Zero

Intercepting routes mask navigation to a route in your own app. This storefront's live-preview modal shows a cross-origin iframe instead — a real reason the pattern never fit here.

Read more