Next.js Advanced Routing
Overview
Provide comprehensive guidance for advanced Next.js App Router features including Route Handlers (API routes), Parallel Routes, Intercepting Routes, Server Actions, error handling, draft mode, and streaming with Suspense.
TypeScript: NEVER Use any Type
CRITICAL RULE: This codebase has @typescript-eslint/no-explicit-any enabled. Using any will cause build failures.
โ WRONG:
function handleSubmit(e: any) { ... }
const data: any[] = [];
โ
CORRECT:
function handleSubmit(e: React.FormEvent<HTMLFormElement>) { ... }
const data: string[] = [];
Common Next.js Type Patterns
function Page({ params }: { params: { slug: string } }) { ... }
function Page({ searchParams }: { searchParams: { [key: string]: string | string[] | undefined } }) { ... }
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { ... }
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { ... }
async function myAction(formData: FormData) { ... }
When to Use This Skill
Use this skill when:
- Creating API endpoints with Route Handlers
- Implementing parallel or intercepting routes
- Building forms with Server Actions
- Setting cookies or handling mutations
- Creating error boundaries
- Implementing draft mode for CMS previews
- Setting up streaming and Suspense boundaries
- Building complex routing patterns (modals, drawers)
โ ๏ธ CRITICAL: Server Action File Naming and Location
When work requirements mention a specific filename, follow that instruction exactly. If no name is given, pick the option that best matches the project conventionsโapp/actions.ts is a safe default for collections of actions, while app/action.ts works for a single form handler.
Choosing between action.ts and actions.ts
- Match existing patterns: Check whether the project already has an actions file and extend it if appropriate.
- Single vs multiple exports: Prefer
action.ts for a single action, and actions.ts for a group of related actions.
- Explicit requirement: If stakeholders call out a specific name, do not change it.
Location guidelines
- Server actions belong under the
app/ directory so they can participate in the App Router tree.
- Keep the file alongside the UI that invokes it unless shared across multiple routes.
- Avoid placing actions in
lib/ or utils/ unless they are triggered from multiple distant routes and remain server-only utilities.
Example placement
app/
โโโ actions.ts โ Shared actions that support multiple routes
โโโ dashboard/
โโโ action.ts โ Route-specific action colocated with a single page
Example: Creating action.ts
'use server';
export async function submitForm(formData: FormData) {
const name = formData.get('name') as string;
console.log('Submitted:', name);
}
Example: Creating actions.ts
'use server';
export async function createPost(formData: FormData) {
}
export async function deletePost(id: string) {
}
Remember: When a project requirement spells out an exact filename, mirror it precisely.
โ ๏ธ CRITICAL: Server Actions Return Types - Form Actions MUST Return Void
This is a TypeScript requirement, not optional. Even if you see code that returns data from form actions, that code is WRONG.
When using form action attribute: <form action={serverAction}>
- The function MUST have no return statement (implicitly returns void)
- TypeScript will REJECT any return value, even
return undefined or return null
- IMPORTANT: If you see example code in the codebase that returns data from a form action, ignore it - it's an anti-pattern. Fix it by removing the return statement.
โ WRONG (causes build error):
export async function saveForm(formData: FormData) {
'use server';
const name = formData.get('name') as string;
if (!name) throw new Error('Name required');
await db.save(name);
return { success: true };
}
<form action={saveForm}> {}
<input name="name" />
</form>
โ
CORRECT - Option 1 (Simple form action, no response):
export async function saveForm(formData: FormData) {
'use server';
const name = formData.get('name') as string;
if (!name) throw new Error('Name required');
await db.save(name);
revalidatePath('/');
}
<form action={saveForm}>
<input name="name" required />
<button type="submit">Save</button>
</form>
โ
CORRECT - Option 2 (With useActionState for feedback):
export async function saveForm(prevState: any, formData: FormData) {
'use server';
const name = formData.get('name') as string;
if (!name) return { error: 'Name required' };
await db.save(name);
return { success: true, message: 'Saved!' };
}
'use client';
const [state, action] = useActionState(saveForm, null);
return (
<form action={action}>
<input name="name" required />
<button type="submit">Save</button>
{state?.error