Skip to content
2 min read

Next.js 16 in practice: the changes that actually bit me

Next.js 16 upgrade notes: async params everywhere, typed PageProps, middleware renamed to proxy, a Tailwind 4 gotcha, and the docs inside node_modules.

On this page
  1. params is a Promise
  2. Use the global PageProps and LayoutProps
  3. middleware.ts is now proxy.ts
  4. The docs ship with the package
  5. Static by default, on purpose
  6. Tailwind 4 gotcha: @apply your own classes
  7. Verdict

I've moved several side projects — and this site — to Next.js 16 with React 19 and Tailwind CSS 4. Most of the upgrade is smooth. These are the things that cost me time.

params is a Promise

In pages, layouts, route handlers and generateMetadata, params and searchParams are Promises. Forget the await and you get a confusing runtime error, not a type error, if you typed the props loosely.

tsx
export default async function Page({ params }: PageProps<"/blog/[slug]">) {
  const { slug } = await params;
  // ...
}

Use the global PageProps and LayoutProps

Next generates route-aware types during next dev, next build or next typegen. PageProps<"/blog/[slug]"> gives strict keys for params — no hand-written interfaces, and a typo in the route literal is a compile error. LayoutProps<"/"> does the same for layouts.

middleware.ts is now proxy.ts

The middleware file convention is deprecated and renamed to proxy. Same idea — run code before a request is rendered for redirects, rewrites and headers — but the name now says what it is for. Don't build auth that depends on shared modules or globals there; it may run separately from your render code.

The docs ship with the package

node_modules/next/dist/docs/ contains the documentation for the exact version you installed. When blog posts and AI suggestions disagree, that folder wins. I grep it before touching any API I haven't used in this version.

Static by default, on purpose

This blog has no database. Posts are Markdown files read at build time:

ts
export const dynamicParams = false;

export function generateStaticParams() {
  return getAllPosts().map((p) => ({ slug: p.slug }));
}

With dynamicParams = false, any slug not generated at build time is a clean 404 — no accidental server rendering of junk URLs. The RSS feed is a route handler marked force-static.

Tailwind 4 gotcha: @apply your own classes

In Tailwind 4 you can't @apply a plain CSS class you defined yourself. Declare it with @utility instead and it composes like any built-in:

css
@utility btn { @apply inline-flex items-center rounded-full px-5 py-2.5; }
@utility btn-primary { @apply btn bg-fg text-bg; }

Verdict

Next 16 rewards you for being explicit: typed routes, static generation you opt out of, and file names that describe behaviour. Read the bundled docs first and the upgrade is mostly mechanical.

If the upgrade is also a chance to look at speed, the levers that matter are in Next.js performance.

Found this useful?Share on XShare on LinkedIn

Related reading