Use when adding new error messages to React, or seeing "unknown error code" warnings.
npx skills add aussiegingersnap/cursor-skills --skill "nextjs-16"
Install specific skill from multi-skill repository
# Description
Next.js 16 specific patterns including proxy.ts (replaces middleware), cache components, and App Router conventions. This skill should be used when setting up a new Next.js 16 project or migrating from earlier versions.
# SKILL.md
name: nextjs-16
description: Next.js 16 specific patterns including proxy.ts (replaces middleware), cache components, and App Router conventions. This skill should be used when setting up a new Next.js 16 project or migrating from earlier versions.
Next.js 16 Skill
Patterns and conventions specific to Next.js 16, including the new proxy.ts system, caching strategies, and modern App Router patterns.
When to Use This Skill
- Setting up a new Next.js 16 project
- Migrating from Next.js 16 or earlier
- Implementing route protection with proxy.ts
- Configuring caching and PPR
- Understanding Next.js 16 breaking changes
Breaking Changes in Next.js 16
proxy.ts Replaces middleware.ts
This is the biggest change. The Edge-based middleware.ts is replaced by Node.js-based proxy.ts.
| Feature | middleware.ts (15) | proxy.ts (16) |
|---|---|---|
| Runtime | Edge | Node.js |
| Export | middleware |
proxy |
| Async | Limited | Full async/await |
| Node APIs | Not available | Available |
proxy.ts Setup
Basic Structure
Create proxy.ts at project root (same level as app/):
import { NextRequest, NextResponse } from 'next/server';
export async function proxy(request: NextRequest): Promise<NextResponse> {
// Your logic here
return NextResponse.next();
}
export const config = {
matcher: [
// Match all paths except static files and API routes
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
};
Authentication Proxy
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@/lib/auth';
import { headers } from 'next/headers';
const protectedRoutes = ['/dashboard', '/settings', '/profile'];
const authRoutes = ['/login', '/signup', '/forgot-password'];
export async function proxy(request: NextRequest): Promise<NextResponse> {
const { pathname } = request.nextUrl;
const isProtectedRoute = protectedRoutes.some((route) =>
pathname.startsWith(route)
);
const isAuthRoute = authRoutes.some((route) =>
pathname.startsWith(route)
);
// Skip for non-protected, non-auth routes
if (!isProtectedRoute && !isAuthRoute) {
return NextResponse.next();
}
// Get session (full async now works!)
const session = await auth.api.getSession({
headers: await headers(),
});
// Redirect unauthenticated users from protected routes
if (isProtectedRoute && !session) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('redirect', pathname);
return NextResponse.redirect(loginUrl);
}
// Redirect authenticated users from auth routes
if (isAuthRoute && session) {
const redirect = request.nextUrl.searchParams.get('redirect') || '/dashboard';
return NextResponse.redirect(new URL(redirect, request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};
Adding Headers
export async function proxy(request: NextRequest): Promise<NextResponse> {
const response = NextResponse.next();
// Add security headers
response.headers.set('X-Frame-Options', 'DENY');
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
return response;
}
Geolocation / Feature Flags
export async function proxy(request: NextRequest): Promise<NextResponse> {
const response = NextResponse.next();
// Access geo data (if available from your provider)
const country = request.geo?.country || 'US';
response.headers.set('x-user-country', country);
return response;
}
Project Structure
Recommended Layout
project-root/
βββ proxy.ts # Route protection (NEW in 16)
βββ next.config.ts # Next.js configuration
βββ drizzle.config.ts # Database configuration
βββ package.json
βββ tsconfig.json
βββ .env.local # Environment variables
βββ src/
β βββ app/
β β βββ layout.tsx # Root layout
β β βββ page.tsx # Home page
β β βββ (auth)/ # Auth route group
β β β βββ login/
β β β βββ signup/
β β βββ (dashboard)/ # Dashboard route group
β β β βββ layout.tsx
β β β βββ page.tsx
β β β βββ settings/
β β βββ api/
β β βββ _lib/ # API utilities
β β βββ auth/ # Auth endpoints
β β βββ [resources]/
β βββ components/
β β βββ ui/ # Base UI components
β β βββ [features]/ # Feature components
β βββ lib/
β β βββ db/ # Database
β β βββ auth.ts # Auth config
β β βββ auth-client.ts
β βββ hooks/ # React hooks
β βββ stores/ # Zustand stores
β βββ providers/ # React providers
βββ drizzle/
β βββ migrations/ # Database migrations
βββ public/ # Static assets
Route Groups
Use route groups (folder) for organization without affecting URLs:
app/
βββ (marketing)/ # Marketing pages
β βββ page.tsx # β /
β βββ about/ # β /about
β βββ pricing/ # β /pricing
βββ (auth)/ # Auth pages (shared layout optional)
β βββ login/ # β /login
β βββ signup/ # β /signup
βββ (dashboard)/ # Dashboard (separate layout)
βββ layout.tsx # Dashboard layout with sidebar
βββ page.tsx # β /dashboard
βββ settings/ # β /dashboard/settings
Server Components vs Client Components
Default: Server Components
// This is a Server Component by default
// Can directly fetch data, access DB, etc.
export default async function ProjectsPage() {
const projects = await db.query.project.findMany();
return (
<div>
{projects.map((project) => (
<ProjectCard key={project.id} project={project} />
))}
</div>
);
}
Client Components
Add 'use client' directive when you need:
- Event handlers (onClick, onChange)
- State (useState, useReducer)
- Effects (useEffect)
- Browser APIs
- Custom hooks that use above
'use client';
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
Composition Pattern
Server Component with Client Component children:
// Server Component (page.tsx)
export default async function DashboardPage() {
const data = await fetchData();
return (
<div>
<h1>Dashboard</h1>
{/* Pass server data to client component */}
<InteractiveChart data={data} />
</div>
);
}
// Client Component (interactive-chart.tsx)
'use client';
export function InteractiveChart({ data }: { data: ChartData }) {
const [filter, setFilter] = useState('all');
// Interactive logic
}
Data Fetching
In Server Components
async function getData() {
// This runs on the server
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 }, // Cache for 1 hour
});
return res.json();
}
export default async function Page() {
const data = await getData();
return <div>{/* Use data */}</div>;
}
Caching Options
// No cache (dynamic)
fetch(url, { cache: 'no-store' });
// Cache forever (static)
fetch(url, { cache: 'force-cache' });
// Revalidate after N seconds
fetch(url, { next: { revalidate: 60 } });
// Revalidate on-demand with tags
fetch(url, { next: { tags: ['projects'] } });
// Then invalidate:
import { revalidateTag } from 'next/cache';
revalidateTag('projects');
Server Actions
Basic Server Action
// actions.ts
'use server';
import { db, project } from '@/lib/db';
import { auth } from '@/lib/auth';
import { headers } from 'next/headers';
import { revalidatePath } from 'next/cache';
import { z } from 'zod';
const createSchema = z.object({
name: z.string().min(1).max(255),
});
export async function createProject(formData: FormData) {
const session = await auth.api.getSession({ headers: await headers() });
if (!session) throw new Error('Unauthorized');
const parsed = createSchema.safeParse({
name: formData.get('name'),
});
if (!parsed.success) {
return { error: 'Invalid input' };
}
await db.insert(project).values({
name: parsed.data.name,
userId: session.user.id,
});
revalidatePath('/dashboard/projects');
return { success: true };
}
Using in Forms
// Client Component
'use client';
import { createProject } from './actions';
import { useActionState } from 'react';
export function CreateProjectForm() {
const [state, formAction, isPending] = useActionState(createProject, null);
return (
<form action={formAction}>
<input name="name" required />
<button type="submit" disabled={isPending}>
{isPending ? 'Creating...' : 'Create Project'}
</button>
{state?.error && <p className="text-red-500">{state.error}</p>}
</form>
);
}
Loading and Error States
Loading UI
// app/dashboard/loading.tsx
export default function Loading() {
return <DashboardSkeleton />;
}
Error Handling
// app/dashboard/error.tsx
'use client';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
);
}
Not Found
// app/dashboard/[id]/page.tsx
import { notFound } from 'next/navigation';
export default async function ProjectPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const project = await getProject(id);
if (!project) {
notFound();
}
return <div>{project.name}</div>;
}
// app/dashboard/[id]/not-found.tsx
export default function NotFound() {
return <div>Project not found</div>;
}
next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
// Recommended settings
reactStrictMode: true,
poweredByHeader: false,
// Image domains
images: {
remotePatterns: [
{
protocol: 'https',
hostname: '*.googleusercontent.com',
},
],
},
// Experimental features
experimental: {
// Enable if using Partial Pre-rendering
// ppr: true,
},
};
export default nextConfig;
Environment Variables
Naming Convention
# Server-only (default)
DATABASE_URL=postgres://...
BETTER_AUTH_SECRET=...
# Client-accessible (NEXT_PUBLIC_ prefix)
NEXT_PUBLIC_APP_URL=http://localhost:3000
NEXT_PUBLIC_POSTHOG_KEY=...
Accessing
// Server-side (any variable)
const dbUrl = process.env.DATABASE_URL;
// Client-side (only NEXT_PUBLIC_ variables)
const appUrl = process.env.NEXT_PUBLIC_APP_URL;
TypeScript Configuration
Ensure tsconfig.json includes:
{
"compilerOptions": {
"target": "ES2022",
"lib": ["dom", "dom.iterable", "esnext"],
"jsx": "preserve",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noEmit": true,
"incremental": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"isolatedModules": true,
"plugins": [
{ "name": "next" }
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
Migration from Next.js 16
Checklist
- [ ] Rename
middleware.tstoproxy.ts - [ ] Change export from
middlewaretoproxy - [ ] Update any Edge-specific code (now runs on Node.js)
- [ ] Review and update
next.config.jsβnext.config.ts - [ ] Test all protected routes
- [ ] Verify async operations in proxy work correctly
# Supported AI Coding Agents
This skill is compatible with the SKILL.md standard and works with all major AI coding agents:
Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.