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
"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.