January 15, 2026 · 8 min read★ Featured
Next.js transforms React development by adding powerful features like server-side rendering, routing, and optimization out of the box. In this post, we'll explore what makes Next.js essential for modern web applications.
Building with React is powerful, but setting up routing, server-side rendering, and performance optimization can be overwhelming. That's where Next.js comes in as framework that handles the heavy lifting so you can focus on building great user experiences. It is a valid choice also coupled with a Wagtail CMS acting as headless backend.
Next.js isn't just another tool. It's a production-ready framework that gives you the structure and features needed for modern web development.
Before jumping into its features, it helps to understand who created Next.js and why it exists.
Next.js was created in 2016 by Vercel (formerly Zeit) as an open-source React framework. It was built to solve common challenges React developers face:
Because Next.js is built on top of React, developers familiar with React components, hooks, and state management will feel right at home. At the same time, Next.js adds framework-level features like file-based routing, API routes, and automatic optimization that make development faster and more enjoyable.
For more information, check the official Next.js documentation and React documentation.
What is a typical project structure?
A typical Next.js project (using App Router mechanism) looks like this, many folders but you have a clear explanation below:
my-nextjs-app/
├── app/
│ ├── layout.tsx
│ ├── page.tsx
│ ├── about/
│ │ └── page.tsx
│ ├── contact/
│ │ └── page.tsx
│ └── blog/
│ ├── page.tsx
│ └── [slug]/
│ └── page.tsx
├── components/
│ ├── Header.tsx
│ ├── Footer.tsx
│ └── ui/
│ ├── Button.tsx
│ └── Card.tsx
├── lib/
│ ├── utils.ts
│ └── api.ts
├── public/
│ ├── images/
│ └── favicon.ico
├── styles/
│ └── globals.css
└── next.config.js
App folder (app/)
The heart of your Next.js application. Uses file-based routing where folders and files define your URL structure automatically.
Components folder (components/)
Reusable React components organized by feature or UI elements. Keeps your code DRY (Don't Repeat Yourself).
Lib folder (lib/)
Utility functions, API clients, and shared logic. Similar to Wagtail's core app—it centralizes reusable code.
Public folder (public/)
Static assets like images, fonts, and files that are served directly without processing.
Styles folder (styles/)
Global CSS, Tailwind configuration, or CSS modules for component styling.
Config (next.config.js)
Project configuration for environment variables, image optimization, and custom webpack settings.
This structure supports scalability: as your app grows, you can add features without restructuring everything.
One of Next.js's most powerful features is file-based routing. Instead of manually configuring routes, your file structure defines your URLs.
Think of it like organizing a filing cabinet:
Think of Next.js routing like organizing folders on your computer:
/Documents, /Photos) are like route folders in app/.report.pdf, vacation.jpg) are like pages (page.tsx).This is why Next.js feels intuitive, it mirrors how you already organize information digitally.
Looking at a real situation I encountered during the development of my website, the general folder structure described above, can be translated as follows,
app/
├── page.tsx → /
├── about/
│ └── page.tsx → /about
└── blog/
├── page.tsx → /blog
└── [slug]/
└── page.tsx → /blog/[slug]
page.tsx in a folder creates that route.[slug] creates routes like /blog/getting-started-nextjs.Example component:
// app/blog/[slug]/page.tsx
export default async function BlogPost({
params,
}: {
params: { slug: string };
}) {
const post = await getPostBySlug(params.slug);
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
No router configuration needed, Next.js handles it automatically. Feels like magic but in reality the keyword [slug] used as folder name act as a promise, meaning that the page expects a parameters which is populated during rendering.
Next.js uses two types of components, each suited for different use cases:
Components render on the server, reducing JavaScript sent to the browser. This is the default in Next.js App Router—unless you specify otherwise, your components are Server Components.
When to use:
// app/blog/page.tsx (Server Component by default)
export default async function BlogPage() {
const posts = await getPosts();
return (
<div>
{posts.map((post) => (
<ArticleCard key={post.id} post={post} />
))}
</div>
);
}
Benefits:
Interactive components that run in the browser. Use the 'use client' directive at the top of your file.
When to use:
'use client';
import { useState } from 'react';
export default function SearchBar() {
const [query, setQuery] = useState('');
return (
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search posts..."
/>
);
}
A common pattern is passing functions from Server Components to Client Components as props. This lets you keep logic on the server while maintaining interactivity on the client. In the example below,
BlogPage defines the callback (handleFilter) and passes it to the Client Component FilterButtons.FilterButtons handles events, but uses server logic to compute outcomes of the event (like URLs).We obtain a clear separation,
This pattern is useful for interactive UI elements like filters, search bars, and forms in Next.js.
BlogPage// app/blog/page.tsx
import FilterButtons from '@/components/FilterButtons';
import { getPosts } from '@/lib/blog';
export default async function BlogPage({
searchParams,
}: {
searchParams: { category?: string };
}) {
const posts = await getPosts(searchParams.category);
// Server-side callback: computes the URL for a given category
const handleFilter = (category: string) => {
return `/blog?category=${category}`;
};
return (
<div>
<FilterButtons
onFilter={handleFilter}
currentCategory={searchParams.category}
/>
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
FilterButtons// components/FilterButtons.tsx (Client Component)
'use client';
import { useRouter } from 'next/navigation';
interface FilterButtonsProps {
onFilter: (category: string) => string; // callback from server
currentCategory?: string;
}
export default function FilterButtons({ onFilter, currentCategory }: FilterButtonsProps) {
const router = useRouter();
const handleClick = (category: string) => {
const url = onFilter(category); // get server-generated URL
router.push(url); // client triggers navigation
};
return (
<div>
<button
onClick={() => handleClick('tech')}
className={currentCategory === 'tech' ? 'active' : ''}
>
Tech
</button>
<button
onClick={() => handleClick('design')}
className={currentCategory === 'design' ? 'active' : ''}
>
Design
</button>
</div>
);
}
You can mix Server and Client Components! A page can be a Server Component that fetches data, while rendering Client Components for interactive parts like search bars, filters, or comment forms. The Client Component uses callbacks to trigger navigation, which causes the Server Component to refetch with new parameters.
components/blog/, components/auth/).lib/ folder for utilities: Centralize API calls, formatters, and helpers.next/image for automatic optimization and responsive images.After understanding Next.js fundamentals:
Mastering Next.js structure, routing, and rendering strategies sets the foundation for building fast, scalable, modern web applications.
Reach out via the contact form or connect on Linkedin!