Skip to main content
โšก Calmops

Next.js Fundamentals: Routing, SSR/SSG, API Routes & Image Optimization

Next.js is a production-ready React framework that makes building fast, SEO-friendly, and scalable web apps straightforward. In this post you’ll learn four foundational features that unlock Next.js’s power: file-based routing, server-side rendering (SSR) and static site generation (SSG) (plus Incremental Static Regeneration, ISR), API routes for backend endpoints, and built-in image optimization. Each section explains what the feature does, why it matters, and shows practical code examples you can drop into a Next.js project.


Core Terms & Abbreviations โ€” Quick Glossary ๐Ÿ”ค

  • SFC (Single File Component): not specific to Next.js but common in frameworks โ€” components colocating markup and logic.
  • SSR (Server-Side Rendering): rendering HTML on every request on the server.
  • SSG (Static Site Generation): build-time rendering of pages to static HTML.
  • ISR (Incremental Static Regeneration): revalidate and regenerate static pages after build without a full rebuild.
  • CDN (Content Delivery Network): global caching layer for serving assets and static pages.
  • LCP / TTFB: Core Web Vitals metrics (Largest Contentful Paint, Time to First Byte) relevant for measuring performance.

If any term is unfamiliar, the Further Reading section links to authoritative resources.

1) File-based Routing โ€” routes from the filesystem ๐Ÿ—‚๏ธ

How it works

Next.js creates routes automatically based on files in the pages/ (classic) or app/ (new app router) directories. You don’t wire up routers manually โ€” the framework maps files to URLs.

Examples

  • pages/index.js -> /
  • pages/about.js -> /about
  • pages/blog/[slug].js -> /blog/:slug (dynamic route using bracket notation)

Dynamic route example (pages router):

// pages/blog/[slug].js
import { useRouter } from 'next/router';

export default function Post({ post }) {
  // `post` can be fetched via getStaticProps / getServerSideProps
  const router = useRouter();
  if (router.isFallback) return <p>Loadingโ€ฆ</p>;
  return <article><h1>{post.title}</h1><p>{post.content}</p></article>;
}

App router (Next.js 13+) uses file-based folders and page.js files. Nested layouts and server components make composition easier.

App router example (app directory):

// app/blog/[slug]/page.js  (server component)
export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts').then(r => r.json());
  return posts.map(p => ({ slug: p.slug }));
}

export default async function Post({ params }) {
  const post = await fetch(`https://api.example.com/posts/${params.slug}`).then(r => r.json());
  return (<article><h1>{post.title}</h1><div dangerouslySetInnerHTML={{ __html: post.html }} /></article>);
}

Dynamic patterns:

  • Catch-all: pages/blog/[...slug].js maps /blog/a/b/c to slug = ['a','b','c'].
  • Optional catch-all: [[...slug]] allows the param to be empty.

Why it matters

File-based routing reduces boilerplate and keeps routes discoverable. Dynamic routes and nested layouts help model content-driven sites naturally.

Key tip

  • For large apps, organize route folders by feature to keep related code and styles together.

2) SSR vs SSG (and ISR) โ€” choose the right rendering strategy โš™๏ธ

Definitions

  • SSR (Server-Side Rendering): the server generates HTML on every request โ€” great for dynamic data that needs to be fresh.
  • SSG (Static Site Generation): pages are pre-rendered at build time โ€” ideal for pages that don’t change often (very fast & cacheable).
  • ISR (Incremental Static Regeneration): a hybrid where static pages are regenerated on a schedule or on demand, combining SSG performance with freshness.

Pages router examples

SSR example (runs on each request):

// pages/profile.js
export async function getServerSideProps(context) {
  const res = await fetch(`https://api.example.com/users/${context.params.id}`);
  const user = await res.json();
  return { props: { user } };
}
export default function Profile({ user }) { return <div>{user.name}</div>; }

SSG example (build time; pre-render):

// pages/posts/[slug].js
export async function getStaticPaths() {
  const posts = await fetch('https://api.example.com/posts').then(r => r.json());
  return { paths: posts.map(p => ({ params: { slug: p.slug } })), fallback: true };
}
export async function getStaticProps({ params }) {
  const post = await fetch(`https://api.example.com/posts/${params.slug}`).then(r => r.json());
  return { props: { post }, revalidate: 60 }; // ISR: revalidate after 60s
}

### Fallback behavior

- `fallback: false` โ€” only paths returned by `getStaticPaths` are available (others 404).
- `fallback: true` โ€” pages not generated at build time render a fallback on first request and are generated in background.
- `fallback: 'blocking'` โ€” the server will render the page on first request and block until it's ready (no client fallback UI needed).

### On-demand (API-triggered) ISR revalidation

You can revalidate static pages on demand (useful after content updates) via an API route that calls `res.revalidate()`.

```js
// pages/api/revalidate.js
export default async function handler(req, res) {
  // protect with a secret token
  if (req.query.secret !== process.env.REVALIDATE_SECRET) {
    return res.status(401).json({ message: 'Invalid token' });
  }
  try {
    await res.revalidate(`/posts/${req.query.slug}`);
    return res.json({ revalidated: true });
  } catch (err) {
    return res.status(500).send('Error revalidating');
  }
}

App router hint (Next 13+): use fetch in server components with fetch(url, { next: { revalidate: 60 } }) or export export const revalidate = 60 in page modules.

When to use each

  • Use SSR for per-request personalization (auth’d dashboards, user-specific content).
  • Use SSG for marketing pages, blogs, docs โ€” where speed and caching matter.
  • Use ISR when content is mostly static but needs periodic updates without full rebuilds.

3) API Routes โ€” build backend endpoints inside your app ๐Ÿ”Œ

What they are

Next.js lets you create API endpoints inside pages/api/ (or app/api/ with the app router). These endpoints run as serverless functions and simplify full-stack development since backend code lives alongside the frontend.

Example (pages API):

// pages/api/hello.js
export default function handler(req, res) {
  res.status(200).json({ message: 'Hello from Next.js API route' });
}

Example (app router, route handlers):

// app/api/hello/route.js
export async function GET(request) {
  return new Response(JSON.stringify({ message: 'Hello from app route' }), { status: 200 });
}

POST example with JSON body (pages API):

// pages/api/messages.js
export default async function handler(req, res) {
  if (req.method !== 'POST') return res.status(405).end();
  const { text } = req.body; // Next.js parses JSON automatically
  // validate & use env secrets from process.env
  if (!text) return res.status(400).json({ error: 'Missing text' });
  // persist or forward to a service
  await saveMessage({ text });
  res.status(201).json({ ok: true });
}

Security & runtime notes:

  • Protect revalidation endpoints with a secret and environment variables (don’t embed secrets in client code).
  • You can opt in to the edge runtime for faster cold starts in supported providers by exporting export const runtime = 'edge' in your route module (app router).

Why it matters

API routes are ideal for small CRUD endpoints, webhooks, form handlers, and server-side logic that doesn’t require a separate backend service. They integrate with environment variables and middleware, and are easy to protect (authentication, rate limits).

Key tip

  • For complex or high-throughput APIs, prefer a dedicated API service, but API routes are perfect for co-located, app-specific endpoints.

4) Image Optimization โ€” faster pages, better Core Web Vitals ๐Ÿ–ผ๏ธ

Built-in optimization

Next.js’s <Image /> component automatically optimizes images: it serves appropriately sized images per device, lazy-loads below-the-fold images, and can convert to modern formats (WebP/AVIF) on demand.

Example usage:

import Image from 'next/image';

export default function Hero() {
  return (
    <Image
      src="/images/hero.jpg"
      width={1600}
      height={900}
      alt="Hero"
      priority={true} // for LCP
    />
  );
}

Notes:

  • Configure allowed external domains in next.config.js (if using remote images).
  • Use priority for LCP images and placeholder="blur" for progressive loading.

Example next.config.js image config:

// next.config.js
module.exports = {
  images: {
    domains: ['images.example.com'],
    formats: ['image/avif', 'image/webp'],
  },
};

Responsive image note:

<Image src="/photo.jpg" width={1200} height={800} sizes="(max-width:600px) 100vw, 50vw" placeholder="blur" />

Important: next/image optimization requires a server or hosting that supports on-demand image sizing (Vercel supports this out of the box). For next export (fully static exports), you may need to set unoptimized or handle images differently.

Why it matters

Optimized images improve Largest Contentful Paint (LCP) and reduce bandwidth, especially on mobile. Next.js handles much of the heavy lifting so you can focus on content.


Deployment & Architecture โ€” simple text graph ๐Ÿ—๏ธ

Common flow for a Next.js site deployed to Vercel (recommended):

git -> Vercel -> CDN (edge cache) -> edge functions (SSR/API) -> backend services / DB

Image optimization pipeline example: git -> build -> CDN -> image optimizer (on-edge or third-party) -> client

For static sites:

git -> Vercel (build) -> static assets -> CDN -> client

Key point: deploy providers like Vercel tightly integrate SSR, ISR, and edge functions for low-latency global delivery.


Pitfalls, Best Practices & When Not To Use Next.js โš ๏ธโœ…

Pitfalls

  • Choosing SSR by default increases complexity and can hurt scalability; prefer SSG/ISR where possible.
  • Putting expensive synchronous work in server-side functions can slow responses โ€” async, caching, and incremental rendering help.
  • Misconfigured image domains or sizes can cause issues โ€” test diverse viewports.

Best Practices

  • Use SSG + ISR for content that benefits from speed with periodic updates (blogs, docs).
  • Use SSR for per-request personalization and secure data that can’t be cached.
  • Cache API responses and use revalidation to reduce origin load.
  • Measure Core Web Vitals and prioritize LCP and TTFB improvements.

Additional best practices:

  • Use the next/script component to load third-party scripts non-blocking (strategy="lazyOnload" or afterInteractive).
  • Use SWR or React Query on the client for incremental data fetching and stale-while-revalidate patterns when appropriate.
  • For large apps, prefer the app router for nested layouts and server components, but the pages router remains stable and supported.

When to consider alternatives

  • Choose React + custom backend if you need very specialized server infrastructure or a large microservices backend managed separately.
  • Consider frameworks like Remix if control over caching and nested routes fits your team better.

Further Reading & Resources ๐Ÿ“š


Conclusion โ€” practical, fast, and flexible ๐Ÿš€

Next.js simplifies many hard parts of modern web apps: routes become files, rendering can be tuned (SSR/SSG/ISR), backend endpoints can live with your frontend, and images are optimized for performance. Start by converting key pages to SSG or SSR as needed, use API routes for tightly-coupled server logic, and leverage the <Image /> component to boost performance. When in doubt, measure: real user metrics will guide the right tradeoffs for your app.

Comments