Next.js: The React Framework That Handles the Hard Parts

I spent the first half of 2019 building React apps with Create React App, and it worked fine for internal tools and dashboards. But the moment I tried to build a public-facing site that needed SEO, fast initial loads, and clean URLs, I hit walls. Client-side rendering means search engines see an empty div until JavaScript loads. Routing requires installing react-router and configuring it manually. Code splitting requires dynamic imports and careful configuration. Each of these problems has a solution, but bolting them all together yourself is tedious and error-prone.
Next.js handles all of it. File-based routing, server-side rendering, static generation, API routes, image optimization, and more. It is opinionated in the ways that save you time and flexible where it matters. After building three projects with it, I do not start React projects without it anymore.
File-Based Routing
In Next.js, the pages directory is your router. Every file in pages/ becomes a route:
pages/
index.js // -> /
about.js // -> /about
blog/
index.js // -> /blog
[slug].js // -> /blog/my-post, /blog/another-post
No route configuration files. No react-router setup. You create a file, and it is a page. The [slug].js syntax creates a dynamic route where slug becomes a parameter you can access in your component. This convention removes an entire category of boilerplate from React applications.
For nested layouts, you just nest directories. For catch-all routes, use [...params].js. The mapping between your file system and your URL structure is intuitive, and new developers on the team understand the routing immediately because they can see it in the directory tree.
Static Generation with getStaticProps
Static generation (SSG) is the performance sweet spot for content that does not change on every request. Next.js builds the HTML at build time and serves it from a CDN. The page loads instantly because there is no server rendering on each request.
// pages/blog/[slug].js
export async function getStaticPaths() {
var posts = await getAllPosts();
var paths = posts.map(function(post) {
return { params: { slug: post.slug } };
});
return { paths: paths, fallback: false };
}
export async function getStaticProps(context) {
var post = await getPostBySlug(context.params.slug);
return {
props: { post: post }
};
}
export default function BlogPost(props) {
return (
<article>
<h1>{props.post.title}</h1>
<div>{props.post.content}</div>
</article>
);
}
getStaticPaths tells Next.js which slugs to generate at build time. getStaticProps fetches the data for each page. The result is pure HTML files that load fast and are perfectly crawlable by search engines. This pattern works beautifully for blogs, documentation sites, product catalogs, and any content that changes infrequently.
Server-Side Rendering with getServerSideProps
For pages where the content changes on every request or depends on the user's session, getServerSideProps renders the page on the server for each request:
// pages/dashboard.js
export async function getServerSideProps(context) {
var session = await getSession(context.req);
if (!session) {
return { redirect: { destination: '/login', permanent: false } };
}
var userData = await fetchUserData(session.userId);
return {
props: { user: userData }
};
}
export default function Dashboard(props) {
return (
<div>
<h1>Welcome back, {props.user.name}</h1>
<p>Your account balance: {props.user.balance}</p>
</div>
);
}
The key decision: use getStaticProps when the data can be computed at build time, and getServerSideProps when it must be fresh on every request. Most pages in a typical application can be statically generated, and Next.js makes it easy to mix both strategies in the same project.
API Routes
Next.js includes a built-in API layer. Any file in pages/api/ becomes a serverless API endpoint:
// pages/api/posts.js
export default async function handler(req, res) {
if (req.method === 'GET') {
var posts = await fetchPostsFromDatabase();
res.status(200).json(posts);
} else if (req.method === 'POST') {
var newPost = await createPost(req.body);
res.status(201).json(newPost);
} else {
res.setHeader('Allow', ['GET', 'POST']);
res.status(405).end('Method Not Allowed');
}
}
This eliminates the need for a separate Express server for simple backends. For a blog, a contact form, or a webhook handler, API routes are all you need. They run as serverless functions on Vercel (the company behind Next.js) or as regular Node.js handlers on other hosting platforms.
Image Optimization and Link Prefetching
Two features that seem minor but have a huge impact on user experience. The next/image component handles responsive images, lazy loading, and format conversion automatically:
import Image from 'next/image';
function ProductCard(props) {
return (
<div>
<Image
src={props.product.image}
alt={props.product.name}
width={400}
height={300}
layout="responsive"
/>
<h3>{props.product.name}</h3>
</div>
);
}
Next.js automatically resizes images, converts them to WebP where supported, and serves them from a built-in optimization endpoint. You do not need to generate multiple image sizes manually or set up a separate image CDN.
The next/link component prefetches linked pages in the background when they enter the viewport:
import Link from 'next/link';
function Navigation() {
return (
<nav>
<Link href="/about"><a>About</a></Link>
<Link href="/blog"><a>Blog</a></Link>
</nav>
);
}
When a user scrolls and the "About" link becomes visible, Next.js fetches the about page data in the background. When they click, the page appears instantly because it is already loaded. This makes the site feel like a single-page application while maintaining all the SEO benefits of server-rendered HTML.
Building a Blog Page
Here is a practical example combining several Next.js features to build a blog index page:
// pages/blog/index.js
import Link from 'next/link';
export async function getStaticProps() {
var posts = await getAllPosts();
return {
props: { posts: posts },
revalidate: 3600 // Regenerate every hour
};
}
export default function Blog(props) {
return (
<div>
<h1>Blog</h1>
{props.posts.map(function(post) {
return (
<article key={post.slug}>
<h2>
<Link href={'/blog/' + post.slug}>
<a>{post.title}</a>
</Link>
</h2>
<p>{post.excerpt}</p>
<time>{post.date}</time>
</article>
);
})}
</div>
);
}
The revalidate: 3600 option enables Incremental Static Regeneration (ISR). The page is statically generated at build time but regenerates in the background every hour. You get CDN-level performance with near-real-time content freshness, no redeploy needed.
Deployment
Deploying a Next.js app to Vercel is as simple as connecting your GitHub repo. Push to main, and it deploys. Preview deployments are created automatically for every pull request. But Next.js is not locked to Vercel. You can deploy to any Node.js hosting platform, run it in a Docker container, or export it as a fully static site with next export.
After months of using Next.js, the thing I appreciate most is how it lets me focus on building features instead of configuring infrastructure. The hard parts (routing, rendering strategies, optimization, deployment) are handled. You spend your time writing components and fetching data instead of wiring together a dozen libraries. That is the promise of a good framework, and Next.js delivers on it.