DEV Community

Cover image for How to Access Route Parameter Inside getServerSideProps
Mahdi BEN RHOUMA
Mahdi BEN RHOUMA

Posted on Originally published at iloveblogs.blog

How to Access Route Parameter Inside getServerSideProps

The error: Cannot read property 'id' of undefined

TypeError: Cannot read property 'id' of undefined
    at getServerSideProps (pages/posts/[id].js:12:24)
Enter fullscreen mode Exit fullscreen mode

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. 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...
}
Enter fullscreen mode Exit fullscreen mode

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,
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Open pages/posts/[id].js.
  2. Locate the getServerSideProps function and find where id is being accessed.
  3. Replace context.query.id or context.req.query.id with context.params.id.
  4. Add a guard clause (if (!id) return { notFound: true }) to handle edge cases.
  5. Save and restart the dev server (npm run dev).

Checking that params is populated

Run:

npm run dev
Enter fullscreen mode Exit fullscreen mode

Then 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);
Enter fullscreen mode Exit fullscreen mode

I saw:

context.params: { id: '123' }
Enter fullscreen mode Exit fullscreen mode

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'
}
Enter fullscreen mode Exit fullscreen mode

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']
Enter fullscreen mode Exit fullscreen mode

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...
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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.

Related


Originally published at https://www.iloveblogs.blog

Top comments (0)