Fundamentals
Next.js is a tool built on top of React that helps you build complete websites - both what users see (frontend) and the behind-the-scenes logic (backend). It handles a lot of the hard stuff for you, so you can focus on building your app.
Setup and Installation
Section titled “Setup and Installation”To create a new Next.js project, run this command in your terminal:
Docs: https://nextjs.org/docs/app/getting-started/installation
bun create next-app@latest my-next-appThis creates a new folder called my-next-app with everything you need. Now start the app:
cd my-next-appbun devOpen your browser and go to http://localhost:3000 - you should see your app running!
Folder Structure
Section titled “Folder Structure”When you create a Next.js project, it sets up folders for you. Here’s what each one is for:
my-next-app/├── app/│ ├── (marketing)/ # Public pages like Home, Pricing, Features│ │ ├── page.tsx│ │ ├── pricing/│ │ └── features/│ ││ ├── (auth)/ # Login and signup pages│ │ ├── sign-in/│ │ ├── sign-up/│ │ ├── forgot-password/│ │ └── reset-password/│ ││ ├── (dashboard)/ # Pages only logged-in users can see│ │ ├── dashboard/│ │ ├── settings/│ │ ├── profile/│ │ └── billing/│ ││ ├── api/ # Your backend - handles data and logic│ │ ├── auth/│ │ ├── users/│ │ ├── posts/│ │ └── webhooks/│ ││ ├── favicon.ico│ ├── globals.css│ ├── layout.tsx│ ├── loading.tsx│ ├── error.tsx│ ├── not-found.tsx│ └── page.tsx│├── components/│ ├── ui/ # Small reusable pieces like buttons and inputs│ │ ├── button.tsx│ │ ├── input.tsx│ │ ├── modal.tsx│ │ └── table.tsx│ ││ ├── forms/ # Forms like login or profile update│ │ ├── login-form.tsx│ │ └── profile-form.tsx│ ││ ├── layouts/ # Things that appear on every page (navbar, footer)│ │ ├── navbar.tsx│ │ ├── sidebar.tsx│ │ └── footer.tsx│ ││ └── shared/ # Components used across the whole app│ ├── logo.tsx│ ├── avatar.tsx│ └── loading-spinner.tsx│├── actions/ # Functions that run on the server when a form is submitted│ ├── auth.ts│ ├── user.ts│ └── post.ts│├── lib/│ ├── db.ts # Connects your app to the database│ ├── auth.ts # Handles login/logout logic│ ├── permissions.ts # Controls who can see or do what│ ├── validations.ts # Checks that form inputs are correct│ ├── constants.ts│ ├── env.ts│ ├── logger.ts│ └── utils.ts│├── schemas/ # Rules for what your data should look like│ ├── auth.ts│ ├── user.ts│ └── post.ts│├── services/ # The main logic of your app (e.g. "create a post")│ ├── auth.service.ts│ ├── user.service.ts│ ├── post.service.ts│ └── email.service.ts│├── repositories/ # Talks directly to the database│ ├── user.repository.ts│ ├── post.repository.ts│ └── subscription.repository.ts│├── hooks/ # Custom helpers for your React components│ ├── use-auth.ts│ ├── use-debounce.ts│ └── use-local-storage.ts│├── providers/ # Wraps your app to share data (like login state)│ ├── auth-provider.tsx│ ├── theme-provider.tsx│ └── query-provider.tsx│├── store/ # Stores data that multiple pages need to share│ ├── auth-store.ts│ ├── user-store.ts│ └── ui-store.ts│├── types/│ ├── auth.ts│ ├── user.ts│ ├── api.ts│ └── index.ts│├── emails/ # Email templates sent to users│ ├── welcome-email.tsx│ ├── reset-password.tsx│ └── verification-email.tsx│├── prisma/ # Database setup and migrations│ ├── schema.prisma│ ├── migrations/│ └── seed.ts│├── public/│ ├── images/│ ├── icons/│ ├── logos/│ └── uploads/│├── tests/│ ├── unit/│ ├── integration/│ └── e2e/│├── docs/ # Notes and documentation for your project│├── scripts/ # One-off scripts like adding test data│ ├── seed.ts│ └── cleanup.ts│├── middleware.ts├── next.config.ts├── instrumentation.ts├── package.json├── tsconfig.json├── eslint.config.js├── prettier.config.js├── .env├── .env.local├── .env.example└── README.mdLayouts
Section titled “Layouts”A layout is a wrapper that stays the same across multiple pages. Think of it like a picture frame - the frame (navbar, footer) stays the same while the picture (page content) changes.
app/├── layout.tsx ← the frame (navbar + footer)└── dashboard/ └── page.tsx ← the content inside the frameCommon things you put in a layout:
- Navbar (top bar with links)
- Sidebar (left panel)
- Footer (bottom of the page)
Here’s a simple layout:
// app/layout.tsximport { ReactNode } from "react";
export default function Layout({ children }: { children: ReactNode }) { return ( <div> <nav style={{ background: "#333", color: "white", padding: "1rem" }}> My App </nav>
<main style={{ padding: "2rem" }}> {children} {/* This is where each page loads */} </main>
<footer style={{ background: "#f0f0f0", padding: "1rem", textAlign: "center" }}> © 2025 My App </footer> </div> );}Every page inside app/ will automatically get the navbar and footer - you don’t have to repeat them.
Routing and Navigation
Section titled “Routing and Navigation”In most frameworks, you have to manually set up routes. In Next.js, the folder structure IS your routes. Create a folder, add a page.tsx file - that’s your new page.
Basic Routes
Section titled “Basic Routes”app/├── page.tsx -> /├── about/│ └── page.tsx -> /about└── contact/ └── page.tsx -> /contactThat’s it. No extra setup needed.
Dynamic Routes
Section titled “Dynamic Routes”Sometimes you want one page to work for many different items - like a blog post page that works for every post. Use square brackets [] for that.
app/└── blog/ └── [slug]/ └── page.tsxNow all of these just work:
/blog/my-first-post/blog/how-to-learn-nextjs/blog/top-10-tipsHere’s how you use the slug inside your page:
// app/blog/[slug]/page.tsx
type Props = { params: { slug: string };};
export default function BlogPost({ params }: Props) { return ( <div> <h1>You are reading: {params.slug}</h1> <p>Blog post content goes here.</p> </div> );}Route Groups
Section titled “Route Groups”You can group pages together in folders to keep things organized - without changing the URL. Just wrap the folder name in ().
app/├── (marketing)/│ └── about/│ └── page.tsx -> /about│└── (auth)/ └── sign-in/ └── page.tsx -> /sign-inThe (marketing) and (auth) parts don’t show up in the URL - they’re just for you to stay organized.
Navigation
Section titled “Navigation”Use the Link component to move between pages. It’s faster than a regular <a> tag because it doesn’t reload the whole page.
// app/page.tsximport Link from "next/link";
export default function Home() { return ( <div> <h1>Welcome to My App</h1> <nav> <Link href="/about">About Us</Link> {" | "} <Link href="/contact">Contact</Link> {" | "} <Link href="/blog/my-first-post">Read Blog</Link> </nav> </div> );}Server and Client Components
Section titled “Server and Client Components”This is one of the most important ideas in Next.js. Every component is either a Server Component or a Client Component.
Here’s the simple way to think about it:
- Server Component -> runs on the server, sends ready-made HTML to the browser. Great for loading data.
- Client Component -> runs in the user’s browser. Needed when something needs to respond to clicks or typing.
Server Components
Section titled “Server Components”All components in the app folder are Server Components by default. You don’t need to add anything special.
Use them when you need to load data (like posts from a database) and display it:
// app/posts/page.tsx// No special keyword needed - this runs on the server
type Post = { id: number; title: string;};
export default async function PostsPage() { // This data is fetched on the server before the page is sent to the user const res = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=5"); const posts: Post[] = await res.json();
return ( <div> <h1>Latest Posts</h1> <ul> {posts.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> </div> );}The user gets the full list of posts already in the HTML - fast and good for search engines.
Client Components
Section titled “Client Components”Add "use client" at the top when your component needs to react to user actions - like clicking a button or filling out a form.
// components/ui/like-button.tsx"use client";
import { useState } from "react";
export default function LikeButton() { const [liked, setLiked] = useState(false); const [count, setCount] = useState(42);
function handleClick() { setLiked(!liked); setCount(liked ? count - 1 : count + 1); }
return ( <button onClick={handleClick} style={{ color: liked ? "red" : "gray" }} > {liked ? "❤️" : "🤍"} {count} Likes </button> );}When you click the button, it updates instantly - no page reload needed.
When to Use Which?
Section titled “When to Use Which?”| What you’re doing | Use this |
|---|---|
| Loading posts from a database | Server Component |
| Showing a list of products | Server Component |
| A button that does something when clicked | Client Component |
| A search bar that filters as you type | Client Component |
| A popup or dropdown menu | Client Component |
| A page that just shows text or images | Server Component |
A good pattern is to use Server Components to load data, then pass it down to small Client Components that handle interactions.
Data Fetching
Section titled “Data Fetching”There are two main places where you can load data in Next.js - on the server (before the page loads) or in the browser (after the page loads).
Fetching Data on the Server
Section titled “Fetching Data on the Server”This is the most common and recommended way. You fetch the data inside a Server Component, and the page arrives at the browser already filled with content.
// app/users/page.tsx
type User = { id: number; name: string; email: string;};
export default async function UsersPage() { const res = await fetch("https://jsonplaceholder.typicode.com/users"); const users: User[] = await res.json();
return ( <div> <h1>Our Team</h1> <ul> {users.map((user) => ( <li key={user.id}> <strong>{user.name}</strong> - {user.email} </li> ))} </ul> </div> );}Why this is good: The page is fast and search engines can read the content right away.
Fetching Data in the Browser
Section titled “Fetching Data in the Browser”Sometimes you need to load data after the user does something - like searching or clicking a button. For that, you fetch data inside a Client Component.
// components/search-results.tsx"use client";
import { useState } from "react";
type Post = { id: number; title: string;};
export default function SearchResults() { const [query, setQuery] = useState(""); const [results, setResults] = useState<Post[]>([]); const [loading, setLoading] = useState(false);
async function handleSearch() { setLoading(true); const res = await fetch( `https://jsonplaceholder.typicode.com/posts?title_like=${query}` ); const data: Post[] = await res.json(); setResults(data.slice(0, 5)); setLoading(false); }
return ( <div> <input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search posts..." /> <button onClick={handleSearch}>Search</button>
{loading && <p>Loading...</p>}
<ul> {results.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> </div> );}Caching
Section titled “Caching”Caching means saving a copy of the data so Next.js doesn’t have to fetch it again every single time. It’s like keeping leftovers in the fridge instead of cooking a new meal from scratch every time.
// app/products/page.tsx
export default async function ProductsPage() { // force-cache: use the saved copy if it exists (fastest) const cachedRes = await fetch("https://api.example.com/products", { cache: "force-cache", });
// no-store: always fetch fresh data, never save a copy const freshRes = await fetch("https://api.example.com/products", { cache: "no-store", });
// revalidate: use the saved copy, but refresh it every 60 seconds const timedRes = await fetch("https://api.example.com/products", { next: { revalidate: 60 }, });
const products = await timedRes.json();
return ( <ul> {products.map((p: { id: number; name: string }) => ( <li key={p.id}>{p.name}</li> ))} </ul> );}Which one should you use?
force-cache-> for data that never or rarely changes (like a list of countries)no-store-> for data that must always be up-to-date (like stock prices)revalidate-> for data that changes occasionally (like a blog or product list)
Static vs Dynamic Pages
Section titled “Static vs Dynamic Pages”Next.js can build your pages in two ways:
Static Pages
Section titled “Static Pages”The page is built once when you deploy your app and the same HTML is sent to every visitor. Think of it like printing a flyer - you print it once and hand out copies.
This is fast and the default behavior for most pages.
// app/about/page.tsx// This page is built ONCE when you deploy.// The time shown will NEVER change, even if you refresh.
export default function AboutPage() { const builtAt = new Date().toLocaleString(); // This runs only at build time
return ( <div> <h1>About Us</h1> <p>We are a team of developers building awesome things.</p> <p>This page was built at: {builtAt}</p> {/* No matter how many times you refresh, this time stays the same */} </div> );}Go ahead and refresh the page - the time will never change. That’s proof it was built once and frozen.
Dynamic Pages
Section titled “Dynamic Pages”The page is rebuilt fresh every time someone visits it. Use this when the content needs to be up to date.
To make a page dynamic, just add this one line at the top:
export const dynamic = "force-dynamic";// app/time/page.tsx// This page is rebuilt on EVERY visit.// The time shown will update every time you refresh.
export const dynamic = "force-dynamic";
export default function TimePage() { const now = new Date().toLocaleString(); // This runs on every single visit
return ( <div> <h1>Current Time</h1> <p>Right now it is: {now}</p> {/* Refresh the page - the time will update every time */} </div> );}Refresh that page a few times - the time updates on every visit. That’s dynamic rendering in action.
| Page type | When is it built? | Time example | Best for |
|---|---|---|---|
| Static | Once, when you deploy | Always shows the same time | About page, blog posts, docs |
| Dynamic | Every time someone visits | Shows the current time | Dashboards, user profiles, feeds |