Core concepts - minimal brutalist documentation
File-system based routing with layouts and nested routes.
app/
├── layout.tsx # Root layout
├── page.tsx # Home page (/)
├── about/page.tsx # /about
└── blog/
├── layout.tsx # Blog layout
└── [slug]/page.tsx # /blog/:slug
Default component type. Run on server, zero client JS.
// app/page.tsx (Server Component)
export default async function Page() {
const data = await fetch('https://api.example.com/data');
return <div>{data.title}</div>;
}
Interactive components with hooks. Add 'use client' directive.
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
Server-side mutations called from client.
// app/actions.ts
'use server';
export async function createPost(formData: FormData) {
const title = formData.get('title');
await db.posts.create({ title });
revalidatePath('/posts');
}
// app/page.tsx
import { createPost } from './actions';
export default function Page() {
return (
<form action={createPost}>
<input name="title" />
<button type="submit">Create</button>
</form>
);
}
File names with brackets create dynamic segments.
// app/blog/[slug]/page.tsx
export default function Post({ params }: { params: { slug: string } }) {
return <div>Post: {params.slug}</div>;
}
Shared UI that doesn't re-render on navigation.
// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<nav>Navigation</nav>
{children}
</body>
</html>
);
}
Automatic loading UI with loading.tsx.
// app/loading.tsx
export default function Loading() {
return <div>Loading...</div>;
}
Catch errors with error.tsx.
// app/error.tsx
'use client';
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
return (
<div>
<h2>Something went wrong</h2>
<button onClick={reset}>Try again</button>
</div>
);
}
SEO and social sharing metadata.
// app/page.tsx
export const metadata = {
title: 'My Page',
description: 'Page description',
};
Server Components can fetch directly.
export default async function Page() {
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 } // Cache for 1 hour
});
return <div>{data.title}</div>;
}
Send UI progressively with <Suspense>.
import { Suspense } from 'react';
export default function Page() {
return (
<Suspense fallback={<Loading />}>
<SlowComponent />
</Suspense>
);
}
API routes in App Router.
// app/api/posts/route.ts
export async function GET() {
const posts = await db.posts.findMany();
return Response.json(posts);
}
export async function POST(request: Request) {
const body = await request.json();
const post = await db.posts.create(body);
return Response.json(post);
}
Run code before request completes.
// middleware.ts
import { NextResponse } from 'next/server';
export function middleware(request: Request) {
const token = request.cookies.get('token');
if (!token) {
return NextResponse.redirect('/login');
}
}
export const config = {
matcher: '/dashboard/:path*',
};
Automatic image optimization with <Image>.
import Image from 'next/image';
<Image
src="/photo.jpg"
alt="Photo"
width={500}
height={300}
priority
/>
Client-side navigation with automatic prefetching.
import Link from 'next/link';
<Link href="/about" prefetch={false}>About</Link>
'use client' for interactivityProtected routes:
// middleware.ts
export function middleware(request: Request) {
const isAuthenticated = request.cookies.get('auth');
if (!isAuthenticated) return NextResponse.redirect('/login');
}
Data revalidation:
import { revalidatePath, revalidateTag } from 'next/cache';
await revalidatePath('/posts');
await revalidateTag('posts');
Parallel routes:
app/
├── @modal/
│ └── page.tsx
└── layout.tsx # Receives modal as prop