Next.js Basics Explained: Pages, Routing, and Data Fetching
- File-based routing means your folder structure in pages/ or app/ IS your navigation — no router config to maintain
- SSG (getStaticProps) builds HTML at deploy time and serves from CDN — always default to this
- ISR (adding revalidate: N to getStaticProps) gives you the speed of static with near-real-time freshness
- Next.js is a React framework that adds file-based routing, server rendering, and build-time optimisation
- File-based routing: drop a file in pages/ or app/ and the route is live — no router config needed
- SSG (getStaticProps) builds HTML at deploy time — fastest option, served from CDN
- SSR (getServerSideProps) builds HTML per request — use only when data must be fresh AND in initial HTML
- ISR adds a revalidate timer to SSG — static speed with near-real-time freshness
- Biggest mistake: using SSR everywhere for 'freshness' when ISR solves 90% of those cases faster
Page is server-rendered instead of static
next build 2>&1 | grep -E '●|λ|○'cat .next/server/pages/products/*.html 2>/dev/null | head -5Build fails with getStaticPaths error
grep -rn 'getStaticPaths' pages/ --include='*.js' --include='*.tsx'next build 2>&1 | grep -A5 'getStaticPaths'Image optimization not working in production
cat next.config.js | grep -A10 'images'curl -sI http://localhost:3000/_next/image?url=/test.jpg | head -10Hydration mismatch error in console
grep -rn 'typeof window\|Date.now\|Math.random' pages/ components/ --include='*.js' --include='*.tsx'grep -rn 'useEffect\|useState' pages/ --include='*.js' --include='*.tsx'Production Incident
Production Debug GuideDiagnose rendering and data fetching issues in production
Next.js solves three problems that plain React cannot: initial page load performance, search engine visibility, and routing complexity. A standard React app ships a blank HTML shell and waits for JavaScript to hydrate — users see a spinner, crawlers see nothing. Next.js renders pages on the server or at build time, so users and search engines get real HTML immediately.
The framework supports four rendering strategies in a single project: Static Site Generation (SSG), Server-Side Rendering (SSR), Client-Side Rendering (CSR), and Incremental Static Regeneration (ISR). Choosing the right strategy per page is the core skill that separates a competent Next.js developer from one who ships slow, expensive applications.
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
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.
// 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
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.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' // }} // />
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 with caching options. It's more flexible but requires a mental shift.fetch()
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> ); }
'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.API Routes and Middleware: Building the Backend Inside Next.js
Next.js includes a built-in API layer. Files inside pages/api/ become serverless API endpoints — no separate backend server needed. This is the simplest way to add server-side logic to a Next.js application: form submissions, webhook handlers, authentication endpoints, and database queries all live inside the same project.
The App Router changes the API story. Instead of pages/api/, API routes live inside app/api/ as route.js files. Each HTTP method (GET, POST, PUT, DELETE) is exported as a named function. The handler receives a standard Web Request object and returns a standard Web Response — aligning with the web platform instead of Node.js-specific APIs.
Middleware runs before every request reaches your pages or API routes. It lives in middleware.js at the project root and is the right place for authentication checks, redirects, feature flags, and request logging. Middleware runs on the Edge Runtime by default — it executes at the CDN edge, not on your origin server, which means sub-millisecond latency for simple checks.
// --- App Router API Route --- // File: app/api/users/route.js // Each HTTP method is a named export import { NextResponse } from 'next/server'; // GET /api/users — list all users export async function GET(request) { const { searchParams } = new URL(request.url); const page = parseInt(searchParams.get('page') || '1'); const limit = parseInt(searchParams.get('limit') || '20'); // In production: call your database here const users = await db.users.findMany({ skip: (page - 1) * limit, take: limit, }); return NextResponse.json({ data: users, pagination: { page, limit }, }); } // POST /api/users — create a new user export async function POST(request) { const body = await request.json(); // Validate input if (!body.email || !body.name) { return NextResponse.json( { error: 'Email and name are required' }, { status: 400 } ); } const user = await db.users.create({ data: body }); return NextResponse.json({ data: user }, { status: 201 }); } // --- Middleware --- // File: middleware.js (project root) // Runs on every request before pages/API routes import { NextResponse } from 'next/server'; export function middleware(request) { // Example: redirect unauthenticated users const token = request.cookies.get('auth-token'); // Protect dashboard routes if (request.nextUrl.pathname.startsWith('/dashboard') && !token) { return NextResponse.redirect(new URL('/login', request.url)); } // Add request ID for tracing const response = NextResponse.next(); response.headers.set('x-request-id', crypto.randomUUID()); return response; } // Matcher controls which routes trigger middleware export const config = { matcher: ['/dashboard/:path*', '/api/protected/:path*'], };
- Runs on the Edge Runtime — executes at the CDN, not your origin server
- Sub-millisecond latency for simple checks like auth tokens and feature flags
- Cannot use Node.js APIs (fs, crypto.randomBytes) — only Web APIs available
- Use matcher config to limit which routes trigger middleware — avoid running on static assets
- The right place for: auth checks, redirects, A/B testing, request logging, rate limiting
Deployment and Performance: What Matters in Production
Understanding rendering strategies is only half the picture. In production, deployment configuration, caching headers, and build optimisation determine whether your app is fast or expensive. A perfectly coded Next.js app deployed without CDN caching is slower than a poorly coded app deployed with proper caching.
Vercel is the default deployment platform for Next.js — both are made by the same company. It handles ISR caching, edge functions, image optimisation, and preview deployments automatically. But Next.js deploys anywhere: AWS, Google Cloud, Docker containers, or static hosting for fully-static sites.
The three production levers: CDN caching (serve static assets from edge locations), ISR revalidation (serve cached pages, regenerate in background), and bundle analysis (ship less JavaScript to the browser). Pulling these three levers correctly is worth more than any code optimisation.
// File: next.config.js // Production configuration that actually matters /** @type {import('next').NextConfig} */ const nextConfig = { // Image optimisation domains — required for next/image with remote images images: { domains: ['images.example.com', 'cdn.example.com'], // Or use remotePatterns for more control (Next.js 13+) remotePatterns: [ { protocol: 'https', hostname: '**.example.com', }, ], }, // Headers: set caching policies for static assets async headers() { return [ { // Cache static assets (JS, CSS, images) for 1 year source: '/_next/static/:path*', headers: [ { key: 'Cache-Control', value: 'public, max-age=31536000, immutable', }, ], }, { // Cache API responses for 60 seconds source: '/api/:path*', headers: [ { key: 'Cache-Control', value: 'public, s-maxage=60, stale-while-revalidate=300', }, ], }, ]; }, // Redirects: handle legacy URLs and SEO async redirects() { return [ { source: '/old-blog/:slug', destination: '/blog/:slug', permanent: true, // 301 redirect — SEO-friendly }, ]; }, // Rewrites: proxy external APIs through Next.js async rewrites() { return [ { source: '/api/external/:path*', destination: 'https://external-api.example.com/:path*', }, ]; }, // Bundle analysis — run ANALYZE=true next build to visualise // Identifies large dependencies that should be code-split ...(process.env.ANALYZE === 'true' && { webpack: (config) => { const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); config.plugins.push( new BundleAnalyzerPlugin({ analyzerMode: 'static', reportFilename: './bundle-analysis.html', }) ); return config; }, }), }; module.exports = nextConfig; // --- Production Checklist --- // // 1. Run ANALYZE=true next build — check bundle-analysis.html for large deps // 2. Set Cache-Control headers for static assets (1 year, immutable) // 3. Set Cache-Control for API routes (s-maxage + stale-while-revalidate) // 4. Use next/image for all images — never use plain <img> // 5. Use next/link for all internal navigation — never use plain <a> // 6. Pre-render top pages with getStaticProps — serve from CDN // 7. Use ISR for pages that change infrequently — not full SSR // 8. Add middleware for auth — do not check auth in every page component
| 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 — no router config to maintain
- SSG (getStaticProps) builds HTML at deploy time and serves from CDN — always default to this
- ISR (adding revalidate: N to getStaticProps) gives you the speed of static with near-real-time freshness
- getServerSideProps runs a server function on every request — use it only when data must be fresh per-request
- In the App Router, components are Server Components by default — only add 'use client' when you need interactivity
- Cache-Control headers and next/image optimisation are the biggest production performance levers
⚠ Common Mistakes to Avoid
Interview Questions on This Topic
- QWhat is the difference between getStaticProps and getServerSideProps, and how do you decide which one to use for a given page?Mid-levelReveal
- QWhat are React Server Components in Next.js's App Router, and why can't you use useState or useEffect inside them?Mid-levelReveal
- 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.SeniorReveal
- QWhat is the difference between the Pages Router and the App Router in Next.js?JuniorReveal
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 is 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.
Can I deploy Next.js without Vercel?
Yes. Next.js deploys anywhere: AWS (with SST or Amplify), Google Cloud, Docker containers, Netlify, or static hosting for fully-static sites. Vercel provides the tightest integration (automatic ISR caching, edge functions, preview deployments) but is not required. For self-hosted deployments, use next start with a Node.js server or export a static site with next export for fully-static applications.
How do I know which rendering strategy a page is using?
Run next build — the output shows each page with a symbol: ● (static/SSG), λ (server/SSR), or ○ (static with ISR). In development mode, check the terminal output or use the Next.js DevTools browser extension. You can also check the response headers — static pages have different cache headers than server-rendered pages.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.