Getting Started with Next.js 15: App Router, Server Components, and What Changed
Next.js 15 introduced breaking changes to how async params work in dynamic routes and shifted how we think about caching. Here is a practical guide based on real production experience.
Getting Started with Next.js 15: App Router, Server Components, and What Changed
Next.js 15 is a significant release that builds on the App Router introduced in Next.js 13. If you are upgrading from Next.js 14 or starting fresh, there are several things you need to understand to avoid the gotchas that caught me out when building Work Log Pro.
The App Router Mental Model
The App Router represents a fundamental shift in how Next.js works. The old Pages Router (pages/ directory) treated every file as a client-side page with optional server-side data fetching. The App Router (app/ directory) treats every component as a Server Component by default.
This matters because:
- Server Components run on the server, have direct database access, and never ship JavaScript to the client
- Client Components (marked with "use client") run in the browser and can use React hooks, event listeners, and browser APIs
- The default is server — you opt into client rendering explicitly
This is the opposite of what most React developers are used to. The mental shift takes time.
Async Params in Dynamic Routes
This is the change that broke my code when upgrading to Next.js 15.
In Next.js 14, dynamic route parameters were synchronous:
// Next.js 14 - worked fine
export async function GET(
req: NextRequest,
{ params }: { params: { id: string } }
) {
const { id } = params; // synchronous access
}In Next.js 15, params is a Promise:
// Next.js 15 - required change
export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params; // must await
}If you upgrade without making this change, TypeScript will tell you the second argument type is invalid, and the build will fail. I hit this exact error in my admin token management API routes.
The fix is mechanical — add Promise<> to the type annotation and await the params — but you need to do it on every dynamic route handler.