Next.js basics cheatsheet (App Router, Next 15+).
Bootstrap
npx create-next-app@latest myapp --typescript --tailwind --eslint --app
cd myapp
npm run dev
Structure
app/
├── layout.tsx # root layout (required)
├── page.tsx # /
├── about/
│ └── page.tsx # /about
├── posts/
│ ├── layout.tsx # layout for /posts/*
│ ├── page.tsx # /posts
│ └── [id]/
│ └── page.tsx # /posts/:id
└── api/
└── hello/
└── route.ts # /api/hello
Root layout
// app/layout.tsx
export const metadata = { title: "My App" };
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
Page (server component by default)
// app/page.tsx
export default function Home() {
return <h1>Welcome</h1>;
}
// app/posts/[id]/page.tsx
async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params; // Next 15+: params is async
const post = await fetchPost(id);
return <h1>{post.title}</h1>;
}
export default Page;
Nested layouts
// app/posts/layout.tsx
export default function PostsLayout({ children }: { children: React.ReactNode }) {
return (
<section>
<nav>Posts nav</nav>
{children}
</section>
);
}
Layouts wrap all page.tsx in their subtree. They don’t re-render on route changes within their subtree.
Link
import Link from "next/link";
<Link href="/about">About</Link>
<Link href={`/posts/${id}`}>Post {id}</Link>
<Link href="/x" prefetch={false}>X</Link>
<Link href="/y" replace>Y</Link>
Prefetches the route on hover. Client-side navigation.
useRouter (client)
"use client";
import { useRouter } from "next/navigation";
function Comp() {
const router = useRouter();
return <button onClick={() => router.push("/x")}>Go</button>;
}
Other methods: replace, back, forward, refresh.
useParams / useSearchParams / usePathname
"use client";
import { useParams, useSearchParams, usePathname } from "next/navigation";
const { id } = useParams();
const params = useSearchParams();
const path = usePathname();
const q = params.get("q");
redirect / notFound
import { redirect, notFound } from "next/navigation";
async function Page({ params }) {
const post = await db.post.find(params.id);
if (!post) notFound();
if (!user) redirect("/login");
return <Article post={post} />;
}
loading.tsx (auto Suspense)
// app/posts/loading.tsx
export default function Loading() {
return <p>Loading…</p>;
}
Wraps the route segment in <Suspense fallback={<Loading />}> automatically.
error.tsx
// app/posts/error.tsx
"use client";
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
return (
<div>
<p>{error.message}</p>
<button onClick={reset}>Try again</button>
</div>
);
}
not-found.tsx
// app/not-found.tsx
export default function NotFound() {
return <h1>404</h1>;
}
Metadata
// app/page.tsx
export const metadata = {
title: "Home",
description: "...",
openGraph: { title: "Home", images: ["/og.png"] },
};
// Or generateMetadata for dynamic
export async function generateMetadata({ params }) {
const post = await fetchPost(params.id);
return { title: post.title };
}
API route (Route Handlers)
// app/api/hello/route.ts
import { NextResponse } from "next/server";
export async function GET(req: Request) {
return NextResponse.json({ ok: true });
}
export async function POST(req: Request) {
const body = await req.json();
return NextResponse.json({ received: body });
}
HTTP methods exported as named functions.
Server vs client component
By default: server. Mark client with "use client" at the top.
"use client";
import { useState } from "react";
export function Counter() {
const [n, setN] = useState(0);
return <button onClick={() => setN(n + 1)}>{n}</button>;
}
CSS / Tailwind
import "./globals.css";
<div className="px-4 py-2 bg-gray-100">...</div>
CSS modules:
import styles from "./Foo.module.css";
<div className={styles.box}>...</div>
Image
import Image from "next/image";
<Image src="/x.jpg" width={400} height={300} alt="..." priority />
Optimization, lazy loading, srcSet built-in.
Font
import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin"] });
<html className={inter.className}>...</html>
Environment variables
# .env.local
DATABASE_URL=postgres://...
NEXT_PUBLIC_API_URL=https://api.example.com
process.env.DATABASE_URL; // server only
process.env.NEXT_PUBLIC_API_URL; // available in browser
Variables not prefixed with NEXT_PUBLIC_ are server-only.
Common mistakes
- Using
useStatewithout"use client"directive. - Reading
paramssynchronously in Next 15+ (it’s a Promise now). - Putting
"use client"at root → entire app is client. - Forgetting
await fetchin server component. - Mixing Pages Router and App Router conventions.
Read this next
If you want my Next.js starter, it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .