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.

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 useState without "use client" directive.
  • Reading params synchronously in Next 15+ (it’s a Promise now).
  • Putting "use client" at root → entire app is client.
  • Forgetting await fetch in 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 .