Loading content…
Loading content…
Master Next.js App Router and modern file-based routing
app/
layout.tsx → Root layout
page.tsx → Home page (/)
error.tsx → Error boundary
not-found.tsx → 404 page
products/
layout.tsx → Product layout
page.tsx → /products
[id]/
page.tsx → /products/:id
loading.tsx → Suspense boundary
// app/products/[id]/page.tsx
interface Props {
params: { id: string };
}
export default function Product({ params }: Props) {
return <h1>Product {params.id}</h1>;
}
// Generate static paths
export async function generateStaticParams() {
const products = await fetch("/api/products");
return products.map((p) => ({
id: p.id.toString()
}));
}
// app/docs/[...slug]/page.tsx
// Matches /docs, /docs/a, /docs/a/b/c, etc.
interface Props {
params: { slug: string[] };
}
export default function Docs({ params }: Props) {
const path = params.slug?.join("/") || "root";
return <h1>Docs: {path}</h1>;
}
// app/posts/page.tsx
import { PostList } from "./post-list";
import { Suspense } from "react";
export default function Posts() {
return (
<div>
<h1>Posts</h1>
<Suspense fallback={<div>Loading...</div>}>
<PostList />
</Suspense>
</div>
);
}
// Async component
async function PostList() {
const posts = await fetch("/api/posts");
return posts.map(p => <article key={p.id}>{p.title}</article>);
}
// app/posts/error.tsx
"use client";
interface Props {
error: Error;
reset: () => void;
}
export default function Error({ error, reset }: Props) {
return (
<div>
<h1>Something went wrong!</h1>
<p>{error.message}</p>
<button onClick={reset}>Try again</button>
</div>
);
}
// app/posts/loading.tsx
export default function Loading() {
return <div>Loading posts...</div>;
}
// Or use Suspense for more control
<Suspense fallback={<Loading />}>
<PostList />
</Suspense>
Senior Developer Wisdom
app/
(marketing)/
page.tsx → /
about/page.tsx → /about
(dashboard)/
dashboard/page.tsx → /dashboard
settings/page.tsx → /settings
// app/(marketing)/layout.tsx
export default function MarketingLayout({ children }) {
return (
<div>
<header>Marketing header</header>
{children}
</div>
);
}
// app/(dashboard)/layout.tsx
export default function DashboardLayout({ children }) {
return (
<div>
<Sidebar />
<main>{children}</main>
</div>
);
}
// middleware.ts
import { NextRequest, NextResponse } from "next/server";
export function middleware(request: NextRequest) {
// Add headers, redirect, etc.
const response = NextResponse.next();
response.headers.set("x-custom-header", "value");
return response;
}
export const config = {
matcher: [
"/((?!api|_next/static|_next/image|favicon.ico).*)"
]
};
// app/api/posts/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
return NextResponse.json({ posts: [] });
}
export async function POST(request: NextRequest) {
const data = await request.json();
return NextResponse.json({ id: 1, ...data }, { status: 201 });
}
// app/api/posts/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
return NextResponse.json({ id: params.id, title: "Post" });
}
Pro Tip
// next.config.ts
export default {
async redirects() {
return [
{
source: "/old-page",
destination: "/new-page",
permanent: true // 301 redirect
}
];
},
async rewrites() {
return [
{
source: "/api/:path*",
destination: "/internal-api/:path*"
}
];
}
};
.env.local for local development..env.example to document required variables.process.env.NAME in server code; use NEXT_PUBLIC_ prefix for client-only values.// .env.local (development)
// .env.production (production)
const apiUrl = process.env.NEXT_PUBLIC_API_URL; // Client-side
const apiKey = process.env.API_KEY; // Server-only
NEXT_PUBLIC_ to expose to client..env Best Practices
Common Pitfall
.env.local files. Always use .env.example as a template. Secrets should never be in client-side code.App Router Mastery
[slug] patternMarking it complete updates your roadmap progress percentage.