Next.js App Router: A Practical Guide for Real Projects
Server components, layouts, loading states and caching explained with the decisions that matter when you ship a real Next.js site.
TypeScript pays for itself when you use it to describe your data, not just to silence the editor.
type ButtonProps = {
label: string;
variant?: 'primary' | 'ghost';
onClick: () => void;
};
A union of string literals is better than string: typos become compile errors and the editor autocompletes the options.
Do not annotate what TypeScript already knows. const [count, setCount] = useState(0) is a number without help. Add a generic only when the initial value is not enough: useState<User | null>(null).
Model loading states so impossible combinations cannot exist.
type State =
| { status: 'loading' }
| { status: 'error'; message: string }
| { status: 'ready'; data: User[] };
Inside a switch on status, TypeScript knows exactly which fields exist.
Partial<T> for update payloads.Pick<T, K> and Omit<T, K> to reuse a shape.Record<K, V> for lookup tables.ReturnType<typeof fn> to avoid duplicating types.Use React.ChangeEvent<HTMLInputElement> for inputs and useRef<HTMLDivElement>(null) for DOM refs.
TypeScript types disappear at runtime. A response from an API is untrusted, so parse it with a schema library such as Zod and infer the type from the schema. Then your types and your validation can never drift apart.
anyPrefer unknown and narrow it. any quietly turns off checking for everything it touches.
Describe data precisely, model states with unions, and validate at the edges. The compiler then catches bugs before users do.
Server components, layouts, loading states and caching explained with the decisions that matter when you ship a real Next.js site.
Module boundaries, DTO validation, guards, exception filters and configuration: the NestJS structure that stays clean as your API grows.
Stop guessing. Measure first, then apply the React optimizations that move the needle: memoization, code splitting, list rendering and more.