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
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 moreThis 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
useParamsinside a server component**: Next.js 15's early canary threw hydration errors becauseuseParamsis a client hook. I had to wrap the cache in a client component boundary. - **Automated
generateStaticParamsgeneration**: 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.


