Next.js Cheatsheet
Pages and Layouts
Use this Next.js reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.
page.tsx — Route UI
The default export from page.tsx renders for the route URL.
// app/about/page.tsx → /about export default function AboutPage() { return <main>About us</main> }
Async page (data fetching):
// app/blog/[slug]/page.tsx export default async function BlogPost({ params, searchParams, }: { params: Promise<{ slug: string }> searchParams: Promise<{ [key: string]: string | string[] | undefined }> }) { const { slug } = await params const { page } = await searchParams const post = await fetchPost(slug) return <article>{post.body}</article> }
In Next.js 15+, both
paramsandsearchParamsare Promises.
layout.tsx — Persistent Wrapper
Layouts wrap child routes and do not re-mount on navigation — state is preserved.
// app/layout.tsx (Root layout — required, must include <html> and <body>) import type { Metadata } from 'next' export const metadata: Metadata = { title: 'My App', description: 'My Next.js app', } export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( <html lang="en"> <body>{children}</body> </html> ) }
Nested layout (no <html> / <body>):
// app/dashboard/layout.tsx → wraps all /dashboard/* routes export default function DashboardLayout({ children, }: { children: React.ReactNode }) { return ( <div className="flex"> <Sidebar /> <main className="flex-1">{children}</main> </div> ) }
Layout Nesting Chain
app/layout.tsx ← root (wraps everything) app/dashboard/ layout.tsx ← dashboard layout (wraps /dashboard/*) app/dashboard/ settings/ layout.tsx ← settings layout (wraps /dashboard/settings/*) page.tsx ← actual page content
template.tsx — Re-mounting Wrapper
Same as layout.tsx but creates a new instance on each navigation (state resets, effects re-run). Useful for animations or analytics page-view tracking.
// app/dashboard/template.tsx export default function DashboardTemplate({ children, }: { children: React.ReactNode }) { return <div className="animate-fade-in">{children}</div> }
loading.tsx — Streaming Suspense UI
Automatically wraps the page in <Suspense>. Shown while the page component or its data loads.
// app/dashboard/loading.tsx export default function Loading() { return <div className="spinner" aria-label="Loading…" /> }
Next.js streams the layout + loading UI immediately; the page replaces it when ready.
error.tsx — Error Boundary
Must be a Client Component. Catches errors thrown during rendering or data fetching in the segment.
// app/dashboard/error.tsx 'use client' import { useEffect } from 'react' export default function Error({ error, reset, }: { error: Error & { digest?: string } reset: () => void }) { useEffect(() => { console.error(error) }, [error]) return ( <div> <h2>Something went wrong</h2> <button onClick={reset}>Try again</button> </div> ) }
reset()re-renders the segment.error.digestis a hashed server error ID safe to log/display.
not-found.tsx — 404 UI
Rendered when notFound() is called in the same segment (or nested).
// app/not-found.tsx ← global 404 export default function NotFound() { return ( <section> <h2>Page not found</h2> <a href="/">Go home</a> </section> ) }
Scoped 404:
// app/blog/[slug]/not-found.tsx export default function PostNotFound() { return <p>This post does not exist.</p> }
Triggering it:
import { notFound } from 'next/navigation' const post = await db.post.findUnique({ where: { slug } }) if (!post) notFound()
global-error.tsx — Root Error Boundary
Catches errors in the root layout.tsx. Must include its own <html> and <body>.
// app/global-error.tsx 'use client' export default function GlobalError({ error, reset, }: { error: Error & { digest?: string } reset: () => void }) { return ( <html> <body> <h2>Something went catastrophically wrong</h2> <button onClick={reset}>Try again</button> </body> </html> ) }
Route Group Layouts
Use (group) folders to apply different layouts to disjoint URL groups.
app/ ├── (auth)/ │ ├── layout.tsx ← minimal layout for sign-in/sign-up │ ├── login/page.tsx → /login │ └── signup/page.tsx → /signup └── (main)/ ├── layout.tsx ← full app shell layout └── dashboard/ └── page.tsx → /dashboard
default.tsx — Parallel Route Fallback
Used with parallel routes (@slot). Renders when a slot has no matching route during soft navigation.
// app/dashboard/@analytics/default.tsx export default function AnalyticsDefault() { return null // render nothing if no analytics match }
Metadata in Pages and Layouts
Static metadata export (works in page.tsx and layout.tsx):
import type { Metadata } from 'next' export const metadata: Metadata = { title: 'Dashboard', description: 'Your personal dashboard', }
Dynamic metadata:
export async function generateMetadata({ params, }: { params: Promise<{ slug: string }> }): Promise<Metadata> { const { slug } = await params const post = await fetchPost(slug) return { title: post.title, description: post.excerpt } }
TypeScript Helpers (PageProps, LayoutProps)
Next 15.5+ generates globally available route-typed helpers (no import needed) — pass the route literal and params/searchParams are typed for you:
// app/blog/[slug]/page.tsx export default async function Page(props: PageProps<'/blog/[slug]'>) { const { slug } = await props.params // typed as string const { page } = await props.searchParams return <div>{slug}</div> }
// app/dashboard/layout.tsx export default function Layout(props: LayoutProps<'/dashboard'>) { return <section>{props.children}</section> // parallel route slots are typed too }
Types are generated by next dev / next build, or on demand with npx next typegen. Manual typing still works if you prefer:
type Props = { params: Promise<{ slug: string }> } export default async function Page({ params }: Props) { const { slug } = await params return <div>{slug}</div> }