wsimmonds/claude-nextjs-skills/nextjs-advanced-routing

Claude Code and agent skill by wsimmonds/claude-nextjs-skills

Trust tier 3
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

Guide for advanced Next.js App Router patterns including Route Handlers, Parallel Routes, Intercepting Routes, Server Actions, error boundaries, draft mode, and streaming with Suspense. CRITICAL for server actions (action.ts, actions.ts files, 'use server' directive), setting cookies from client components, and form handling. Use when requirements involve server actions, form submissions, cookies, mutations, API routes, `route.ts`, parallel routes, intercepting routes, or streaming. Essential for separating server actions from client components.

Install

npx vskill@latest install wsimmonds/claude-nextjs-skills/nextjs-advanced-routing

SKILL.md

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:

typescript
function handleSubmit(e: any) { ... }
const data: any[] = [];

✅ CORRECT:

typescript
function handleSubmit(e: React.FormEvent<HTMLFormElement>) { ... }
const data: string[] = [];
Common Next.js Type Patterns
typescript
// Page props
function Page({ params }: { params: { slug: string } }) { ... }
function Page({ searchParams }: { searchParams: { [key: string]: string | string[] | undefined } }) { ... }
// Form events
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { ... }
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { ... }
// Server actions
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
typescript
// app/action.ts (single-action example)
'use server';
export async function submitForm(formData: FormData) {
const name = formData.get('name') as string;
// Process the form
console.log('Submitted:', name);
}
Example: Creating actions.ts
typescript
// app/actions.ts (multiple related actions)
'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):

typescript
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 }; // ❌ BUILD ERROR: Type mismatch
}
// In component:
<form action={saveForm}> {/* ❌ Expects void function */}
<input name="name" />
</form>

✅ CORRECT - Option 1 (Simple form action, no response):

typescript
export async function saveForm(formData: FormData) {
'use server';
const name = formData.get('name') as string;
// Validate - throw errors instead of returning them
if (!name) throw new Error('Name required');
await db.save(name);
revalidatePath('/'); // Trigger UI update
// No return statement - returns void implicitly
}
// In component:
<form action={saveForm}>
<input name="name" required />
<button type="submit">Save</button>
</form>

✅ CORRECT - Option 2 (With useActionState for feedback):

typescript
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!' }; // ✅ OK with useActionState
}
// In component:
'use client';
const [state, action] = useActionState(saveForm, null);
return (
<form action={action}>
<input name="name" required />
<button type="submit">Save</button>
{state?.error && <p>{state.error}</p>}
{state?.success && <p>{state.message}</p>}
</form>
);

The key rule: <form action={...}> expects void. If you need to return data, use useActionState.

Route Handlers (API Routes)

Basic Route Handler

Route Handlers replace API Routes from the Pages Router. Create them in route.ts or route.js files.

typescript
// app/api/hello/route.ts
export async function GET(request: Request) {
return Response.json({ message: 'Hello World' });
}
export async function POST(request: Request) {
const body = await request.json();
return Response.json({
message: 'Data received',
data: body
});
}
Supported HTTP Methods
typescript
// app/api/items/route.ts
export async function GET(request: Request) { }
export async function POST(request: Request) { }
export async function PUT(request: Request) { }
export async function PATCH(request: Request) { }
export async function DELETE(request: Request) { }
export async function HEAD(request: Request) { }
export async function OPTIONS(request: Request) { }
Dynamic Route Handlers
typescript
// app/api/posts/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const id = params.id;
const post = await db.posts.findUnique({ where: { id } });
return Response.json(post);
}
export async function DELETE(
request: Request,
{ params }: { params: { id: string } }
) {
await db.posts.delete({ where: { id: params.id } });
return Response.json({ success: true });
}
Request Headers and Cookies
typescript
// app/api/profile/route.ts
import { cookies, headers } from 'next/headers';
export async function GET(request: Request) {
// Access headers
const headersList = await headers();
const authorization = headersList.get('authorization');
// Access cookies
const cookieStore = await cookies();
const sessionToken = cookieStore.get('session-token');
if (!sessionToken) {
return Response.json({ error: 'Unauthorized' }, { status: 401 });
}
const user = await fetchUser(sessionToken.value);
return Response.json(user);
}
Setting Cookies in Route Handlers
typescript
// app/api/login/route.ts
import { cookies } from 'next/headers';
export async function POST(request: Request) {
const { email, password } = await request.json();
const token = await authenticate(email, password);
if (!token) {
return Response.json({ error: 'Invalid credentials' }, { status: 401 });
}
// Set cookie
const cookieStore = await cookies();
cookieStore.set('session-token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 60 * 60 * 24 * 7, // 1 week
path: '/',
});
return Response.json({ success: true });
}
CORS Configuration
typescript
// app/api/public/route.ts
export async function GET(request: Request) {
const data = await fetchData();
return Response.json(data, {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
});
}
export async function OPTIONS(request: Request) {
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
});
}
Streaming Responses
typescript
// app/api/stream/route.ts
export async function GET() {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
for (let i = 0; i < 10; i++) {
controller.enqueue(encoder.encode(`data: ${i}\n\n`));
await new Promise(resolve => setTimeout(resolve, 1000));
}
controller.close();
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
});
}

Server Actions

Server Actions enable server-side mutations without creating API endpoints.

Basic Server Action (Without Return)
typescript
// app/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
const post = await db.posts.create({
data: { title, content },
});
revalidatePath('/posts');
// No return statement - Server Actions with forms should return void
}

Note: See "Using Server Actions in Forms" section below for patterns that return data vs. those that don't.

⚠️ CRITICAL: Server Actions File Organization

File Naming Precision:

  • When stakeholders specify a filename (e.g., “create a server action in a file called action.ts”), mirror it exactly.
  • Common filenames: action.ts (singular) or actions.ts (plural)—choose the one that matches the brief or existing code.
  • Place the file in the appropriate directory: typically app/action.ts or app/actions.ts.

Two Patterns for 'use server' Directive:

Pattern 1: File-level (recommended for multiple actions):

typescript
// app/actions.ts
'use server'; // At the top - ALL exports are server actions
export async function createPost(formData: FormData) { ... }
export async function updatePost(formData: FormData) { ... }
export async function deletePost(postId: string) { ... }

Pattern 2: Function-level (for single action or mixed file):

typescript
// app/action.ts or any file
export async function createPost(formData: FormData) {
'use server'; // Inside the function - ONLY this function is a server action
const title = formData.get('title') as string;
await db.posts.create({ data: { title } });
}

Client Component Calling Server Action:

When a client component needs to call a server action (e.g., onClick, form submission):

  1. Create the server action in a SEPARATE file with 'use server' directive
  2. Import and use it in the client component

✅ CORRECT Pattern:

typescript
// app/actions.ts - Server Actions file
'use server';
import { cookies } from 'next/headers';
export async function updateUserPreference(key: string, value: string) {
const cookieStore = await cookies();
cookieStore.set(key, value);

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 90 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
75
Repository forks
9
7-day trend score
22

Stars and forks are for the whole GitHub repository, not this skill alone. Refreshed 17 Mar 2026, 7 months ago.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/wsimmonds/claude-nextjs-skills/nextjs-advanced-routing/badge)](https://verified-skill.com/skills/wsimmonds/claude-nextjs-skills/nextjs-advanced-routing)

Something wrong with this skill or its page? Tell us.