Skip to content

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.

To create a new Next.js project, run this command in your terminal:

Docs: https://nextjs.org/docs/app/getting-started/installation

Terminal window
bun create next-app@latest my-next-app

This creates a new folder called my-next-app with everything you need. Now start the app:

Terminal window
cd my-next-app
bun dev

Open your browser and go to http://localhost:3000 - you should see your app running!

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.md

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 frame

Common 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.tsx
import { 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.

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.

app/
├── page.tsx -> /
├── about/
│ └── page.tsx -> /about
└── contact/
└── page.tsx -> /contact

That’s it. No extra setup needed.

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.tsx

Now all of these just work:

/blog/my-first-post
/blog/how-to-learn-nextjs
/blog/top-10-tips

Here’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>
);
}

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-in

The (marketing) and (auth) parts don’t show up in the URL - they’re just for you to stay organized.

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.tsx
import 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>
);
}

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.

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.

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.

What you’re doingUse this
Loading posts from a databaseServer Component
Showing a list of productsServer Component
A button that does something when clickedClient Component
A search bar that filters as you typeClient Component
A popup or dropdown menuClient Component
A page that just shows text or imagesServer Component

A good pattern is to use Server Components to load data, then pass it down to small Client Components that handle interactions.

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).

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.

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 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)

Next.js can build your pages in two ways:

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.

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 typeWhen is it built?Time exampleBest for
StaticOnce, when you deployAlways shows the same timeAbout page, blog posts, docs
DynamicEvery time someone visitsShows the current timeDashboards, user profiles, feeds