skills/nextjs-16/SKILL.md
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.
npx skillsauth add aussiegingersnap/cursor-skills nextjs-16Install this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Patterns and conventions specific to Next.js 16, including the new proxy.ts system, caching strategies, and modern App Router patterns.
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 |
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).*)',
],
};
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).*)'],
};
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;
}
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-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
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
// 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>
);
}
Add 'use client' directive when you need:
'use client';
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
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
}
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>;
}
// 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');
// 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 };
}
// 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>
);
}
// app/dashboard/loading.tsx
export default function Loading() {
return <DashboardSkeleton />;
}
// 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>
);
}
// 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>;
}
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;
# 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=...
// Server-side (any variable)
const dbUrl = process.env.DATABASE_URL;
// Client-side (only NEXT_PUBLIC_ variables)
const appUrl = process.env.NEXT_PUBLIC_APP_URL;
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"]
}
middleware.ts to proxy.tsmiddleware to proxynext.config.js → next.config.tstools
# Versioning Skill Semantic versioning automation based on conventional commits. Automatically manages version bumps, changelogs, and git tags using `standard-version`. ## When to Use - Before releasing a new version - When preparing a deployment - To generate/update CHANGELOG.md - When the user asks about version management - Setting up versioning for a new project ## Prerequisites - Conventional commits enforced (recommended: lefthook) - Node.js project with package.json ## Setup (One-Ti
tools
Theme generation with tweakcn for shadcn/ui and Magic UI animations. Use when setting up project themes, customizing color schemes, adding dark mode, or integrating animated components.
tools
shadcn/studio component library with MCP integration, theme generation, and block patterns. This skill should be used when building UI with shadcn components, selecting dashboard layouts, or generating landing pages. Canonical source for all shadcn-based work.
development
Enforce a precise, minimal design system inspired by Linear, Notion, and Stripe. Use this skill when building dashboards, admin interfaces, or any UI that needs Jony Ive-level precision - clean, modern, minimalist with taste. Every pixel matters.