Next.js Basics Explained: Pages, Routing, and Data Fetching
Every React developer eventually hits the same wall. Your single-page app loads a blank white screen, then a spinner, then finally the content — and meanwhile Google's crawler has already moved on because it couldn't read anything useful. Users on slow connections bounce. SEO tanks. You start googling 'how to make React faster' at 11pm and every answer says the same thing: use Next.js. There's a reason for that.
File-Based Routing: Why Your Folder Structure IS Your App's Navigation
In a standard React app, you manually wire up React Router — you import BrowserRouter, define Route components, and manage path strings in your head. It works, but it's boilerplate you didn't need to write. Next.js takes a different philosophy: your file system is your router. Drop a file called about.js inside the pages folder and /about becomes a live route instantly. No imports. No configuration. No forgetting to register a new page.
This isn't just a convenience trick. It's an architectural choice that enforces consistency across teams. When a new developer joins your project, they don't need to read routing config — they just look at the folder structure. Files are truth.
Dynamic routes use square-bracket syntax. A file named [productId].js matches /products/42, /products/shoes, or any slug you throw at it. The matched value lands in useRouter().query so you can fetch the right data. This pattern covers 90% of real-world routing needs — product pages, blog posts, user profiles — without a single line of router config.
// File location: pages/products/[productId].js // This single file handles EVERY product URL: /products/1, /products/42, etc. import { useRouter } from 'next/router'; export default function ProductPage() { // useRouter gives us access to the URL parameters const router = useRouter(); // productId comes from the filename [productId].js // On the URL /products/42, this will be the string '42' const { productId } = router.query; // During the initial render on the client, query can be empty // so we guard against that with a loading state if (!productId) { return <p>Loading product...</p>; } return ( <div> <h1>Product Details</h1> {/* Shows the dynamic segment from the URL */} <p>You are viewing product ID: {productId}</p> <p> This page was generated by the file: pages/products/[productId].js </p> </div> ); } // --- FOLDER STRUCTURE REFERENCE --- // pages/ // index.js → renders at / // about.js → renders at /about // products/ // index.js → renders at /products // [productId].js → renders at /products/:productId // blog/ // [slug].js → renders at /blog/:slug
Page renders: 'You are viewing product ID: 42'
Visiting http://localhost:3000/products/running-shoes
Page renders: 'You are viewing product ID: running-shoes'
SSG vs SSR vs CSR: Choosing the Right Data Fetching Strategy
This is where Next.js earns its keep — and where most developers get confused because they treat all three strategies as interchangeable. They're not. Each one answers a different question about WHEN data should be fetched and WHO should do the fetching.
Static Site Generation (SSG) with getStaticProps runs at build time on the server. The HTML is generated once and served from a CDN forever. This is perfect for content that doesn't change often — a product catalogue, a blog post, a pricing page. It's the fastest option because there's no server work happening per request.
Server-Side Rendering (SSR) with getServerSideProps runs on every single request. The server fetches fresh data and builds the HTML before sending it to the user. Use this only when the data must be up-to-the-second fresh AND you need it in the initial HTML — think a live dashboard or a personalised feed that depends on the user's session cookie.
Client-Side Rendering (CSR) is just regular React — the page loads, then JavaScript fetches data in a useEffect. Use this for data that's behind authentication and doesn't need to be indexed by search engines, like user account settings. Mixing all three in the same app based on each page's needs is the Next.js superpower.
// File: pages/blog/[slug].js // This page uses SSG — it's pre-built at deploy time // Perfect for blog posts that don't change every minute export default function BlogPost({ post }) { // 'post' comes from getStaticProps below — it's already resolved // by the time this component renders. No loading state needed! return ( <article> <h1>{post.title}</h1> <p>By {post.author} on {post.publishedDate}</p> <div>{post.content}</div> </article> ); } // getStaticPaths tells Next.js WHICH URLs to pre-build // This runs at build time — not on every request export async function getStaticPaths() { // In real life, fetch this list from your CMS or database const publishedSlugs = ['intro-to-nextjs', 'react-hooks-explained', 'css-grid-guide']; const paths = publishedSlugs.map((slug) => ({ params: { slug }, // must match the [slug] in your filename })); return { paths, // fallback: false means unknown slugs return a 404 // fallback: 'blocking' means unknown slugs trigger SSR on first hit, then cache fallback: false, }; } // getStaticProps receives the slug and fetches that post's data // Runs ONCE per slug at build time — never again (until revalidate triggers) export async function getStaticProps({ params }) { // Simulating a CMS or database fetch using the slug from the URL const response = await fetch(`https://api.example.com/posts/${params.slug}`); const post = await response.json(); return { props: { post }, // injected as props into BlogPost component above // revalidate: 60 means re-generate this page in the background // if a request comes in after 60 seconds — this is called ISR revalidate: 60, }; } // --- WHEN TO USE WHAT --- // getStaticProps → blog posts, product pages, marketing pages (fast, SEO-friendly) // getServerSideProps → user dashboards, pages needing live data or session cookies // useEffect fetch → post-login UI, private account pages, non-SEO content
Page Size First Load JS
─────────────────────────────────────────────────────
● /blog/[slug] 3.2 kB 72.1 kB
├ /blog/intro-to-nextjs
├ /blog/react-hooks-explained
└ /blog/css-grid-guide
● = Static (SSG) — pre-rendered as HTML at build time
Runtime visit to /blog/intro-to-nextjs:
Page is served instantly from CDN — zero server processing.
The Next.js Link and Image Components: Not Just Wrappers
Two components trip up almost every developer coming from plain React: next/link and next/image. They look like simple wrappers around an anchor tag and an img tag, but they're doing serious work under the hood — work that would take hours to implement manually.
next/link enables client-side navigation. When you use it, clicking a link doesn't trigger a full browser reload — Next.js swaps the page in JavaScript, keeping your app state alive. But here's the part that actually matters: Next.js automatically prefetches linked pages in the background when they appear in the viewport. The user moves their mouse toward a link and the destination is already loading. This is why Next.js apps feel snappy — it's not magic, it's strategic prefetching.
next/image solves one of the biggest real-world performance problems: images. It automatically resizes images to the right dimensions for the user's device, serves modern formats like WebP when the browser supports it, lazy-loads images below the fold by default, and prevents layout shift by requiring width and height props. The priority prop tells it to load a specific image eagerly — use that on your above-the-fold hero image to nail your Core Web Vitals score.
// File: components/ProductCard.js // Demonstrates real-world usage of next/link and next/image import Link from 'next/link'; import Image from 'next/image'; export default function ProductCard({ product }) { return ( <div className="product-card"> {/* next/image vs plain <img>: - Automatically serves WebP format if browser supports it - Resizes to the rendered size — not the original 4000px source - Lazy loads by default (doesn't load until near viewport) - Prevents CLS (Cumulative Layout Shift) via the width/height aspect ratio */} <Image src={product.imageUrl} alt={product.name} // alt is required — good for accessibility AND SEO width={400} // intrinsic width of the image height={300} // intrinsic height — sets the aspect ratio // Add priority={true} ONLY for above-the-fold images like hero banners // priority={true} tells Next.js to preload it, skipping lazy loading /> <h2>{product.name}</h2> <p>${product.price}</p> {/* next/link vs plain <a>: - Client-side navigation — no full page reload - Prefetches the linked page HTML+JS in the background when visible - Falls back to normal <a> behaviour if JavaScript hasn't loaded yet */} <Link href={`/products/${product.id}`}> {/* As of Next.js 13+, you don't need a nested <a> tag anymore */} View Product Details </Link> </div> ); } // Usage in a parent page: // <ProductCard // product={{ // id: 42, // name: 'Trail Running Shoes', // price: 129.99, // imageUrl: '/images/trail-shoes.jpg' // }} // />
<div class="product-card">
<img
src="/_next/image?url=%2Fimages%2Ftrail-shoes.jpg&w=400&q=75"
srcset="...webp versions at multiple sizes..."
loading="lazy"
alt="Trail Running Shoes"
width="400"
height="300"
/>
<h2>Trail Running Shoes</h2>
<p>$129.99</p>
<a href="/products/42">View Product Details</a>
</div>
Notice: Next.js rewrote the image src to its optimisation pipeline automatically.
The App Router vs Pages Router: What Changed in Next.js 13+
If you've been reading Next.js docs and feeling confused by two completely different patterns, here's the honest explanation: Next.js 13 introduced a brand-new routing system called the App Router, and both systems now coexist in the same framework. The older system is the Pages Router (everything inside a pages/ folder, which is what this article has covered so far). The App Router lives inside an app/ folder and uses a different set of conventions.
The App Router is built around React Server Components — a newer React feature that lets components run only on the server, with zero JavaScript shipped to the browser. In the App Router, every component is a Server Component by default. You opt into client-side interactivity by adding 'use client' at the top of a file.
The data fetching story also changes: instead of getStaticProps and getServerSideProps, you just use async/await directly inside Server Components and call fetch() with caching options. It's more flexible but requires a mental shift.
For new production projects starting today, learn the App Router. For jobs and existing codebases, the Pages Router is still everywhere — understanding both is what separates a competent Next.js developer from a great one.
// File: app/products/[productId]/page.js (App Router convention) // Notice: no getStaticProps, no useRouter for data — just async/await directly // This is a SERVER COMPONENT by default. // It runs only on the server. Zero JS is sent to the browser for this component. // That means: faster page loads, direct database access, no API layer needed. export default async function ProductPage({ params }) { // params comes from the folder name [productId] — same concept as Pages Router const { productId } = params; // fetch() in Server Components is enhanced by Next.js // next: { revalidate: 3600 } is equivalent to ISR — cache for 1 hour const response = await fetch( `https://api.example.com/products/${productId}`, { next: { revalidate: 3600 } } // ISR-style caching built right into fetch ); const product = await response.json(); return ( <div> <h1>{product.name}</h1> <p>{product.description}</p> <p>Price: ${product.price}</p> {/* AddToCartButton needs onClick — that's client-side behaviour. We mark it 'use client' in its own file and import it here. The Server Component passes data down; the Client Component handles interaction. */} <AddToCartButton productId={productId} /> </div> ); } // --- app/components/AddToCartButton.js --- // 'use client' at the top opts this file into client-side rendering // Only components that NEED interactivity should be client components 'use client'; import { useState } from 'react'; export function AddToCartButton({ productId }) { const [added, setAdded] = useState(false); function handleClick() { // In real life: call your cart API here console.log(`Adding product ${productId} to cart`); setAdded(true); } return ( <button onClick={handleClick}> {added ? 'Added to Cart ✓' : 'Add to Cart'} </button> ); }
Fetching https://api.example.com/products/42 (cache MISS — first request)
Rendered ProductPage on server in 34ms
HTML sent to browser — zero product page JS bundle included
Browser receives:
- Full HTML with product data already populated
- Small JS bundle only for AddToCartButton (the client component)
User clicks 'Add to Cart':
Adding product 42 to cart
Button text changes to: 'Added to Cart ✓'
| Feature | Pages Router (pages/) | App Router (app/) |
|---|---|---|
| Introduced in version | Next.js 1 (original) | Next.js 13 |
| Default component type | Client Component | Server Component |
| Data fetching method | getStaticProps / getServerSideProps | async/await directly in component |
| ISR support | revalidate in getStaticProps | { next: { revalidate: N } } in fetch |
| Client interactivity | All components can use hooks | Requires 'use client' directive |
| Layouts | Custom _app.js + _document.js | Nested layout.js files per folder |
| Learning curve | Lower — familiar React patterns | Higher — new mental model required |
| Production readiness | Battle-tested, widely used | Stable since Next.js 13.4 |
| Best for | Existing projects, teams new to Next.js | New projects, performance-critical apps |
🎯 Key Takeaways
- File-based routing means your folder structure in pages/ or app/ IS your navigation — there's no router config to maintain, which keeps large codebases readable by default.
- SSG (getStaticProps) builds HTML at deploy time and serves from CDN — always default to this. Only upgrade to SSR (getServerSideProps) when the data truly must be fresh per-request AND needs to be in the initial HTML.
- ISR (adding revalidate: N to getStaticProps) gives you the speed of static with near-real-time freshness — it's the correct choice for most e-commerce and content sites, not SSR.
- In the App Router, components are Server Components by default and run only on the server — only add 'use client' when a component specifically needs browser APIs, event handlers, or React state/effects.
⚠ Common Mistakes to Avoid
- ✕Mistake 1: Using getServerSideProps everywhere for 'freshness' — Symptom: pages feel slow, Time to First Byte is high, server costs are unexpectedly large — Fix: Default to getStaticProps with revalidate (ISR). Only reach for getServerSideProps when you genuinely need per-request data like session cookies or live stock prices. SSR runs a server function on every single page load — it's expensive and often unnecessary.
- ✕Mistake 2: Putting a plain tag inside next/link — Symptom: In older Next.js versions, you get a warning 'Do not use inside '. In Next.js 13+, if you accidentally wrap a Link around an existing , you get nested anchor tags which breaks accessibility and navigation — Fix: In Next.js 13+, Link renders its own tag automatically. Just put text or other non-anchor elements as children of Link. Only add an tag as a child if you're on Next.js 12 or older.
- ✕Mistake 3: Ignoring the
fallbackoption in getStaticPaths — Symptom: You deploy, a user visits a new product page that wasn't in your build, and they hit a 404 even though the product exists in your database — Fix: Setfallback: 'blocking'in getStaticPaths. This tells Next.js to SSR any page not pre-built, then cache it for future visitors. This way your build only pre-generates your most popular pages (keeping build times fast) while still serving any valid URL correctly.
Interview Questions on This Topic
- QWhat's the difference between getStaticProps and getServerSideProps, and how do you decide which one to use for a given page?
- QWhat are React Server Components in Next.js's App Router, and why can't you use useState or useEffect inside them?
- QIf a page uses getStaticProps with revalidate: 60, and two users hit that page within the same minute — what does each user actually receive? Walk me through what Next.js does behind the scenes.
Frequently Asked Questions
Do I need to know React before learning Next.js?
Yes — Next.js is a framework built on top of React, not a replacement for it. You should be comfortable with React components, props, state, and hooks before starting with Next.js. The framework handles routing, rendering strategy, and optimisation on top of the React you already know.
Is Next.js only for server-side rendering?
No — this is one of the biggest misconceptions. Next.js supports static generation (SSG), server-side rendering (SSR), client-side rendering (CSR), and incremental static regeneration (ISR) all in the same project, on a page-by-page basis. Most production apps use a mix of all four strategies depending on what each page needs.
What's the difference between the app/ folder and the pages/ folder in Next.js 13+?
They represent two different routing and rendering systems that can coexist in one project. The pages/ folder uses the original Pages Router with getStaticProps and getServerSideProps. The app/ folder uses the newer App Router where components are Server Components by default and data fetching happens with async/await directly in components. New projects should use the App Router; existing projects can migrate gradually.
Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.