Repository: vercel/next.js
Pull request: feat: add RouteContext type for app router route handlers

## Add typed RouteContext for App Router route handlers

This PR adds type support for route handlers in the App Router by introducing a new `RouteContext` interface. This allows for strongly-typed access to route parameters in API routes.

### Features:

- Adds a new global `RouteContext<AppRouteHandlerRoute>` interface with typed `params` property
- Adds test cases for various route parameter patterns
- Updates type tests to verify correct type checking for route handlers

This enhancement improves the developer experience by providing type safety when accessing route parameters in API route handlers, similar to the existing type support for pages and layouts.

Changed files:
filename: packages/next/src/server/lib/router-utils/typegen.ts
patch: @@ -569,6 +569,20 @@ declare global {
   } & {
     [K in LayoutSlotMap[LayoutRoute]]: React.ReactNode
   }
+
+  /**
+   * Context for Next.js App Router route handlers
+   * @example
+   * \`\`\`tsx
+   * export async function GET(request: NextRequest, context: RouteContext<'/api/users/[id]'>) {
+   *   const { id } = await context.params
+   *   return Response.json({ id })
+   * }
+   * \`\`\`
+   */
+  interface RouteContext<AppRouteHandlerRoute extends AppRouteHandlerRoutes> {
+    params: Promise<ParamMap[AppRouteHandlerRoute]>
+  }
 }
 `
 }

filename: test/e2e/app-dir/typed-routes/app/api/docs/[...slug]/route.ts
patch: @@ -0,0 +1,9 @@
+import { NextRequest } from 'next/server'
+
+export async function GET(
+  request: NextRequest,
+  context: RouteContext<'/api/docs/[...slug]'>
+) {
+  const { slug } = await context.params
+  return Response.json({ slug })
+}

filename: test/e2e/app-dir/typed-routes/app/api/shop/[[...category]]/route.ts
patch: @@ -0,0 +1,9 @@
+import { NextRequest } from 'next/server'
+
+export async function GET(
+  request: NextRequest,
+  context: RouteContext<'/api/shop/[[...category]]'>
+) {
+  const { category } = await context.params
+  return Response.json({ category })
+}

filename: test/e2e/app-dir/typed-routes/app/api/users/[id]/route.ts
patch: @@ -0,0 +1,18 @@
+import { NextRequest } from 'next/server'
+
+export async function GET(
+  request: NextRequest,
+  context: RouteContext<'/api/users/[id]'>
+) {
+  const { id } = await context.params
+  return Response.json({ id })
+}
+
+export async function POST(
+  request: NextRequest,
+  context: RouteContext<'/api/users/[id]'>
+) {
+  const { id } = await context.params
+  const body = await request.json()
+  return Response.json({ id, ...body })
+}

filename: test/e2e/app-dir/typed-routes/typed-routes.test.ts
patch: @@ -2,7 +2,7 @@ import { nextTestSetup } from 'e2e-utils'
 
 const expectedDts = `
 type AppRoutes = "/" | "/_shop/[[...category]]" | "/dashboard" | "/dashboard/settings" | "/docs/[...slug]" | "/gallery/photo/[id]" | "/project/[slug]"
-type AppRouteHandlerRoutes = never
+type AppRouteHandlerRoutes = "/api/docs/[...slug]" | "/api/shop/[[...category]]" | "/api/users/[id]"
 type PageRoutes = "/about" | "/users/[id]"
 type LayoutRoutes = "/" | "/dashboard"
 type RedirectRoutes = "/blog/[category]/[[...slug]]" | "/project/[slug]"
@@ -59,20 +59,33 @@ describe('typed-routes', () => {
     })
   }
 
+  it('should generate RouteContext type for route handlers', async () => {
+    const dts = await next.readFile('.next/types/routes.d.ts')
+    expect(dts).toContain(
+      'interface RouteContext<AppRouteHandlerRoute extends AppRouteHandlerRoutes>'
+    )
+    expect(dts).toContain('params: Promise<ParamMap[AppRouteHandlerRoute]>')
+  })
+
   if (isNextStart) {
     it('should throw type errors', async () => {
       await next.stop()
       await next.patchFile(
         'app/type-testing.ts',
         `type ValidPage = PageProps<'/dashboard'>
-type InvalidPage = PageProps<'/dasboard'>`
+type InvalidPage = PageProps<'/dasboard'>
+type ValidRoute = RouteContext<'/api/users/[id]'>
+type InvalidRoute = RouteContext<'/api/users/invalid'>`
       )
 
       const { cliOutput } = await next.build()
 
       expect(cliOutput).toContain(
         `Type '"/dasboard"' does not satisfy the constraint 'AppRoutes'.`
       )
+      expect(cliOutput).toContain(
+        `Type '"/api/users/invalid"' does not satisfy the constraint 'AppRouteHandlerRoutes'.`
+      )
     })
   }
 })