Next.js: the framework that turns React into a supercharged, server-rendered, SEO-friendly powerhouse.
If React is a Swiss Army knife, Next.js is that version with a built-in flamethrower and bottle opener.
:)
(How’s that for a visual? )
A Brief History of Next.js (Or: How We Got Here)
Next.js was born in 2016, crafted by the fine folks at Vercel (then called ZEIT). Their mission? To make React even better by simplifying server-side rendering (SSR) and static site generation (SSG).
Before Next.js, doing SSR with React felt like assembling IKEA furniture without a manual—painful, confusing, and somehow always missing one key piece.
Here’s how Next.js evolved over the years:
Version | Release Date | Notable Features |
---|
1.0 | 2016-10-25 | Server-side rendering (SSR), simple routing |
2.0 | 2017-03-27 | Static exports, custom server support |
3.0 | 2017-08-14 | Prefetching, dynamic imports |
4.0 | 2017-12-07 | Improved SSR, better error handling |
5.0 | 2018-03-09 | Webpack 4, multi-zone support |
6.0 | 2018-06-27 | Automatic static optimization |
7.0 | 2018-11-21 | Faster builds, improved prefetching |
8.0 | 2019-02-27 | Incremental Static Regeneration (ISR) |
9.0 | 2019-07-17 | API routes, automatic static generation |
10.0 | 2020-10-27 | Internationalized routing, image optimization |
11.0 | 2021-06-15 | Webpack 5, faster refresh |
12.0 | 2021-10-26 | Middleware, React 18 support |
13.0 | 2022-10-25 | App directory, RSC (React Server Components) |
14.0 | 2023-10-26 | TurboPack, huge performance boost |
1. Creating a Simple Page in Next.js
Every file inside the /pages
directory is a route automatically. Here’s the simplest page you can create:
1
2
3
4
| // pages/index.js
export default function Home() {
return <h1>Welcome to Next.js!</h1>;
}
|
2. Dynamic Routing with URL Parameters
Need dynamic routes? No problem.
1
2
3
4
5
6
7
8
| // pages/post/[id].js
import { useRouter } from 'next/router';
export default function Post() {
const router = useRouter();
const { id } = router.query;
return <h1>Post ID: {id}</h1>;
}
|
3. Server-Side Rendering (SSR)
Need to fetch data on each request? Use getServerSideProps
.
1
2
3
4
5
6
7
8
9
10
| // pages/ssr-example.js
export async function getServerSideProps() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts/1');
const data = await res.json();
return { props: { post: data } };
}
export default function SSRPage({ post }) {
return <h1>{post.title}</h1>;
}
|
4. Static Site Generation (SSG)
For super-fast performance, pre-render pages at build time.
1
2
3
4
5
6
7
8
9
10
| // pages/ssg-example.js
export async function getStaticProps() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts/1');
const data = await res.json();
return { props: { post: data } };
}
export default function SSGPage({ post }) {
return <h1>{post.title}</h1>;
}
|
5. API Routes (Your Built-in Backend)
Want to create an API inside your Next.js project? Here you go:
1
2
3
4
| // pages/api/hello.js
export default function handler(req, res) {
res.status(200).json({ message: 'Hello, API!' });
}
|
6. Using Middleware for Custom Logic
Middleware can intercept requests before they hit your pages.
1
2
3
4
5
6
7
8
| // middleware.js
import { NextResponse } from 'next/server';
export function middleware(req) {
if (!req.cookies.authToken) {
return NextResponse.redirect('/login');
}
}
|
7. Optimized Images with Next.js
Next.js has an amazing <Image />
component that auto-optimizes images.
1
2
3
4
5
| import Image from 'next/image';
export default function OptimizedImage() {
return <Image src="/example.jpg" width={500} height={500} alt="Example" />;
}
|
8. Using Edge Functions (Next.js 12+)
Run server-side code closer to the user!
1
2
3
4
5
6
| // pages/api/edge-example.js
export default async function handler(req) {
return new Response('Hello from the edge!', {
status: 200,
});
}
|
9. Incremental Static Regeneration (ISR)
Regenerate static pages dynamically without a full rebuild.
1
2
3
4
5
6
7
8
9
10
| // pages/isr-example.js
export async function getStaticProps() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts/1');
const data = await res.json();
return { props: { post: data }, revalidate: 10 };
}
export default function ISRPage({ post }) {
return <h1>{post.title}</h1>;
}
|
10. Internationalized Routing (i18n)
Easily handle multiple languages without a headache.
1
2
3
4
5
6
7
| // next.config.js
module.exports = {
i18n: {
locales: ['en', 'es', 'fr'],
defaultLocale: 'en',
},
};
|
11. Custom _app.js
for Global State Management
Want to persist global state across pages? Modify _app.js
.
1
2
3
4
5
6
7
8
9
| // pages/_app.js
import { useState } from 'react';
import '../styles/globals.css';
function MyApp({ Component, pageProps }) {
const [user, setUser] = useState(null);
return <Component {...pageProps} user={user} setUser={setUser} />;
}
export default MyApp;
|
12. Custom _document.js
for Modifying <html>
and <body>
Use _document.js
to modify the document structure.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // pages/_document.js
import { Html, Head, Main, NextScript } from 'next/document';
export default function Document() {
return (
<Html lang="en">
<Head>
<link rel="stylesheet" href="/custom.css" />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
|
13. Fetch Data on the Client Side with useEffect
If you don’t need SSR, fetch data client-side instead.
1
2
3
4
5
6
7
8
9
10
11
12
13
| import { useEffect, useState } from 'react';
export default function ClientFetch() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('/api/example')
.then((res) => res.json())
.then((data) => setData(data));
}, []);
return <div>{data ? JSON.stringify(data) : 'Loading...'}</div>;
}
|
14. Creating Protected Routes (Authentication)
Protect pages by checking authentication status.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // pages/protected.js
import { useRouter } from 'next/router';
import { useEffect } from 'react';
export default function ProtectedPage() {
const router = useRouter();
useEffect(() => {
const token = localStorage.getItem('authToken');
if (!token) router.push('/login');
}, []);
return <h1>Protected Content</h1>;
}
|
15. Custom Express Server with Next.js
Want more control? Use a custom server.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // server.js
const express = require('express');
const next = require('next');
const app = next({ dev: process.env.NODE_ENV !== 'production' });
const handle = app.getRequestHandler();
app.prepare().then(() => {
const server = express();
server.get('*', (req, res) => {
return handle(req, res);
});
server.listen(3000, () => console.log('Server running on port 3000'));
});
|
Boost SEO with dynamic <meta>
tags.
1
2
3
4
5
6
7
8
9
10
11
12
13
| import Head from 'next/head';
export default function SEOPage() {
return (
<>
<Head>
<title>SEO in Next.js</title>
<meta name="description" content="Learn SEO in Next.js" />
</Head>
<h1>SEO Page</h1>
</>
);
}
|
17. Styling with Tailwind CSS
Next.js works great with Tailwind.
1
2
3
4
5
6
7
| // Install Tailwind: npm install tailwindcss postcss autoprefixer
// Add Tailwind to globals.css
import 'tailwindcss/tailwind.css';
export default function TailwindExample() {
return <h1 className="text-4xl font-bold text-blue-500">Hello Tailwind!</h1>;
}
|
18. Using next/script
for Third-Party Scripts
Load third-party scripts efficiently.
1
2
3
4
5
6
7
8
9
10
| import Script from 'next/script';
export default function ExternalScript() {
return (
<>
<h1>Using Next.js Script</h1>
<Script src="https://example.com/script.js" strategy="lazyOnload" />
</>
);
}
|
Improve navigation speed with prefetching.
1
2
3
4
5
6
7
8
9
| import Link from 'next/link';
export default function PrefetchExample() {
return (
<Link href="/about" prefetch>
Go to About Page
</Link>
);
}
|
20. Handling 404 Pages
Customize your 404 error page.
1
2
3
4
| // pages/404.js
export default function Custom404() {
return <h1>Oops! Page Not Found 😢</h1>;
}
|
Want to add security headers or modify responses dynamically? Middleware is your best friend.
1
2
3
4
5
6
7
8
| // middleware.js
import { NextResponse } from 'next/server';
export function middleware(req) {
const res = NextResponse.next();
res.headers.set('X-Custom-Header', 'MyHeaderValue');
return res;
}
|
22. Prefetching API Requests with SWR
For blazing-fast UI updates, use SWR for API fetching with caching and revalidation.
1
2
3
4
5
6
7
8
9
10
11
12
| import useSWR from 'swr';
const fetcher = (url) => fetch(url).then((res) => res.json());
export default function Dashboard() {
const { data, error } = useSWR('/api/data', fetcher);
if (error) return <div>Failed to load</div>;
if (!data) return <div>Loading...</div>;
return <h1>Welcome, {data.user}</h1>;
}
|
23. Generating Dynamic Sitemaps
Make sure search engines love your Next.js app with a dynamic sitemap.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // pages/sitemap.xml.js
import { getServerSideProps } from 'next';
export default function Sitemap() {}
export async function getServerSideProps({ res }) {
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url><loc>https://example.com/</loc></url>
</urlset>`;
res.setHeader('Content-Type', 'text/xml');
res.write(sitemap);
res.end();
return { props: {} };
}
|
24. Optimizing Fonts with next/font
Improve performance by loading Google Fonts the Next.js way.
1
2
3
4
5
6
7
| import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });
export default function Home() {
return <h1 className={inter.className}>Optimized Fonts!</h1>;
}
|
25. Caching API Responses with revalidate
Want data that refreshes every 10 seconds? Incremental Static Regeneration (ISR) has you covered.
1
2
3
4
5
6
7
8
9
| export async function getStaticProps() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts/1');
const data = await res.json();
return {
props: { post: data },
revalidate: 10,
};
}
|
26. Creating a Custom 500 Error Page
Make sure your error pages are just as polished as the rest of your app.
1
2
3
4
| // pages/500.js
export default function Custom500() {
return <h1>Something went wrong! 🚨</h1>;
}
|
27. Enabling React Strict Mode
For better debugging and catching potential issues, enable strict mode in next.config.js
.
1
2
3
4
| // next.config.js
module.exports = {
reactStrictMode: true,
};
|
Reduce initial load times by dynamically importing components.
1
2
3
4
5
6
7
8
9
| import dynamic from 'next/dynamic';
const HeavyComponent = dynamic(() => import('../components/HeavyComponent'), {
ssr: false,
});
export default function Page() {
return <HeavyComponent />;
}
|
29. Using WebSockets in Next.js API Routes
Need real-time updates? WebSockets work inside API routes!
1
2
3
4
5
6
7
8
9
10
| // pages/api/socket.js
import { Server } from 'socket.io';
export default function handler(req, res) {
if (!res.socket.server.io) {
const io = new Server(res.socket.server);
res.socket.server.io = io;
}
res.end();
}
|
30. Customizing the Build Output Directory
Change the default .next
folder to something custom in next.config.js
.
1
2
3
4
| // next.config.js
module.exports = {
distDir: 'build',
};
|
31. Advanced Dynamic Routing with Catch-All Routes
Need a route that captures multiple segments? Catch-all routes got you covered.
1
2
3
4
5
6
7
8
9
| // pages/blog/[...slug].js
import { useRouter } from 'next/router';
export default function BlogPost() {
const router = useRouter();
const { slug } = router.query;
return <h1>Blog Post: {slug?.join(' / ')}</h1>;
}
|
32. Using Edge Functions for Lightning-Fast Requests
Deploy serverless functions at the edge for near-instant responses.
1
2
3
4
5
6
7
8
| // pages/api/edge.js
export const config = { runtime: 'edge' };
export default async function handler(req) {
return new Response(JSON.stringify({ message: 'Hello from the edge!' }), {
headers: { 'Content-Type': 'application/json' },
});
}
|
33. Optimizing API Responses with Cache-Control
Use cache headers to boost performance and reduce unnecessary requests.
1
2
3
4
5
| // pages/api/cached-data.js
export default function handler(req, res) {
res.setHeader('Cache-Control', 's-maxage=60, stale-while-revalidate');
res.json({ message: 'Cached response' });
}
|
34. Implementing Role-Based Access Control (RBAC)
Secure pages based on user roles dynamically.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // components/ProtectedRoute.js
import { useRouter } from 'next/router';
import { useEffect } from 'react';
export default function ProtectedRoute({ user, allowedRoles, children }) {
const router = useRouter();
useEffect(() => {
if (!user || !allowedRoles.includes(user.role)) {
router.push('/unauthorized');
}
}, [user, allowedRoles, router]);
return <>{children}</>;
}
|
35. Custom Webpack Configuration
Extend Next.js with a custom Webpack config.
1
2
3
4
5
6
7
8
9
| // next.config.js
module.exports = {
webpack: (config) => {
config.module.rules.push({
test: /\.md$/, use: 'raw-loader',
});
return config;
},
};
|
36. Prefetching API Data for Better UX
Preload API calls before users even navigate to a page.
1
2
3
4
5
6
7
8
9
10
11
| // pages/index.js
import Link from 'next/link';
import { useEffect } from 'react';
export default function Home() {
useEffect(() => {
fetch('/api/data'); // Prefetch API data
}, []);
return <Link href="/dashboard">Go to Dashboard</Link>;
}
|
37. Custom ESLint Rules in Next.js
Force best practices with custom ESLint rules.
1
2
3
4
5
6
7
| // .eslintrc.js
module.exports = {
rules: {
'no-console': 'warn',
'react/no-unescaped-entities': 'off',
},
};
|
38. Testing Next.js Apps with Jest and React Testing Library
Unit test your Next.js components like a pro.
1
2
3
4
5
6
7
8
| // __tests__/index.test.js
import { render, screen } from '@testing-library/react';
import Home from '../pages/index';
test('renders welcome message', () => {
render(<Home />);
expect(screen.getByText(/Welcome to Next.js!/i)).toBeInTheDocument();
});
|
39. Implementing Webhooks for Real-Time Events
Trigger actions based on external events.
1
2
3
4
5
6
7
8
9
| // pages/api/webhook.js
export default async function handler(req, res) {
if (req.method === 'POST') {
console.log('Webhook received:', req.body);
res.status(200).send('OK');
} else {
res.status(405).send('Method Not Allowed');
}
}
|
40. Handling Background Jobs with Serverless Functions
Process long-running tasks asynchronously.
1
2
3
4
5
| // pages/api/background-task.js
export default async function handler(req, res) {
setTimeout(() => console.log('Background job done!'), 5000);
res.status(200).json({ status: 'Job started' });
}
|