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->/aboutpages/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].jsmaps/blog/a/b/ctoslug = ['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
edgeruntime for faster cold starts in supported providers by exportingexport 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
priorityfor LCP images andplaceholder="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/scriptcomponent to load third-party scripts non-blocking (strategy="lazyOnload"orafterInteractive). - Use SWR or React Query on the client for incremental data fetching and stale-while-revalidate patterns when appropriate.
- For large apps, prefer the
approuter for nested layouts and server components, but thepagesrouter 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 ๐
- Official Next.js docs: nextjs.org
- Vercel docs: vercel.com/docs
- Image component guide: Next/Image docs
- Examples repo: vercel/next.js examples
- Articles & tutorials: Next.js announcement/blog posts and community tutorials on edge rendering and ISR
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