How to Access Route Parameter Inside getServerSideProps
Learn why your dynamic route parameter appears as undefined in getServerSideProps and how to correctly extract it from the context object for server-side data fetching.
The error: Cannot read property 'id' of undefined#
TypeError: Cannot read property 'id' of undefined
at getServerSideProps (pages/posts/[id].js:12:24)I hit this when I tried to access context.query.id or req.query.id inside getServerSideProps, expecting the dynamic route segment (e.g., /posts/123) to appear there. The behavior was the same across development (npm run dev) and production builds, and it reproduced in both local and deployed environments. I found this confusing at first because the naming (query vs params) is counterintuitive.
The short version: route parameters live in context.params, not req.query. Destructure { params } from the context argument, read params.id, and the TypeError goes away. The rest of this post walks through where Next.js actually stores routing data, the working fetch pattern, and the two variants that keep the error alive after the obvious fix — mixed-up query strings and catch-all routes.
Where Next.js actually puts routing data#
In Next.js Pages Router, getServerSideProps receives a context object with two distinct properties for routing data: params and query — documented in full in the official getServerSideProps API reference. params contains only the dynamic path segments from the file-based route (e.g., [id] in pages/posts/[id].js). query contains the dynamic route segments plus any URL query string parameters (e.g., for /posts/123?page=2, context.query would be { id: '123', page: '2' }). Use context.params for route segments—it is unambiguous and the idiomatic choice. I initially reached for req.query, which doesn't exist on a raw Node.js request, causing the error.
The relevant code path is:
// pages/posts/[id].js
export async function getServerSideProps(context) {
// ⚠️ Works but not preferred: context.query includes route segments AND query string
const { id } = context.query;
// ❌ Wrong: context.req is a raw Node IncomingMessage — .query does not exist (Express addition only)
// const { id } = context.req.query;
// ✅ Correct: params holds dynamic route segments
const { id } = context.params;
// Fetch data using id...
}The context object is passed by Next.js at request time and is not the same as the req/res pair used in API routes. context.req is a raw Node.js IncomingMessage object; it has no .query property at all (.query is an Express addition, not part of Node core). context.query does exist and includes both the dynamic route segments and query string parameters—but context.params is the preferred way to access route segments because it only contains the matched path segments, with no ambiguity.
The underlying invariant is worth internalizing: route parameters are path-based (params), query parameters are query-string-based (query). Next.js enforces this separation deliberately to avoid ambiguity between URL segments and query strings.
The working pattern for pages/posts/[id].js#
// pages/posts/[id].js
import { createClient } from '@/lib/supabase-client';
export async function getServerSideProps(context) {
const { params } = context;
const { id } = params;
if (!id) {
return {
notFound: true,
};
}
const supabase = createClient();
const { data: post, error } = await supabase
.from('posts')
.select('*')
.eq('id', id)
.single();
if (error || !post) {
return {
notFound: true,
};
}
return {
props: {
post,
},
};
}That single change resolved the issue for me because context.params is explicitly populated by Next.js with the matched dynamic segments from the file path, and params.id is guaranteed to be a string when the route matches.
Applying it to an existing page:
- Open
pages/posts/[id].js. - Locate the
getServerSidePropsfunction and find whereidis being accessed. - Replace
context.query.idorcontext.req.query.idwithcontext.params.id. - Add a guard clause (
if (!id) return { notFound: true }) to handle edge cases. - Save and restart the dev server (
npm run dev).
Checking that params is populated#
Run:
npm run devThen visit http://localhost:3000/posts/123 in your browser.
I saw the post with ID 123 rendered, and the terminal logged no TypeError. To confirm params was populated, I temporarily added:
console.log('context.params:', context.params);I saw:
context.params: { id: '123' }If the error persists after the change, the cause is usually one of the two scenarios below.
Segments and query strings in the same URL#
You might be mixing up query string parameters with route parameters. For example, /posts/123?draft=true has:
context.params.id === '123'context.query.draft === 'true'
If you need both, destructure both objects:
export async function getServerSideProps(context) {
const { params, query } = context;
const { id } = params;
const { draft } = query;
// Now id = '123', draft = 'true'
}Catch-all routes: params.slug is an array#
For catch-all routes like pages/posts/[...slug].js, context.params.slug is an array, not a string:
// URL: /posts/a/b/c
// context.params.slug = ['a', 'b', 'c']Handle it like this:
export async function getServerSideProps(context) {
const { params } = context;
const slugArray = Array.isArray(params.slug) ? params.slug : [params.slug];
const slug = slugArray.join('/'); // 'a/b/c'
// Use slug to fetch nested data...
}Locking it down with TypeScript#
To prevent myself from making this mistake again, I added a TypeScript type guard and ESLint rule. In types/next.d.ts:
import { GetServerSidePropsContext } from 'next';
declare module 'next' {
interface GetServerSidePropsContext<
P = Record<string, any>,
Q = Record<string, any>
> {
params?: P;
query?: Q;
}
}Then in getServerSideProps, use generics:
export async function getServerSideProps(
context: GetServerSidePropsContext<{ id: string }>
) {
const { id } = (context.params ?? {}); // ✅ Type-safe — params is typed as optional (params?: Q), so guard against undefined
}With the generic in place, reaching for a property that only exists on query (or on the non-existent req.query) becomes a compile-time error instead of a runtime TypeError.
The App Router equivalent: params is a prop, and it's async#
If the project uses App Router instead of Pages Router, there is no
getServerSideProps and no context object at all — dynamic route segments arrive as a
params prop passed directly to the page component:
// app/posts/[id]/page.tsx
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const post = await getPost(id);
// ...
}The detail that catches people migrating from the Pages Router pattern in this article:
since Next.js 15, params (and searchParams) in App Router page and layout components
are Promises that must be awaited before their properties can be read — synchronous
destructuring (const { id } = params) either throws or silently returns undefined
depending on the exact API surface touched. This is a deliberate breaking change tied to
Partial Prerendering, not a bug. Query-string values in App Router come through a
separate searchParams prop on the page component, mirroring the params/query split
this article covers for Pages Router — the names differ (searchParams vs context.query)
but the underlying idea, that path segments and query strings are separate objects, carries
over unchanged.
Catch-all segments keep the same array shape covered above: app/posts/[...slug]/page.tsx
receives params.slug as a string array once awaited, exactly like
context.params.slug in Pages Router.
Anyone mid-migration between the two routers, or maintaining both simultaneously, should
treat this as the single biggest gotcha in the switch — it fails silently in JavaScript
(reading a property off a Promise doesn't throw, it just returns undefined) and only
surfaces as a TypeScript error if params is typed correctly as a Promise in the first
place. The
Pages to App Router migration guide
walks through this alongside the rest of the data-fetching model change, including how
generateStaticParams replaces getStaticPaths.
Further reading: Fix: Next.js proxy.js matcher not working for static assets, Next.js Query String Params: searchParams + useRouter.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
Continue Reading
Fix: Next.js proxy.js matcher not working for static assets
Without a matcher, proxy.js runs on every request including _next/static and public assets. And Server Functions are not separate routes — a matcher gap silently drops auth coverage.
Next.js 'URL is malformed' error: use only absolute URLs
Reproduced on Next.js 16.4.2: the relative redirect that throws, the absolute redirect that then loops on itself, and the basePath case where new URL quietly drops the prefix.
Fix Error: NEXT_REDIRECT logged in a Route Handler catch
Reproduced on Next.js 16.4.2: why redirect() inside try/catch logs Error: NEXT_REDIRECT, sends users to the wrong URL or returns a 500, and the three fixes that hold.


