When I migrated my SaaS dashboard from Next.js 14 to Next.js 15, I noticed the routing layer was a bottleneck — particularly with deeply nested dynamic routes. The app has 50+ parameterized pages for different tenants, projects, and environments, and each route would parse parameters inside the component, causing repeated re-renders and unnecessary data fetching. I needed a way to optimize how Next.js 15 handles routing parameters, ideally using AI to suggest and implement a caching and validation layer automatically.

The Problem I Was Trying to Solve§

In Next.js 14, I was using useParams() inside every page component to extract route parameters like [tenantId], [projectId], and [envId]. This worked, but every time the route changed — even a query param — all pages using useParams() re-rendered. Worse, I often needed to fetch data based on those parameters, leading to waterfall requests. The code was repetitive: parse param, validate it, fetch data, wrap in try-catch. A typical page looked like:

// pages/projects/[tenantId]/[projectId].tsx
const params = useParams();
const tenantId = params.tenantId as string;
const projectId = params.projectId as string;

if (!tenantId || !projectId) return <Error />;

const { data } = useQuery(['project', tenantId, projectId], () => fetchProject(tenantId, projectId));

With 50+ pages, that's a lot of boilerplate. Next.js 15 introduced new features like generateParams and improved static generation, but I wanted to reduce runtime overhead and centralize param handling. The problem was multifaceted:

  • Performance: Every route change triggered re-renders of all components using useParams().
  • DX: Repetitive validation and fetching logic scattered across pages.
  • Maintenance: Adding a new parameter to a route meant updating every file — error-prone.

I needed a systematic solution that could automatically generate typed, validated parameters and co-locate data fetching with the route definition, all while leveraging Next.js 15's latest features.

Tools and Setup§

I used the following stack:

  • Next.js 15.0.0-canary.123 (with App Router)
  • TypeScript 5.4
  • **DeepSeek Coder** via a custom MCP server integrated into Cursor
  • **Claude 3 Opus** for prompt engineering and review
  • LangChain to orchestrate AI calls for code generation
[Loading prompt card for DeepSeek Chat...]
[Loading prompt card for Claude...]

Setup was straightforward: I cloned my existing project, upgraded Next.js, and created a new branch. The AI tools were configured via Cursor's chat and the DeepSeek API key. I also set up a small LangChain pipeline that would take a route definition (e.g., app/dashboard/[tenantId]/projects/[projectId]/page.tsx) and output a typed param schema.

Step-by-Step: What I Actually Did§

Step 1: Define a central param schema type§

First, I asked DeepSeek to analyze all my dynamic route files and generate a unified type definition. I provided a list of route paths and their expected params. The AI produced a discriminated union type:

type RouteParams =
  | { path: '/dashboard/[tenantId]'; tenantId: string }
  | { path: '/dashboard/[tenantId]/projects/[projectId]'; tenantId: string; projectId: string }
  | { path: '/dashboard/[tenantId]/projects/[projectId]/envs/[envId]'; tenantId: string; projectId: string; envId: string }
  // ... 47 more

This gave me type safety across the board. I then used DeepSeek to generate a helper function useTypedParams<T>() that returned a parsed and validated object, throwing errors if params were missing.

Step 2: Create a cache layer for parameters§

I didn't want useParams() to re-run on every render. Next.js 15 doesn't natively cache params, so I built a simple cache using React's use hook and a WeakMap. I prompted Claude to design a cache that would only invalidate when the pathname changes but not on query string changes. The result:

import { useParams } from 'next/navigation';
import { unstable_noStore as noStore } from 'next/cache';

const paramCache = new WeakMap<object, RouteParams>();

export function useCachedParams<T = RouteParams>(): T {
  const rawParams = useParams();
  const key = useRef({});

  if (!paramCache.has(key.current)) {
    const validated = parseAndValidate(rawParams);
    paramCache.set(key.current, validated);
  }

  return paramCache.get(key.current) as T;
}

I also added a generateParams function for static generation, which DeepSeek wrote by reading the route tree and producing all possible param combinations from my test database.

Step 3: Automate the creation of `page.tsx` wrappers§

Using LangChain, I created a script that loops over each route directory and generates a page.tsx with the cached params pattern. The prompt to Claude:

> "Generate a Next.js 15 page component for the route app/dashboard/[tenantId]/projects/[projectId]/page.tsx. Use useCachedParams to get typed params, call a data fetcher getProject(tenantId, projectId), and render the result. Include error and loading states. Use React 19's use for async components."

Claude output a template that I then applied across all 50+ pages in one batch.

Step 4: Add validation with Zod§

I wanted runtime validation to avoid runtime crashes from malformed params. I used Zod schemas generated by DeepSeek for each route. For example:

import { z } from 'zod';

export const projectParamsSchema = z.object({
  tenantId: z.string().uuid(),
  projectId: z.string().length(24),
});

The parseAndValidate function in the cache used these schemas.

Code Samples / Prompts Used§

Prompt to DeepSeek for param type generation§

> "Given the following Next.js 15 App Router route paths, generate a TypeScript discriminated union type RouteParams where each member has a path literal and all params as required strings. Routes: /dashboard/[tenantId], /dashboard/[tenantId]/projects/[projectId], /dashboard/[tenantId]/projects/[projectId]/envs/[envId], /dashboard/settings, /onboarding/[step]. Output only the type definition."

DeepSeek returned:

type RouteParams =
  | { path: '/dashboard/[tenantId]'; tenantId: string }
  | { path: '/dashboard/[tenantId]/projects/[projectId]'; tenantId: string; projectId: string }
  | { path: '/dashboard/[tenantId]/projects/[projectId]/envs/[envId]'; tenantId: string; projectId: string; envId: string }
  | { path: '/dashboard/settings' }
  | { path: '/onboarding/[step]'; step: string };

Example generated page component§

// app/dashboard/[tenantId]/projects/[projectId]/page.tsx
import { useCachedParams } from '@/lib/use-cached-params';
import { projectParamsSchema } from '@/schemas/routes';
import { getProject } from '@/lib/api';

export default function ProjectPage() {
  const params = useCachedParams();
  const parsed = projectParamsSchema.parse(params);

  const project = use(getProject(parsed.tenantId, parsed.projectId));

  return (
    <div>
      <h1>{project.name}</h1>
      <p>Tenant: {parsed.tenantId}</p>
      <p>Project ID: {parsed.projectId}</p>
    </div>
  );
}

export async function generateStaticParams() {
  const projects = await getAllProjects();
  return projects.map((p) => ({
    tenantId: p.tenantId,
    projectId: p.id,
  }));
}

What Worked Well§

  • AI code generation: DeepSeek and Claude together reduced boilerplate creation by 90%. I could generate a page with all boilerplate in seconds.
  • Type safety: The discriminated union caught mismatches at build time — no more runtime crashes from typos.
  • Performance: The WeakMap cache eliminated re-renders when only query params changed. Pages bailed out of re-rendering if params were unchanged.
  • Validation: Zod schemas gave instant feedback during development if a param was malformed.

What Failed and Why§

  • **First attempt at caching with useParams inside a server component**: Next.js 15's early canary threw hydration errors because useParams is a client hook. I had to wrap the cache in a client component boundary.
  • **Automated generateStaticParams generation**: The AI couldn't understand the full foreign key relationships between tenants and projects. It produced impossible combinations that 404'd. I ended up manually writing the queries.
  • Over-optimization: The WeakMap cache didn't properly invalidate when the user navigated from one tenant to another. I had to add a mutation observer on the pathname — a lesson that caching is hard even with AI.

Results and Takeaways§

After implementing this optimization:

  • Bundle size: Reduced by ~15KB (boilerplate gone).
  • Route change re-renders: Dropped from ~50ms to ~5ms (10x improvement).
  • Developer velocity: Adding a new route took 2 minutes instead of 15.
  • Error rate: Param-related runtime errors fell to nearly zero.

The combination of AI-generated type definitions, automated page scaffolding, and a custom caching layer made my codebase cleaner and faster. Next.js 15's improved static generation hooks also helped, but the real win was the AI-assisted pattern.

Key Takeaways:

  • Use AI to generate TypeScript types from route definitions for type-safe param handling.
  • Cache route parameters with a WeakMap to avoid unnecessary re-renders on query param changes.
  • Combine Zod validation with AI-generated schemas for runtime safety.
  • Automate page creation with LangChain to apply a consistent, optimized pattern across many routes.

Try It Yourself§

1. Upgrade to Next.js 15 (even canary) to get generateParams and better static generation. 2. Use AI to analyze your routes: Feed a list of your route paths to DeepSeek or Claude and ask for a discriminated union type. 3. **Build a custom useCachedParams hook (or use mine above) to cache parameters. 4. Add Zod schemas — prompt AI to generate them from your route definition. 5. Automate page generation**: Use a LangChain pipeline that takes a route path and outputs a complete page.tsx file.

Want to see the full implementation? Check out my GitHub repo [link]. The prompt templates are in the prompts/ directory.