Home JavaScript Next.js Basics Explained: Pages, Routing, and Data Fetching

Next.js Basics Explained: Pages, Routing, and Data Fetching

In Plain English 🔥
Think of a regular React app like a food truck where every customer has to wait while the chef cooks their meal from scratch. Next.js is like a restaurant that pre-cooks popular dishes during quiet hours, so when you sit down, your food arrives almost instantly. For less popular dishes, it still cooks fresh — but it knows the difference. That's the core idea: Next.js decides the smartest way to serve each page of your app.
⚡ Quick Answer
Think of a regular React app like a food truck where every customer has to wait while the chef cooks their meal from scratch. Next.js is like a restaurant that pre-cooks popular dishes during quiet hours, so when you sit down, your food arrives almost instantly. For less popular dishes, it still cooks fresh — but it knows the difference. That's the core idea: Next.js decides the smartest way to serve each page of your app.

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.

ProductPage.js · JAVASCRIPT
1234567891011121314151617181920212223242526272829303132333435363738394041
// 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
▶ Output
Visiting http://localhost:3000/products/42

Page renders: 'You are viewing product ID: 42'

Visiting http://localhost:3000/products/running-shoes

Page renders: 'You are viewing product ID: running-shoes'
⚠️
Watch Out: router.query is empty on first renderWhen Next.js hydrates a page on the client, `router.query` starts as an empty object `{}` before populating with the actual URL values. Always guard with `if (!productId) return null` or a loading state — otherwise you'll pass `undefined` to your API calls and wonder why nothing works.

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.

BlogPost.js · JAVASCRIPT
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253
// 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
▶ Output
Build output (next build):

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.
⚠️
Pro Tip: ISR is the sweet spot for most real appsIncremental Static Regeneration (ISR) — enabled by adding `revalidate: N` to your `getStaticProps` return — gives you static speed with near-real-time freshness. The page serves from cache until N seconds pass, then regenerates in the background on the next visit. Most e-commerce and content sites should default to ISR over full SSR.

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.

ProductCard.js · JAVASCRIPT
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
// 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'
//   }}
// />
▶ Output
Rendered HTML (simplified):

<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.
🔥
Interview Gold: Why does next/image require width and height?It's not just about layout — it's about Cumulative Layout Shift (CLS), a Core Web Vitals metric. Without explicit dimensions, the browser doesn't know how much space to reserve for an image before it loads, causing content to jump around as images pop in. Providing width and height lets the browser pre-allocate that space, which keeps your CLS score near zero and directly impacts your Google search ranking.

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.

ProductPageAppRouter.js · JAVASCRIPT
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859
// 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>
  );
}
▶ Output
Server logs during request:
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 ✓'
⚠️
Pro Tip: Don't reach for 'use client' by defaultNew App Router developers instinctively add `'use client'` to every component because it feels familiar. Resist this. Keep components as Server Components unless they specifically need `useState`, `useEffect`, browser APIs, or event handlers. The more Server Components you have, the less JavaScript your users download — and that's the whole point of the App Router.
FeaturePages Router (pages/)App Router (app/)
Introduced in versionNext.js 1 (original)Next.js 13
Default component typeClient ComponentServer Component
Data fetching methodgetStaticProps / getServerSidePropsasync/await directly in component
ISR supportrevalidate in getStaticProps{ next: { revalidate: N } } in fetch
Client interactivityAll components can use hooksRequires 'use client' directive
LayoutsCustom _app.js + _document.jsNested layout.js files per folder
Learning curveLower — familiar React patternsHigher — new mental model required
Production readinessBattle-tested, widely usedStable since Next.js 13.4
Best forExisting projects, teams new to Next.jsNew 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.

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.

🔥
TheCodeForge Editorial Team Verified Author

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.

← PreviousReact Testing with JestNext →React Custom Hooks
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged