Back to articlesDevelopment
Introduction to Next.js 16
What actually changes when you build with Next.js 16 and React 19: async params, Server Components, Server Actions and Turbopack.
#nextjs#react#frontend
Mike Codeur
August 03, 20265 min
Next.js 16 runs on React 19, and the App Router is no longer a novelty to get used to: it is simply how you write an application. Rather than a changelog, here is what changes when you write code day to day, with examples pulled from a real production app.
No
This is the change that surprises people the most when migrating, and the one you hit on your very first dynamic page. Route parameters are no longer an object but a promise:
The rule applies everywhere those props show up:
It is called straight from a form, with no network layer to write:
On a growing project, this is the kind of detail that changes how you work: you restart without thinking about it.
The rule that holds it together: never skip a layer. A page importing a SQL query directly is a shortcut that gets expensive six months later.
The examples in this article come from a project running Next.js 16.2 and
React 19.2, in strict TypeScript.
Server Components are the default
A component is a Server Component until you write'use client'. It runs on the server, never ships to the browser, and can do async work directly:
export default async function CoursesPage() {
const courses = await getCoursesDal()
return (
<ul>
{courses.map((course) => (
<li key={course.id}>{course.title}</li>
))}
</ul>
)
}useEffect, no useState, no loading state to hand-roll: the data is there at render time.
The right discipline: keep
'use client' for the leaves of the tree — a form,
a menu, an interactive button. Anything that does not need interactivity stays
on the server and weighs nothing in the bundle.params and searchParams are async
This is the change that surprises people the most when migrating, and the one you hit on your very first dynamic page. Route parameters are no longer an object but a promise:
interface PageProps {
params: Promise<{slug: string}>
}
export default async function Page({params}: PageProps) {
const {slug} = await params
return <article>{slug}</article>
}page.tsx, layout.tsx, generateMetadata, generateStaticParams.
In strict TypeScript, a forgotten
await shows up immediately. In JavaScript
you get a silent undefined — type your props, it is the best safety net in
this migration.Server Actions replace most API routes
For a mutation, you no longer need an API route, a serialisedfetch and JSON handling on both sides. A server function is enough:
'use server'
import {revalidatePath} from 'next/cache'
export async function updateProfileAction(formData: FormData) {
const user = await requireActionAuth()
const parsed = profileSchema.safeParse({
name: formData.get('name'),
email: formData.get('email'),
})
if (!parsed.success) {
return {success: false, message: 'Invalid data'}
}
await updateUserService({id: user.id, ...parsed.data})
revalidatePath('/account')
return {success: true, message: 'Profile updated'}
}'use client'
export function ProfileForm() {
return (
<form action={updateProfileAction}>
<input name="name" />
<button type="submit">Save</button>
</form>
)
}A Server Action is a public entry point, exactly like an API route. It
must check authentication and validate its input on every call. The fact that
it is invoked from a form in your own app proves nothing about the actual
caller.
Turbopack in development
The dev server runs on Turbopack. In practice, startup is measured in hundreds of milliseconds and refresh keeps up with your typing:pnpm dev▲ Next.js 16.2.10 (Turbopack)
- Local: http://localhost:3000
✓ Ready in 233msWhere does what go
The most common question when starting with the App Router is not technical, it is about placement. A simple guide:| Need | Where to write it |
|---|---|
| Read data for display | Server Component, direct await |
| Mutate data | Server Action ('use server') |
| Local state, events, animations | Client Component ('use client') |
| Machine-facing response (webhook, API) | Route Handler (route.ts) |
| Protect a page | Both the layout and the page |
A layout does not re-render on every client-side navigation. Protecting only
the layout leaves a gap: double up the check at the page level.
A structure that lasts
The App Router does not impose an architecture, which is as much a freedom as a trap: nothing stops you from querying the database straight from a component. On a project meant to last, it pays to set a single direction of travel:1
Presentation
Pages and components. They display; they do not know where data comes from.
2
Data access
A dedicated, cached layer that checks permissions and returns display-ready
objects — never raw database rows.
3
Domain
Validation, rules, authorisation. Decisions live here, not in the component.
4
Persistence
The queries. They know nothing about React or HTTP.
Takeaway
Next.js 16 does not ask you to relearn React: it moves the default towards the server. The three habits that make the difference:- Write server-first, and go client only for what genuinely needs interactivity
awaityourparamsandsearchParams, everywhere- Treat every Server Action as a public entry point: authenticate and validate, every time
Written by
MC
Mike Codeur
Author
Related Articles
Design
Tailwind CSS v4: configuration moves into your CSS
No more tailwind.config.js. v4 moves the whole configuration into your stylesheet, with @theme, @plugin and @custom-variant.
Aug 02, 20264 min
Read more
Tutorial
The Proxy Component in React
A simple pattern to shield your codebase from change: centralise an implementation instead of scattering it across every file.
Aug 01, 20265 min
Read more