JavaScript

Next.js 16 App Router: A Practical Guide for 2026

Master Next.js 16 App Router — server components, data fetching, route handlers, middleware, layouts, loading states, and deployment best practices.

March 15, 20263 min read

Server Components vs Client Components

The most important concept in Next.js 16: components are server-rendered by default.

Server Components (default):
  • Run on the server, send only HTML to the client
  • Can directly access databases, file systems, environment variables
  • Cannot use hooks (useState, useEffect) or browser APIs
  • Zero JavaScript sent to the client = faster page loads

Client Components (opt-in with "use client"):
  • Run in the browser
  • Can use hooks, state, event handlers, browser APIs
  • JavaScript is sent to the client

// Server Component (default) — no directive needed

async function UserList() {

const users = await db.query('SELECT * FROM users'); // Direct DB access!

return (

    {users.map(u =>

    • {u.name}
    )}

);

}

// Client Component — needs "use client" directive

"use client";

import { useState } from 'react';

function Counter() {

const [count, setCount] = useState(0);

return ;

}

Rule of thumb: Keep components as server components unless they need interactivity. Use client components only for forms, buttons, dropdowns, modals — things that respond to user input.

Data Fetching: Server Components vs Route Handlers

In the App Router, you fetch data directly in server components — no getServerSideProps or getStaticProps:

// app/blog/page.tsx — Server Component

export default async function BlogPage() {

const posts = await fetch('https://api.example.com/posts', {

next: { revalidate: 60 }, // ISR: regenerate every 60 seconds

}).then(r => r.json());

return (

{posts.map(post => (

{post.title}

{post.excerpt}

))}

);

}

Caching strategies:
  • { cache: 'force-cache' } — static, never refetches (like getStaticProps)
  • { cache: 'no-store' } — dynamic, refetches every request (like getServerSideProps)
  • { next: { revalidate: 60 } } — ISR, regenerate after 60 seconds

Layouts and Loading States

Layouts persist across navigations — perfect for navbars, sidebars, and shared UI:

// app/layout.tsx — Root layout (wraps everything)

export default function RootLayout({ children }) {

return (

{children}

);

}

// app/dashboard/layout.tsx — Nested layout

export default function DashboardLayout({ children }) {

return (

{children}

);

}

Add instant loading states with loading.tsx:

// app/dashboard/loading.tsx

export default function Loading() {

return

Loading dashboard...
;

}

Next.js automatically wraps the page in a Suspense boundary and shows loading.tsx while the page's async data fetches complete.

Route Handlers (API Routes)

Create API endpoints with route handlers:

// app/api/users/route.ts

import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {

const searchParams = request.nextUrl.searchParams;

const page = parseInt(searchParams.get('page') || '1');

const users = await db.users.findMany({

skip: (page - 1) * 20,

take: 20,

});

return NextResponse.json({ users, page });

}

export async function POST(request: NextRequest) {

const body = await request.json();

if (!body.email) {

return NextResponse.json(

{ error: 'Email required' },

{ status: 400 }

);

}

const user = await db.users.create({ data: body });

return NextResponse.json(user, { status: 201 });

}

Route handlers support all HTTP methods: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS.

Dynamic Routes and Metadata

Dynamic routes use folder names with brackets:

app/

blog/

[slug]/

page.tsx # /blog/my-post

page.tsx # /blog

tools/

[...slug]/

page.tsx # /tools/pdf/convert (catch-all)

Generate SEO metadata dynamically:

// app/blog/[slug]/page.tsx

export async function generateMetadata({ params }) {

const { slug } = await params;

const post = await getPost(slug);

return {

title: post.title,

description: post.description,

openGraph: {

title: post.title,

description: post.description,

type: 'article',

publishedTime: post.date,

},

};

}

export default async function BlogPost({ params }) {

const { slug } = await params;

const post = await getPost(slug);

if (!post) notFound();

return

{/* render post */}
;

}

Note: In Next.js 16, params is a Promise and must be awaited.

next.jsreactapp routerserver componentstypescriptweb development

Related Articles