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.
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:
getServerSideProps | App Router equivalent | Where this repo does it |
|---|---|---|
context.params | params prop (a Promise) | src/app/templates/[slug]/page.tsx |
context.query | searchParams prop (a Promise) | src/app/dashboard/page.tsx |
context.req.cookies / headers | cookies() / headers() from next/headers | src/lib/supabase/server.ts |
return { redirect: {...} } | redirect() from next/navigation | src/app/dashboard/layout.tsx |
return { notFound: true } | notFound() from next/navigation | src/app/templates/[slug]/page.tsx |
return { props } | The page's own JSX; there is no prop hand-off | Any page.tsx |
| A shared check on every page | A layout.tsx that runs the check once | src/app/dashboard/layout.tsx |
| Custom response headers | A Route Handler or proxy.ts | src/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
| Data | Pages Router | App Router |
|---|---|---|
| Same for everyone, known at build | getStaticProps | Plain async component (prerendered) |
| Same for everyone, changes sometimes | getStaticProps + revalidate | fetch caching or revalidate |
| Depends on the request (cookie, query) | getServerSideProps | Read cookies() / searchParams |
| Mostly static, one live widget | Awkward | Static 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
- Delete the
getServerSidePropsexport and make the componentasync. - Replace
context.paramswithawait paramsandcontext.querywithawait searchParams. - Move the data call into the component body.
- Swap
return { redirect }forredirect()andreturn { notFound: true }fornotFound(). - Replace
req.headers/req.cookieswithawait headers()/await cookies(). - 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
| Symptom | Cause | Fix |
|---|---|---|
getServerSideProps does nothing | Exported from a file under app/ | It is ignored there. Fetch in the component |
params.slug is undefined | Not awaited in Next 15 or later | const { slug } = await params |
| Page is served stale | Nothing in it reads request data, so it was prerendered | Read cookies() / searchParams, or export dynamic = "force-dynamic" |
redirect() seems to do nothing | Called inside a try / catch that swallows it | Call it outside the try, or rethrow |
| Data duplicated in the page payload | Passing server data to a client component as props | Pass only what the client needs |
| Waterfall: slow page | Sequential awaits | Batch 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.
