Skip to main content

Next.js 16 Cheatsheet

Comprehensive reference for Next.js 16 features including App Router, Server Components, Cache Components, Server Actions, async params/searchParams, and best practices

Table of Contents


Prerequisites

System Requirements πŸ“‹

  • Node.js: Version 20.9 or later (required)
  • TypeScript: Version 5.1 or later (if using TypeScript)
  • React: Version 19.2+ (bundled with Next.js 16)
  • Browsers: Chrome, Edge, Firefox 111+, Safari 16.4+
Terminal window
# Check Node.js version
node --version
# β†’ v20.9.0 or higher
# Check TypeScript version (if installed)
tsc --version
# β†’ Version 5.1.0 or higher

Key Changes in Next.js 16 ⚠️

  • Turbopack: Default bundler (replaces Webpack)
  • Cache Components: New 'use cache' directive replaces force-static
  • Async Params: params and searchParams are now Promises
  • Proxy: middleware.ts replaced with proxy.ts (Node.js runtime only)
  • AMP Support: Removed entirely

Installation & Setup

Create New Project πŸš€

Terminal window
# Using pnpm (recommended)
pnpm create next-app@latest my-next-app
# Using npm
npx create-next-app@latest my-next-app
# Using yarn
yarn create next-app my-next-app
# With TypeScript and App Router
pnpm create next-app@latest my-app --typescript --app

Upgrade Existing Project πŸ”„

Terminal window
# Upgrade Next.js and React
pnpm add next@latest react@latest react-dom@latest
# Upgrade TypeScript (if using)
pnpm add -D typescript@latest @types/node@latest @types/react@latest @types/react-dom@latest

Development Server πŸ–₯️

Terminal window
# Start dev server (Turbopack is default)
pnpm dev
# Use Webpack instead (if needed)
pnpm dev --webpack
# Start on specific port
pnpm dev --port 3001

Project Structure

App Router Directory Structure πŸ“

my-next-app/
β”œβ”€β”€ app/ # App Router directory
β”‚ β”œβ”€β”€ layout.tsx # Root layout (required)
β”‚ β”œβ”€β”€ page.tsx # Home page (/)
β”‚ β”œβ”€β”€ loading.tsx # Loading UI
β”‚ β”œβ”€β”€ error.tsx # Error UI
β”‚ β”œβ”€β”€ not-found.tsx # 404 page
β”‚ β”œβ”€β”€ global.css # Global styles
β”‚ β”œβ”€β”€ about/
β”‚ β”‚ └── page.tsx # /about route
β”‚ β”œβ”€β”€ blog/
β”‚ β”‚ β”œβ”€β”€ layout.tsx # Blog layout
β”‚ β”‚ β”œβ”€β”€ page.tsx # /blog route
β”‚ β”‚ └── [slug]/
β”‚ β”‚ └── page.tsx # Dynamic route /blog/:slug
β”‚ β”œβ”€β”€ api/
β”‚ β”‚ └── users/
β”‚ β”‚ └── route.ts # API route /api/users
β”‚ └── (auth)/ # Route group (doesn't affect URL)
β”‚ β”œβ”€β”€ login/
β”‚ β”‚ └── page.tsx # /login
β”‚ └── register/
β”‚ └── page.tsx # /register
β”œβ”€β”€ public/ # Static assets
β”œβ”€β”€ components/ # Shared components
β”œβ”€β”€ lib/ # Utility functions
β”œβ”€β”€ next.config.js # Next.js configuration
β”œβ”€β”€ tsconfig.json # TypeScript config
└── package.json

File Conventions πŸ“„

FilePurposeRequired
layout.tsxShared UI for segmentRoot: Yes
page.tsxUnique UI for routeYes
loading.tsxLoading UINo
error.tsxError UINo
not-found.tsx404 UINo
route.tsAPI endpointNo
template.tsxRe-rendered layoutNo

App Router & Routing

Basic Routes πŸ›£οΈ

// app/page.tsx - Home route (/)
export default function Home() {
return <h1>Home Page</h1>;
}
// app/about/page.tsx - /about route
export default function About() {
return <h1>About Page</h1>;
}
// app/contact/page.tsx - /contact route
export default function Contact() {
return <h1>Contact Page</h1>;
}

Dynamic Routes πŸ”€

app/blog/[slug]/page.tsx
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return <h1>Blog Post: {slug}</h1>;
}
// app/shop/[...slug]/page.tsx - Catch-all route
export default async function Shop({
params,
}: {
params: Promise<{ slug: string[] }>;
}) {
const { slug } = await params;
return <h1>Shop: {slug?.join("/")}</h1>;
}
// app/docs/[[...slug]]/page.tsx - Optional catch-all
export default async function Docs({
params,
}: {
params: Promise<{ slug?: string[] }>;
}) {
const { slug } = await params;
return <h1>Docs: {slug?.join("/") || "Home"}</h1>;
}

Route Groups πŸ“¦

// app/(marketing)/about/page.tsx β†’ /about
// app/(marketing)/contact/page.tsx β†’ /contact
// app/(shop)/products/page.tsx β†’ /products
// Route groups don't affect URL structure
// Useful for organizing routes and applying layouts

Parallel Routes ⚑

// app/@analytics/page.tsx - Slot for analytics
// app/@team/page.tsx - Slot for team
// app/dashboard/layout.tsx - Uses @analytics and @team slots
export default function DashboardLayout({
children,
analytics,
team,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
team: React.ReactNode;
}) {
return (
<>
{children}
{analytics}
{team}
</>
);
}

Intercepting Routes πŸ”„

// app/(.)login/page.tsx - Intercepts /login at same level
// app/(..)login/page.tsx - Intercepts /login one level up
// app/(...)login/page.tsx - Intercepts /login from root
// Useful for modals and overlays
import Link from 'next/link'
// Basic link
<Link href="/about">About</Link>
// With query parameters
<Link href="/blog/1">Blog</Link>
// Using object syntax
<Link
href={{
pathname: '/blog',
query: { page: '1', sort: 'date' }
}}
>
Blog
</Link>
// Prefetching (default: true)
<Link href="/about" prefetch={false}>About</Link>
// Replace instead of push
<Link href="/login" replace>Login</Link>
// External link (no prefetch)
<Link href="https://example.com" target="_blank" rel="noopener noreferrer">
External
</Link>

Programmatic Navigation 🧭

"use client";
import { useRouter } from "next/navigation";
export default function Component() {
const router = useRouter();
const handleClick = () => {
router.push("/dashboard");
// router.replace('/dashboard') // Replace current history entry
// router.back() // Go back
// router.forward() // Go forward
// router.refresh() // Refresh current route
};
return <button onClick={handleClick}>Navigate</button>;
}

Server Components

Basic Server Component πŸ–₯️

app/components/ServerComponent.tsx
// Server Components are default - no directive needed
export default async function ServerComponent() {
// Can use async/await directly
const data = await fetch("https://api.example.com/data");
const json = await data.json();
return <div>{json.title}</div>;
}

βœ… Do’s

  • Use Server Components by default
  • Fetch data directly in Server Components
  • Use async/await for data fetching
  • Pass data as props to Client Components
  • Use Server Components for static content

❌ Don’ts

  • Don’t use hooks (useState, useEffect) in Server Components
  • Don’t use browser APIs (window, document)
  • Don’t use event handlers (onClick, onChange)
  • Don’t use 'use client' directive in Server Components

Accessing Request Data πŸ“₯

import { headers, cookies } from "next/server";
export default async function Page() {
// Read headers
const headersList = await headers();
const userAgent = headersList.get("user-agent");
// Read cookies
const cookieStore = await cookies();
const theme = cookieStore.get("theme")?.value;
return <div>User Agent: {userAgent}</div>;
}

Async Params & SearchParams πŸ”„

app/blog/[slug]/page.tsx
export default async function BlogPost({
params,
searchParams,
}: {
params: Promise<{ slug: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) {
// Must await params and searchParams
const { slug } = await params;
const { page } = await searchParams;
return (
<div>
<h1>Post: {slug}</h1>
<p>Page: {page}</p>
</div>
);
}
// Using PageProps helper (TypeScript)
import type { PageProps } from "next";
export default async function BlogPost(props: PageProps<"/blog/[slug]">) {
const { slug } = await props.params;
const query = await props.searchParams;
return <h1>Blog Post: {slug}</h1>;
}

Client Components

Basic Client Component πŸ’»

"use client"; // Must be first line
import { useState, useEffect } from "react";
export default function ClientComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log("Component mounted");
}, []);
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}

Using Params in Client Components πŸ”„

"use client";
import { use } from "react";
export default function Page({
params,
searchParams,
}: {
params: Promise<{ slug: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) {
// Use React.use() to unwrap promises in Client Components
const { slug } = use(params);
const { query } = use(searchParams);
return (
<div>
Slug: {slug}, Query: {query}
</div>
);
}

βœ… Do’s

  • Use 'use client' directive at top of file
  • Use hooks (useState, useEffect, etc.)
  • Use browser APIs and event handlers
  • Keep Client Components small and focused
  • Pass data from Server Components as props

❌ Don’ts

  • Don’t fetch data directly in Client Components (use Server Components)
  • Don’t use Server-only APIs (headers, cookies) in Client Components
  • Don’t make entire app a Client Component unnecessarily

Data Fetching

Fetch API with Caching Strategies πŸ“‘

// Static data (cached at build time)
export default async function Page() {
const res = await fetch("https://api.example.com/data", {
cache: "force-cache", // Default
});
const data = await res.json();
return <div>{data.title}</div>;
}
// Dynamic data (refetch on every request)
export default async function Page() {
const res = await fetch("https://api.example.com/data", {
cache: "no-store",
});
const data = await res.json();
return <div>{data.title}</div>;
}
// Time-based revalidation
export default async function Page() {
const res = await fetch("https://api.example.com/data", {
next: { revalidate: 3600 }, // Revalidate every hour
});
const data = await res.json();
return <div>{data.title}</div>;
}
// Tag-based revalidation
export default async function Page() {
const res = await fetch("https://api.example.com/data", {
next: { tags: ["posts"] },
});
const data = await res.json();
return <div>{data.title}</div>;
}

Database Queries πŸ—„οΈ

import { sql } from "@vercel/postgres";
export default async function Page() {
const { rows } = await sql`
SELECT * FROM posts WHERE published = true
`;
return (
<ul>
{rows.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}

Loading States ⏳

app/blog/loading.tsx
export default function Loading() {
return <div>Loading blog posts...</div>;
}
// Or use Suspense
import { Suspense } from "react";
export default function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<BlogPosts />
</Suspense>
);
}

Error Handling ⚠️

app/blog/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>
);
}

Cache Components

Using 'use cache' Directive πŸ’Ύ

// File-level caching
"use cache";
export default async function Page() {
const data = await fetch("https://api.example.com/data");
return <div>{/* ... */}</div>;
}
// Component-level caching
export async function Bookings({ type }: { type: string }) {
"use cache";
const data = await fetch(`/api/bookings?type=${type}`);
return <div>{/* ... */}</div>;
}
// Function-level caching
export async function getProducts() {
"use cache";
const data = await db.query("SELECT * FROM products");
return data;
}

Cache Life Configuration ⏱️

import { cacheLife } from "next/cache";
export default async function Page() {
"use cache";
cacheLife("hours"); // Cache for hours
// cacheLife('days') // Cache for days
// cacheLife('max') // Maximum cache lifetime
const data = await fetch("https://api.example.com/data");
return <div>{/* ... */}</div>;
}

Cache Tags 🏷️

import { cacheTag } from "next/cache";
export async function getPosts() {
"use cache";
cacheTag("posts");
const posts = await fetchPosts();
return posts;
}
// Revalidate by tag
import { revalidateTag } from "next/cache";
export async function updatePost(id: string) {
await db.posts.update(id, data);
revalidateTag("posts", "max"); // Second arg is cacheLife profile
}

Update Tag (Read-Your-Writes) ✍️

"use server";
import { updateTag } from "next/cache";
export async function updateUserProfile(userId: string, profile: Profile) {
await db.users.update(userId, profile);
updateTag(`user-${userId}`); // Expires cache, fetches fresh data
}

βœ… Do’s

  • Use 'use cache' for static or infrequently changing data
  • Use cacheTag() to mark cached data for revalidation
  • Use updateTag() for read-your-writes semantics
  • Use cacheLife() to control cache duration

❌ Don’ts

  • Don’t use runtime APIs (cookies(), headers()) in cached components
  • Don’t cache user-specific data without proper tags
  • Don’t forget to revalidate when data changes

Server Actions

Basic Server Action 🎯

app/actions.ts
"use server";
export async function createPost(formData: FormData) {
const title = formData.get("title") as string;
const content = formData.get("content") as string;
await db.posts.create({ title, content });
revalidatePath("/blog");
}
// app/components/CreatePost.tsx
("use client");
import { createPost } from "../actions";
export default function CreatePost() {
return (
<form action={createPost}>
<input name="title" type="text" />
<textarea name="content" />
<button type="submit">Create Post</button>
</form>
);
}

Inline Server Actions πŸ“

app/page.tsx
export default function Page() {
async function createInvoice(formData: FormData) {
"use server";
const rawFormData = {
customerId: formData.get("customerId"),
amount: formData.get("amount"),
};
// Mutate data
}
return <form action={createInvoice}>{/* form fields */}</form>;
}

Server Actions with Extra Arguments πŸ”§

app/actions.ts
"use server";
export async function updateUser(userId: string, formData: FormData) {
const name = formData.get("name") as string;
await db.users.update(userId, { name });
}
// app/components/UserProfile.tsx
("use client");
import { updateUser } from "../actions";
export function UserProfile({ userId }: { userId: string }) {
const updateUserWithId = updateUser.bind(null, userId);
return (
<form action={updateUserWithId}>
<input name="name" type="text" />
<button type="submit">Update</button>
</form>
);
}

Server Actions with useActionState 🎬

"use client";
import { useActionState } from "react";
import { createPost } from "./actions";
export default function CreatePostForm() {
const [state, formAction, isPending] = useActionState(createPost, null);
return (
<form action={formAction}>
{state?.error && <p>{state.error}</p>}
<input name="title" disabled={isPending} />
<button type="submit" disabled={isPending}>
{isPending ? "Creating..." : "Create"}
</button>
</form>
);
}

Revalidating After Actions πŸ”„

"use server";
import { revalidatePath, revalidateTag } from "next/cache";
export async function updatePost(id: string, data: PostData) {
await db.posts.update(id, data);
// Revalidate specific path
revalidatePath("/blog");
// Or revalidate by tag
revalidateTag("posts", "max");
}

Route Handlers

Basic Route Handler πŸ›£οΈ

app/api/users/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
const users = await db.users.findMany();
return NextResponse.json(users);
}
export async function POST(request: NextRequest) {
const body = await request.json();
const user = await db.users.create(body);
return NextResponse.json(user, { status: 201 });
}

HTTP Methods πŸ“‘

app/api/posts/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
const { id } = await params;
const post = await db.posts.find(id);
return NextResponse.json(post);
}
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
const { id } = await params;
const body = await request.json();
const post = await db.posts.update(id, body);
return NextResponse.json(post);
}
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
const { id } = await params;
await db.posts.delete(id);
return NextResponse.json({ deleted: true });
}

Reading Request Data πŸ“₯

import { NextRequest } from "next/server";
export async function POST(request: NextRequest) {
// Read JSON body
const body = await request.json();
// Read form data
const formData = await request.formData();
// Read headers
const contentType = request.headers.get("content-type");
// Read search params
const searchParams = request.nextUrl.searchParams;
const page = searchParams.get("page");
return NextResponse.json({ success: true });
}

Route Handler with Cache Components πŸ’Ύ

import { cacheTag } from "next/cache";
async function getPosts() {
"use cache";
cacheTag("posts");
const posts = await fetchPosts();
return posts;
}
export async function GET() {
const posts = await getPosts();
return Response.json(posts);
}

Middleware & Proxy

Proxy (Next.js 16) πŸ”€

// proxy.ts (replaces middleware.ts)
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function proxy(request: NextRequest) {
// Modify request
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-pathname", request.nextUrl.pathname);
// Redirect
if (request.nextUrl.pathname === "/old") {
return NextResponse.redirect(new URL("/new", request.url));
}
// Rewrite
if (request.nextUrl.pathname.startsWith("/api")) {
return NextResponse.rewrite(new URL("/api-proxy", request.url));
}
// Add headers to response
const response = NextResponse.next({
request: {
headers: requestHeaders,
},
});
response.headers.set("x-custom-header", "value");
return response;
}

Matcher Configuration 🎯

proxy.ts
export const config = {
matcher: ["/api/:path*", "/((?!_next/static|_next/image|favicon.ico).*)"],
};

βœ… Do’s

  • Use proxy.ts for request interception (Next.js 16+)
  • Keep proxy logic simple and fast
  • Use matcher to limit which routes run proxy
  • Return NextResponse.next() to continue

❌ Don’ts

  • Don’t use middleware.ts (replaced by proxy.ts)
  • Don’t use Edge Runtime APIs in proxy (Node.js runtime only)
  • Don’t block requests unnecessarily

Styling

CSS Modules 🎨

// app/components/Button.module.css
.button {
padding: 0.5rem 1rem;
background: blue;
color: white;
}
// app/components/Button.tsx
import styles from './Button.module.css'
export default function Button() {
return <button className={styles.button}>Click me</button>
}

Tailwind CSS 🎨

Terminal window
# Install Tailwind
pnpm add -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
app/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;
// app/components/Card.tsx
export default function Card() {
return (
<div className="rounded-lg bg-white p-6 shadow-md">
<h2 className="text-xl font-bold">Card Title</h2>
</div>
)
}

Global Styles 🌐

app/layout.tsx
import "./globals.css";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}

CSS-in-JS (Styled Components) πŸ’…

"use client";
import styled from "styled-components";
const Button = styled.button`
padding: 0.5rem 1rem;
background: blue;
color: white;
`;
export default function Component() {
return <Button>Click me</Button>;
}

Configuration

next.config.js βš™οΈ

/** @type {import('next').NextConfig} */
const nextConfig = {
// React Compiler (automatic memoization)
reactCompiler: true,
// Image optimization
images: {
domains: ["example.com"],
remotePatterns: [
{
protocol: "https",
hostname: "**.example.com",
},
],
},
// Environment variables
env: {
CUSTOM_KEY: "value",
},
// Redirects
async redirects() {
return [
{
source: "/old",
destination: "/new",
permanent: true,
},
];
},
// Rewrites
async rewrites() {
return [
{
source: "/api/proxy/:path*",
destination: "https://api.example.com/:path*",
},
];
},
// Headers
async headers() {
return [
{
source: "/api/:path*",
headers: [{ key: "Access-Control-Allow-Origin", value: "*" }],
},
];
},
};
module.exports = nextConfig;

TypeScript Configuration πŸ“˜

tsconfig.json
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

Deployment

Vercel Deployment πŸš€

Terminal window
# Install Vercel CLI
pnpm add -g vercel
# Deploy
vercel
# Production deployment
vercel --prod

Environment Variables πŸ”

Terminal window
# .env.local (development)
DATABASE_URL=postgresql://...
API_KEY=secret-key
# .env.production (production)
DATABASE_URL=postgresql://...
API_KEY=production-key
// Access environment variables
const apiKey = process.env.API_KEY;
const dbUrl = process.env.DATABASE_URL;

Build & Start πŸ—οΈ

Terminal window
# Build for production
pnpm build
# Start production server
pnpm start
# Analyze bundle
pnpm build --analyze

Static Export πŸ“¦

next.config.js
const nextConfig = {
output: "export", // Static export
images: {
unoptimized: true, // Required for static export
},
};
module.exports = nextConfig;

Best Practices

βœ… Do’s

  • Use Server Components by default - Only add 'use client' when needed
  • Fetch data in Server Components - Pass data as props to Client Components
  • Use 'use cache' for static data - Improve performance with Cache Components
  • Await params and searchParams - They’re Promises in Next.js 16
  • Use Suspense for loading states - Better UX than loading.tsx alone
  • Revalidate after mutations - Use revalidatePath or revalidateTag
  • Use TypeScript - Better type safety and developer experience
  • Optimize images - Use next/image component
  • Use Route Handlers for API endpoints - Not Server Actions
  • Keep Client Components small - Minimize JavaScript sent to client

❌ Don’ts

  • Don’t fetch in Client Components - Use Server Components instead
  • Don’t use runtime APIs in cached components - cookies(), headers() won’t work
  • Don’t forget to await params/searchParams - They’re Promises now
  • Don’t use middleware.ts - Use proxy.ts in Next.js 16
  • Don’t make entire app a Client Component - Only mark what needs interactivity
  • Don’t ignore error boundaries - Always handle errors gracefully
  • Don’t skip loading states - Users need feedback during data fetching
  • Don’t forget to revalidate - Cache invalidation is crucial

Common Pitfalls

⚠️ Async Params Not Awaited

// ❌ Wrong - params is a Promise
export default function Page({ params }: { params: { slug: string } }) {
const { slug } = params; // Error!
return <div>{slug}</div>;
}
// βœ… Correct - await the Promise
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return <div>{slug}</div>;
}

⚠️ Using Runtime APIs in Cached Components

// ❌ Wrong - cookies() doesn't work in cached components
"use cache";
export default async function Page() {
const cookies = await cookies(); // Error!
return <div>...</div>;
}
// βœ… Correct - Remove 'use cache' or move logic out
export default async function Page() {
const cookies = await cookies(); // Works!
return <div>...</div>;
}

⚠️ Fetching in Client Components

// ❌ Wrong - Fetching in Client Component
"use client";
export default function Page() {
const [data, setData] = useState(null);
useEffect(() => {
fetch("/api/data")
.then((res) => res.json())
.then(setData);
}, []);
return <div>{data?.title}</div>;
}
// βœ… Correct - Fetch in Server Component
export default async function Page() {
const res = await fetch("/api/data");
const data = await res.json();
return <div>{data.title}</div>;
}

⚠️ Not Revalidating After Mutations

// ❌ Wrong - Cache not invalidated
"use server";
export async function updatePost(id: string, data: PostData) {
await db.posts.update(id, data);
// Cache still shows old data!
}
// βœ… Correct - Revalidate cache
("use server");
import { revalidatePath, revalidateTag } from "next/cache";
export async function updatePost(id: string, data: PostData) {
await db.posts.update(id, data);
revalidatePath("/blog");
revalidateTag("posts", "max");
}

⚠️ Using Middleware Instead of Proxy

middleware.ts
// ❌ Wrong - middleware.ts (deprecated in Next.js 16)
export function middleware(request: NextRequest) {
// ...
}
// βœ… Correct - proxy.ts (Next.js 16)
// proxy.ts
export function proxy(request: NextRequest) {
// ...
}