Next.js is a React framework that enables server-side rendering (SSR) and static site generation (SSG) for building fast, scalable, and SEO-friendly web applications. It is developed by Vercel and provides powerful features like:
- Server-side rendering (SSR)
- Static site generation (SSG)
- Incremental Static Regeneration (ISR)
- API routes (backend logic within the same project)
- Built-in CSS and image optimization
- Middleware for request handling
- File-based routing system (No need for React Router)
React applications built with Create React App (CRA) render everything on the client-side, meaning search engines struggle to index the page properly. Next.js solves this problem by pre-rendering pages on the server before sending them to the browser.
- SSR (Server-Side Rendering) generates HTML dynamically on each request, improving SEO.
- SSG (Static Site Generation) creates pre-rendered pages at build time, making them load faster.
π Result: Better search engine rankings and increased organic traffic.
Next.js optimizes performance by pre-rendering pages and lazy loading assets, which means:
- Users get the initial page load faster (SSR or SSG).
- Image optimization improves performance and user experience.
- Built-in code splitting reduces unnecessary JavaScript loading.
π Result: Faster websites lead to better user engagement and retention.
In a React app, we usually set up routing with react-router-dom
. However, Next.js simplifies routing with a file-based system:
π Example:
pages/
βββ index.js β Homepage ("/")
βββ about.js β About page ("/about")
βββ blog/
β βββ index.js β Blog listing ("/blog")
β βββ [id].js β Dynamic route ("/blog/:id")
Key Benefits:
β
No need for react-router-dom
β
Automatic route creation
β
Dynamic routes with [id].js
(e.g., /blog/1
, /blog/2
)
π Result: Simpler, more maintainable routing system.
Next.js allows us to create backend APIs directly within the project under the pages/api/
directory. This eliminates the need for a separate backend server in many cases.
π Example:
pages/api/
βββ hello.js β "/api/hello"
βββ users.js β "/api/users"
π API Example (pages/api/hello.js)
export default function handler(req, res) {
res.status(200).json({ message: "Hello from Next.js API!" });
}
π Result: No need for an external backend (Express.js, Node.js) in many cases.
Next.js provides two types of pre-rendering:
Feature | Server-Side Rendering (SSR) | Static Site Generation (SSG) |
---|---|---|
When HTML is generated? | On each request | At build time |
Data is fetched? | On every request | Once during build |
Use cases | Real-time data (e.g., user dashboards) | Blogs, marketing pages |
π SSR Example:
export async function getServerSideProps() {
const data = await fetch("https://api.example.com/posts").then(res => res.json());
return { props: { posts: data } };
}
π SSG Example:
export async function getStaticProps() {
const data = await fetch("https://api.example.com/posts").then(res => res.json());
return { props: { posts: data } };
}
π Result: We can choose the best rendering strategy based on project requirements.
Next.js allows us to update static pages without rebuilding the entire site.
π Example:
export async function getStaticProps() {
return { props: { time: new Date().toISOString() }, revalidate: 10 };
}
π This will regenerate the page every 10 seconds, keeping it up-to-date without affecting performance.
π Result: Perfect for blogs, e-commerce, and frequently updated pages.
Next.js provides an optimized <Image>
component that automatically resizes and optimizes images.
π Example:
import Image from 'next/image';
export default function Home() {
return <Image src="/image.jpg" width={500} height={300} alt="Example Image" />;
}
π Result: Faster image loading and better performance.
Middleware allows us to run custom logic before a request is completed.
π Example: Redirect users based on authentication:
import { NextResponse } from "next/server";
export function middleware(req) {
const loggedIn = checkUserAuth(req); // Custom function
if (!loggedIn) {
return NextResponse.redirect("/#");
}
}
π Result: More control over authentication, security, and logging.
Next.js has built-in TypeScript support, making it easier to build type-safe applications. It also integrates seamlessly with Tailwind CSS for styling.
π Enable TypeScript:
npx create-next-app@latest my-app --typescript
π Tailwind CSS in Next.js:
npm install tailwindcss postcss autoprefixer
npx tailwindcss init -p
π Result: Easier development with TypeScript and Tailwind CSS.
β
When SEO is important (blogs, e-commerce, marketing websites)
β
When performance is a priority (faster page loads)
β
When we need SSR, SSG, or ISR
β
When we want built-in API routes
β
When we want a simpler file-based routing system
β
When we need image and performance optimizations
β If we are building a small project without SEO needs (Create React App may be enough)
β If we need a backend-heavy project (better to use Express.js or Nest.js separately)
β If we don't need SSR/SSG and just want a simple React SPA
Next.js is a powerful framework that enhances React with server-side rendering, static site generation, API routes, and performance optimizations. It is ideal for SEO-friendly, fast, and scalable web applications.
When we create a new Next.js 15 app using:
npx create-next-app@latest my-next-app
or
npx create-next-app@latest my-next-app --typescript
It generates the following folder structure:
my-next-app/
βββ .next/ # Build output (generated after running `next build`)
βββ node_modules/ # Installed npm packages
βββ public/ # Static assets (images, fonts, icons, etc.)
βββ src/ # Main source folder (new in Next.js 13+)
β βββ app/ # New App Router (Next.js 13+ with React Server Components)
β β βββ layout.tsx # Root layout (persistent layout for all pages)
β β βββ page.tsx # Homepage (`/`)
β β βββ about/page.tsx # About page (`/about`)
β β βββ blog/ # Blog route
β β β βββ page.tsx # Blog listing page (`/blog`)
β β β βββ [id]/page.tsx # Dynamic route for blog (`/blog/:id`)
β β βββ api/ # API routes (`/api/*`)
β β β βββ hello.ts # Example API route (`/api/hello`)
β β βββ globals.css # Global CSS file
β β βββ layout.tsx # Root layout
β β βββ loading.tsx # Loading UI
β β βββ error.tsx # Error handling page
β βββ components/ # Reusable UI components
β βββ styles/ # CSS, Tailwind, or SCSS styles
β βββ lib/ # Utility functions, helpers, services
β βββ hooks/ # Custom React hooks
β βββ context/ # React Context API providers
βββ .env.local # Environment variables (API keys, secrets)
βββ .gitignore # Files to ignore in Git
βββ next.config.js # Next.js configuration file
βββ package.json # Project dependencies & scripts
βββ tsconfig.json # TypeScript configuration (if using TS)
βββ README.md # Project documentation
- Contains the compiled output of our project.
- Not meant to be modified manually.
- Should be ignored in Git (
.gitignore
).
- Stores all installed npm dependencies.
- Automatically created when we run
npm install
oryarn install
.
- Contains static assets like images, fonts, and icons.
- Everything in this folder is served as-is from the root (
/
).
π Example:
- If we put an image inside
public/images/logo.png
, we can access it in the browser as:/images/logo.png
- Inside React components, we use it as:
import Image from "next/image"; export default function Logo() { return <Image src="/images/logo.png" width={100} height={50} alt="Logo" />; }
New in Next.js 13+, it contains all source code and the new App Router (app/
folder).
The app/
folder introduces the new React Server Components model with file-based routing.
π Example structure:
app/
βββ layout.tsx # Root layout (applies to all pages)
βββ page.tsx # Homepage (`/`)
βββ about/
β βββ page.tsx # About page (`/about`)
βββ blog/
β βββ page.tsx # Blog listing (`/blog`)
β βββ [id]/
β β βββ page.tsx # Dynamic blog page (`/blog/:id`)
βββ api/
β βββ hello.ts # API route (`/api/hello`)
βββ globals.css # Global styles
βββ loading.tsx # Loading indicator
βββ error.tsx # Error handling page
Next.js allows us to create backend API routes inside app/api/
.
π Example: API Route (/api/hello
)
export async function GET() {
return Response.json({ message: "Hello, Next.js API!" });
}
- Reusable React components used across the project.
- Helps keep the codebase clean.
π Example: Button Component (components/Button.tsx
)
export default function Button({ label }: { label: string }) {
return <button className="bg-blue-500 text-white p-2">{label}</button>;
}
- Contains global CSS, TailwindCSS, or SCSS styles.
π Example: Global CSS (styles/globals.css
)
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
}
- Stores helper functions or services like database connections.
π Example: Helper Function (lib/fetchData.ts
)
export async function fetchData(url: string) {
const res = await fetch(url);
return res.json();
}
- Stores custom React hooks for state management.
π Example: Custom Hook (hooks/useTheme.ts
)
import { useState, useEffect } from "react";
export function useTheme() {
const [theme, setTheme] = useState("light");
useEffect(() => {
document.body.className = theme;
}, [theme]);
return { theme, setTheme };
}
- Stores global state using Reactβs Context API.
π Example: Theme Context (context/ThemeContext.tsx
)
import { createContext, useContext, useState } from "react";
const ThemeContext = createContext(null);
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
return useContext(ThemeContext);
}
- Stores API keys, database URLs, secrets.
- Example:
DATABASE_URL=mongodb+srv://username:password@cluster.mongodb.net/mydb
- Customizes Next.js settings.
- Example:
module.exports = { reactStrictMode: true, images: { domains: ["example.com"], }, };
- Stores project dependencies & scripts.
- Example:
{ "scripts": { "dev": "next dev", "build": "next build", "start": "next start" } }
Next.js 15 follows a modern folder structure with the app/
directory, making it easier to manage server components, pages, API routes, and reusable logic.
React Server Components (RSC) allow us to render React components on the server instead of the client. This helps in reducing JavaScript bundle size, improving performance, and enabling direct access to databases, APIs, and files without exposing sensitive logic to the browser.
β Key Features of RSC:
- No Client-Side JavaScript Execution β Reduces JS sent to the browser.
- Direct Database & API Calls β No need for fetching data on the client.
- Automatic Code Splitting β Only sends required data to the frontend.
- SEO-Friendly β Renders content on the server before sending it to the client.
- Improved Performance β Faster page loads as less JavaScript is processed in the browser.
React now categorizes components into two types:
1οΈβ£ Server Components β Run only on the server (default in Next.js 13+).
2οΈβ£ Client Components β Run on both the server and browser but require JavaScript.
Feature | Server Components (RSC) π₯οΈ | Client Components π₯οΈπ± |
---|---|---|
Runs on | Server only | Server + Client |
JavaScript in Browser? | β No | β Yes |
Can Fetch Data? | β Yes (Directly) | β Yes (Via useEffect) |
Can Use State (useState )? |
β No | β Yes |
Can Use Effects (useEffect )? |
β No | β Yes |
Can Use Event Handlers? | β No | β Yes (onClick, onChange, etc.) |
Bundle Size Impact | π Smaller (No JS sent) | π Larger (JS sent) |
SEO Optimization | β Better | β Worse |
β
Use Server Components when:
βοΈ Fetching data from a database or API.
βοΈ Rendering static content (e.g., blog posts, articles, products).
βοΈ Improving SEO (Pre-rendered content).
βοΈ Reducing client-side JavaScript.
β
Use Client Components when:
βοΈ Using state (useState
, useReducer
).
βοΈ Handling user interactions (onClick
, onChange
).
βοΈ Using effects (useEffect
, useRef
).
βοΈ Implementing animations (e.g., framer-motion
).
Next.js 13+ follows the App Router (app/
) pattern where all components are Server Components by default.
app/
βββ layout.tsx # Root layout (Server Component)
βββ page.tsx # Home page (Server Component)
βββ about/page.tsx # About page (Server Component)
βββ components/
β βββ Navbar.tsx # Server Component
β βββ Button.tsx # Client Component (interactive)
β βββ UserList.tsx # Server Component (fetches data)
βββ api/
β βββ hello.ts # API Route (Server Side)
- By default, all components in
app/
are Server Components in Next.js 13+. - Can fetch data directly from a database/API without client-side fetching.
π Example: Fetching Data in a Server Component
// app/components/Users.tsx (Server Component)
import React from "react";
async function fetchUsers() {
const res = await fetch("https://jsonplaceholder.typicode.com/users");
return res.json();
}
export default async function Users() {
const users = await fetchUsers();
return (
<div>
<h2>User List (Fetched on Server)</h2>
<ul>
{users.map((user: any) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
β
No useEffect
needed to fetch data.
β
No extra JavaScript sent to the client.
- Client Components must be explicitly marked using
"use client"
. - Used for interactivity, state, event handlers, and effects.
π Example: Client Component with State
// app/components/Counter.tsx
"use client"; // π Marks this as a Client Component
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increase</button>
</div>
);
}
β
Can use useState
, useEffect
, and event handlers.
β
JavaScript is sent to the browser, increasing bundle size.
- Client Components can import Server Components, but NOT vice versa.
- This allows fetching data on the server and passing it to client components.
π Example: Hybrid Approach
// app/components/UserList.tsx (Server Component)
import React from "react";
import Counter from "./Counter"; // β
Importing a Client Component
async function fetchUsers() {
const res = await fetch("https://jsonplaceholder.typicode.com/users");
return res.json();
}
export default async function UserList() {
const users = await fetchUsers();
return (
<div>
<h2>Users:</h2>
<ul>
{users.map((user: any) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
<Counter /> {/* β
Client Component for Interactivity */}
</div>
);
}
β
Users fetched on the server (no client-side fetching).
β
Counter is interactive (uses useState
).
β
Smaller Bundle Size: No unnecessary JavaScript is sent to the browser.
β
Better Performance: Fetch data on the server without affecting UI.
β
Improved SEO: Content is rendered before reaching the browser.
β
Security: Prevents exposing database queries or API calls to the client.
β No Client-Side State (useState
) β Must wrap with a Client Component.
β No Event Handlers (onClick
, onChange
) β Must wrap with a Client Component.
β No useEffect
, useRef
, useContext
β Only works in Client Components.
Feature | Server Component (Default) | Client Component ("use client" ) |
---|---|---|
State (useState ) |
β No | β Yes |
Event Handlers (onClick ) |
β No | β Yes |
Fetch Data | β Yes (Directly) | β
Yes (useEffect ) |
SEO Optimization | β Yes | β No |
Bundle Size Impact | β Small | π Larger |
- Next.js defaults to Server Components, making it easier to fetch data, optimize performance, and improve SEO.
- Client Components are only needed for state, events, and interactivity.
- The best approach is to mix both β Fetch data on the server, then pass it to a Client Component for interactions.
Next.js 15 uses the App Router (app/
), which is a file-based routing system. This makes navigation in Next.js easier and more powerful.
- The App Router (
app/
) replaces the old Pages Router (pages/
). - Routing is based on the file structure inside the
app/
directory. - Every file named
page.tsx
orpage.jsx
automatically becomes a route. - Nested directories create nested routes.
π Example Folder Structure:
app/
βββ layout.tsx # Root Layout (Shared UI)
βββ page.tsx # Home Page β "/"
βββ about/
β βββ page.tsx # About Page β "/about"
βββ blog/
β βββ page.tsx # Blog Page β "/blog"
β βββ [id]/ # Dynamic Route
β β βββ page.tsx # Blog Post β "/blog/:id"
βββ dashboard/
β βββ layout.tsx # Dashboard Layout
β βββ page.tsx # Dashboard Home β "/dashboard"
β βββ settings/
β β βββ page.tsx # Dashboard Settings β "/dashboard/settings"
β
Each page.tsx
represents a route
β
Folders represent URL structure
β
Dynamic routing ([id]
) is supported
Routes in Next.js 15 are defined using page.tsx
inside folders.
π Example: Home Page (/
)
// app/page.tsx
export default function HomePage() {
return <h1>Welcome to Next.js 15!</h1>;
}
π Example: About Page (/about
)
// app/about/page.tsx
export default function AboutPage() {
return <h1>About Us</h1>;
}
β Simple, file-based routing without extra configuration
Next.js 15 allows us to share layouts between multiple pages using layout.tsx
.
π Example: Shared Layout for Dashboard
app/
βββ dashboard/
β βββ layout.tsx # Shared layout for dashboard
β βββ page.tsx # "/dashboard"
β βββ settings/
β β βββ page.tsx # "/dashboard/settings"
π dashboard/layout.tsx
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div>
<nav>Dashboard Navbar</nav>
<main>{children}</main>
</div>
);
}
π dashboard/page.tsx
export default function DashboardPage() {
return <h1>Dashboard Home</h1>;
}
π dashboard/settings/page.tsx
export default function DashboardSettings() {
return <h1>Dashboard Settings</h1>;
}
β
Navbar will persist across all dashboard pages
β
Shared layouts improve code reuse
We can create dynamic routes by wrapping a folder name in square brackets [ ]
.
π Example: Blog Post Route (/blog/:id
)
app/
βββ blog/
β βββ [id]/ # Dynamic Route
β β βββ page.tsx
π blog/[id]/page.tsx
export default function BlogPost({ params }: { params: { id: string } }) {
return <h1>Blog Post ID: {params.id}</h1>;
}
β
Access dynamic parameters using { params.id }
β
/blog/1
β Shows "Blog Post ID: 1"
β
/blog/nextjs
β Shows "Blog Post ID: nextjs"
If we need a route that matches multiple segments, use [[...slug]]
.
π Example: Catch-All Route
app/
βββ docs/
β βββ [[...slug]]/
β β βββ page.tsx
π docs/[[...slug]]/page.tsx
export default function DocsPage({ params }: { params: { slug?: string[] } }) {
return <h1>Docs: {params.slug ? params.slug.join("/") : "Home"}</h1>;
}
β
/docs/
β "Docs: Home"
β
/docs/nextjs/15
β "Docs: nextjs/15"
Use next/link
for client-side navigation.
π Example: Navbar with Links
import Link from "next/link";
export default function Navbar() {
return (
<nav>
<Link href="/">Home</Link>
<Link href="/about">About</Link>
<Link href="/blog/1">Blog Post 1</Link>
</nav>
);
}
β Fast navigation without page reloads
We can create API routes directly inside the app/api/
folder.
π Example: API Route (/api/hello
)
app/
βββ api/
β βββ hello/
β β βββ route.ts
π api/hello/route.ts
export async function GET() {
return new Response(JSON.stringify({ message: "Hello from API!" }), {
headers: { "Content-Type": "application/json" },
});
}
β
Visit /api/hello
to get { "message": "Hello from API!" }
Middleware allows us to modify requests before they reach a route.
π Example: Redirect /old
to /new
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname === "/old") {
return NextResponse.redirect(new URL("/new", request.url));
}
}
β Used for authentication, redirects, logging, etc.
Feature | App Router (Next.js 15) |
---|---|
Routing Type | File-based routing |
Nested Routes | β Yes |
Dynamic Routes ([id] ) |
β Yes |
API Routes (app/api/ ) |
β Yes |
Middleware | β Yes |
Layouts | β Yes |
Navigation (next/link ) |
β Yes |
- Next.js 15 automatically creates routes based on the
app/
directory. - Layouts make it easy to reuse UI across multiple pages.
- Dynamic routes allow flexible URL handling (
/blog/:id
). - API routes can handle backend logic without needing a separate server.
- Client-side navigation (
next/link
) ensures fast performance.
Catch-all segments in Next.js 15 allow us to capture multiple URL segments in a single dynamic route. This is useful for handling flexible or deeply nested paths without defining each one manually.
To create a catch-all route, we wrap a folder name inside square brackets with three dots ([...]
).
π Example: Catch-All Route (/docs/*
)
app/
βββ docs/
β βββ [...slug]/
β β βββ page.tsx
π app/docs/[...slug]/page.tsx
export default function DocsPage({ params }: { params: { slug?: string[] } }) {
return <h1>Docs Path: {params.slug ? params.slug.join("/") : "Home"}</h1>;
}
β Routes and Their Outputs
URL | params.slug |
Output |
---|---|---|
/docs |
undefined |
Docs Path: Home |
/docs/nextjs |
["nextjs"] |
Docs Path: nextjs |
/docs/nextjs/15 |
["nextjs", "15"] |
Docs Path: nextjs/15 |
/docs/nextjs/15/features |
["nextjs", "15", "features"] |
Docs Path: nextjs/15/features |
πΉ params.slug
will always be an array of strings, representing the URL segments.
If we want the route to match even when no segments are provided, we use double square brackets ([[...slug]]
).
π Example: Handling /docs
as well
app/
βββ docs/
β βββ [[...slug]]/
β β βββ page.tsx
π app/docs/[[...slug]]/page.tsx
export default function DocsPage({ params }: { params: { slug?: string[] } }) {
return <h1>Docs: {params.slug ? params.slug.join("/") : "Home"}</h1>;
}
β
Difference Between [...]
and [[...]]
URL | [...] (Required) |
[[...]] (Optional) |
---|---|---|
/docs |
β 404 Error | β "Docs: Home" |
/docs/nextjs |
β "Docs: nextjs" | β "Docs: nextjs" |
πΉ Use [[...slug]]
when the route should work without extra segments.
We can use catch-all segments to generate breadcrumbs dynamically.
π Example: Breadcrumb Component
export default function Breadcrumbs({ params }: { params: { slug?: string[] } }) {
const path = params.slug || [];
return (
<nav>
<ul>
<li><a href="/">Home</a></li>
{path.map((segment, index) => {
const href = "/" + path.slice(0, index + 1).join("/");
return <li key={href}><a href={href}>{segment}</a></li>;
})}
</ul>
</nav>
);
}
β
Visiting /docs/nextjs/15
shows:
Home > docs > nextjs > 15
Feature | [...] (Required) |
[[...]] (Optional) |
---|---|---|
Captures multiple segments | β Yes | β Yes |
Works without segments (/docs ) |
β No (404) | β Yes |
Returns params.slug as an array |
β Yes | β Yes |
π Use cases:
- Dynamic documentation pages (
/docs/[...slug]
) - E-commerce categories (
/products/[...category]
) - Breadcrumb navigation
- URL rewriting and redirection handling
In Next.js 15 (App Router), we can implement custom 404 Not Found error pages in different ways, depending on the context. Let's go through each approach in detail.
If a user visits a non-existing route, we can create a global not-found.tsx
inside the app
directory.
π Example: app/not-found.tsx
export default function NotFound() {
return (
<div className="flex flex-col items-center justify-center h-screen">
<h1 className="text-4xl font-bold">404 - Page Not Found</h1>
<p className="text-lg mt-4">Sorry, we couldnβt find the page youβre looking for.</p>
<a href="/" className="mt-6 px-4 py-2 bg-blue-600 text-white rounded">Go Home</a>
</div>
);
}
β When is this triggered?
- If the user visits a route that does not exist (
/random-page
β 404). - Works automatically when a page is not found.
We can use Next.jsβs built-in notFound()
function inside a route to conditionally trigger a 404 page.
π Example: app/products/[id]/page.tsx
import { notFound } from "next/navigation";
export default function ProductPage({ params }: { params: { id: string } }) {
const validProducts = ["101", "102", "103"]; // Fake product list
if (!validProducts.includes(params.id)) {
notFound(); // Triggers the 404 page
}
return <h1>Product ID: {params.id}</h1>;
}
β When is this triggered?
- If the user visits
/products/999
(an invalid product), it will redirect them to the 404 page.
If we are fetching data from an API, we can return notFound()
when no data exists.
π Example: Fetching user data from an API
import { notFound } from "next/navigation";
async function getUser(id: string) {
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
if (!res.ok) {
notFound(); // Redirects to the 404 page if user is not found
}
return res.json();
}
export default async function UserPage({ params }: { params: { id: string } }) {
const user = await getUser(params.id);
return <h1>User: {user.name}</h1>;
}
β When is this triggered?
- If we visit
/users/999
and the API returns a 404, it will automatically show our custom 404 page.
We can also handle 404 pages inside layouts by checking the route params, but this is not recommended because it might cause unwanted redirects.
π Example: app/products/layout.tsx
import { notFound } from "next/navigation";
export default function ProductLayout({ children, params }: { children: React.ReactNode, params: { id: string } }) {
const validProducts = ["101", "102", "103"];
if (!validProducts.includes(params.id)) {
notFound();
}
return <>{children}</>;
}
β When is this triggered?
- If a product is invalid, it redirects all subpages (
/products/999
) to 404.
Method | Use Case | Example |
---|---|---|
Global not-found.tsx |
Shows a global 404 page for non-existing routes | app/not-found.tsx |
notFound() inside a page |
Conditionally show 404 when data is missing | app/products/[id]/page.tsx |
API-based 404 Handling | If an API request fails, trigger notFound() |
Fetching user/product data |
404 in Layouts (Not Recommended) | Handling 404 inside layouts for groups of pages | app/products/layout.tsx |
β
Use not-found.tsx
β For a global 404 page.
β
Use notFound()
in pages β When handling dynamic routes or data fetching errors.
β
Use API-based 404 Handling β When fetching external data and the resource is missing.
usePathname
is a React Server Component (RSC) hook in Next.js 15 that lets us access the current URL path in a client component. Itβs useful when we need to:
β
Show active navigation styles
β
Conditionally render UI based on the route
β
Generate breadcrumbs
β
Perform analytics or logging
π Importing usePathname
import { usePathname } from "next/navigation";
We can use usePathname
to get the current URL path and display it in our component.
π Example: components/CurrentPath.tsx
"use client"; // Required for usePathname
import { usePathname } from "next/navigation";
export default function CurrentPath() {
const pathname = usePathname();
return (
<div>
<h1>Current Path: {pathname}</h1>
</div>
);
}
β
Visiting /about
will display:
Current Path: /about
We can use usePathname
to apply active styles to the current page link.
π Example: components/Navbar.tsx
"use client";
import { usePathname } from "next/navigation";
import Link from "next/link";
export default function Navbar() {
const pathname = usePathname();
return (
<nav className="flex space-x-4">
{["/", "/about", "/contact"].map((path) => (
<Link
key={path}
href={path}
className={`px-4 py-2 ${
pathname === path ? "bg-blue-500 text-white" : "text-gray-700"
}`}
>
{path === "/" ? "Home" : path.replace("/", "").toUpperCase()}
</Link>
))}
</nav>
);
}
β
If we visit /about
, the About link is highlighted.
[ Home ] [ ABOUT ] [ Contact ]
πΉ Only the active link gets the blue background.
We can use usePathname
to generate breadcrumb links dynamically.
π Example: components/Breadcrumbs.tsx
"use client";
import { usePathname } from "next/navigation";
import Link from "next/link";
export default function Breadcrumbs() {
const pathname = usePathname();
const pathSegments = pathname.split("/").filter(Boolean);
return (
<nav className="mt-4">
<ul className="flex space-x-2">
<li><Link href="/">Home</Link></li>
{pathSegments.map((segment, index) => {
const href = "/" + pathSegments.slice(0, index + 1).join("/");
return (
<li key={href}>
/ <Link href={href} className="text-blue-500">{segment}</Link>
</li>
);
})}
</ul>
</nav>
);
}
β
Visiting /docs/nextjs/hooks
generates:
Home / docs / nextjs / hooks
We can use usePathname
with useEffect
to redirect users under certain conditions.
π Example: Redirect to login if not authenticated
"use client";
import { usePathname } from "next/navigation";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
export default function AuthRedirect() {
const pathname = usePathname();
const router = useRouter();
const isAuthenticated = false; // Assume user is not logged in
useEffect(() => {
if (!isAuthenticated && pathname !== "/#") {
router.push("/#");
}
}, [pathname]);
return null;
}
β
If the user is not logged in and tries to visit /dashboard
, they will be redirected to /#
.
Feature | Use Case | Example |
---|---|---|
Get Current Path | Show path in UI | usePathname() |
Highlight Active Link | Change nav styles | Navbar component |
Breadcrumbs | Generate links from URL | /docs/nextjs/hooks β docs > nextjs > hooks |
Redirect Users | Protect routes | Redirect to /# if not authenticated |
- β
Use
usePathname
in client components ("use client"
is required). - β It works well for UI-based logic (menus, breadcrumbs, redirects).
- β Donβt use it inside Server Components, since it only works in client-side rendering.
Next.js 15 introduces private folders and file colocation as best practices for organizing components, utilities, and other project files. These concepts help improve structure, maintainability, and scalability in Next.js projects. Letβs explore them in detail.
Private folders are directories that Next.js does not treat as routes. They are used to store reusable components, utilities, styles, or constants without exposing them as pages or API routes.
- Any folder prefixed with an underscore (
_
) is ignored by Next.js as a route. - Next.js will not generate a route for these folders.
- This is useful for storing helper functions, components, or shared utilities inside the
app
directory.
app
βββ _components/ β (Reusable components, ignored as a route)
β βββ Button.tsx
β βββ Navbar.tsx
βββ _utils/ β (Helper functions, ignored as a route)
β βββ formatDate.ts
β βββ fetchData.ts
βββ page.tsx β (Home Page)
βββ about/page.tsx β (About Page)
β
Files inside _components
and _utils
will not create pages or routes.
import Navbar from "../_components/Navbar"; // β
Allowed (Not a route)
import { formatDate } from "../_utils/formatDate"; // β
Allowed
export default function Home() {
return (
<div>
<Navbar />
<p>{formatDate(new Date())}</p>
</div>
);
}
File colocation is the practice of keeping related files together inside the same directory. This improves maintainability by grouping:
β
Components related to a page
β
Styles specific to a page
β
Utility functions for a page
app
βββ blog/ β (Blog page route)
β βββ page.tsx β (Main Blog Page)
β βββ Post.tsx β (Post Component)
β βββ post.module.css β (CSS specific to blog posts)
β βββ fetchPosts.ts β (API function to fetch blog posts)
β
Everything related to the blog is inside blog/
instead of spreading files across different directories.
import Post from "./Post"; // β
Colocated Component
import styles from "./post.module.css"; // β
Colocated Styles
import { fetchPosts } from "./fetchPosts"; // β
Colocated Utility
export default async function BlogPage() {
const posts = await fetchPosts();
return (
<div className={styles.container}>
{posts.map((post) => (
<Post key={post.id} title={post.title} />
))}
</div>
);
}
Feature | Benefit |
---|---|
Private Folders (_folder ) |
Prevents unintended routing & keeps project clean |
File Colocation | Improves maintainability by grouping related files together |
Performance | Keeps imports optimized and reduces unnecessary file lookups |
Scalability | Easier to manage large projects with well-structured folders |
- β
Private folders (
_components
,_utils
) prevent Next.js from creating unwanted routes. - β File colocation keeps all relevant files in one place, improving project structure.
- β Best practice: Combine both to create a well-organized Next.js project.
Route Groups in Next.js 15 allow us to organize routes without affecting the URL structure. They help in:
β
Structuring large projects
β
Grouping related pages
β
Improving code maintainability
β
Keeping URLs clean (the group name does not appear in the URL)
- Route groups are created by wrapping a folder name inside parentheses
(group-name)
. - Next.js ignores the group name in the URL, but the folder structure helps organize the project.
app
βββ (marketing)/ β (Route Group for marketing pages)
β βββ about/page.tsx β (Accessible at `/about`)
β βββ contact/page.tsx β (Accessible at `/contact`)
βββ (dashboard)/ β (Route Group for dashboard pages)
β βββ page.tsx β (Accessible at `/dashboard`)
β βββ settings/page.tsx β (Accessible at `/dashboard/settings`)
βββ page.tsx β (Home Page `/`)
β
Even though about
and contact
are inside (marketing)
,
they are accessible at /about
and /contact
, not /marketing/about
.
β The route group name is ignored in the URL.
Each route group can have its own layout to wrap pages under it.
app
βββ (dashboard)/
β βββ layout.tsx β (Layout for dashboard pages)
β βββ page.tsx β (Accessible at `/dashboard`)
β βββ settings/page.tsx β (Accessible at `/dashboard/settings`)
βββ page.tsx β (Home Page `/`)
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div className="dashboard-layout">
<nav>Dashboard Navbar</nav>
<main>{children}</main>
</div>
);
}
β
All pages inside (dashboard)
will use this layout.
β
Visiting /dashboard/settings
automatically includes the Dashboard layout.
We can separate protected routes (dashboard, admin panel) from public routes (home, about).
app
βββ (public)/
β βββ page.tsx β (Accessible at `/`)
β βββ about/page.tsx β (Accessible at `/about`)
βββ (auth)/
β βββ login/page.tsx β (Accessible at `/#`)
β βββ register/page.tsx β (Accessible at `/register`)
βββ (dashboard)/
β βββ layout.tsx β (Protected Layout)
β βββ page.tsx β (Accessible at `/dashboard`)
β βββ settings/page.tsx β (Accessible at `/dashboard/settings`)
import { redirect } from "next/navigation";
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
const isAuthenticated = false; // Assume user is not logged in
if (!isAuthenticated) {
redirect("/#"); // Redirect to login if not authenticated
}
return <div className="dashboard-layout">{children}</div>;
}
β
If the user is not logged in, they are redirected to /#
.
β
The public and auth pages remain accessible to everyone.
Route groups also work with API routes inside the app/api/
directory.
app
βββ (api)/
β βββ users/route.ts β (API at `/api/users`)
β βββ posts/route.ts β (API at `/api/posts`)
export async function GET() {
return Response.json([{ id: 1, name: "John Doe" }]);
}
β
Accessible at /api/users
β
The (api)
group is ignored in the URL.
Feature | Benefit |
---|---|
Route Groups | Organize files without affecting URLs |
Clean URLs | Folder names in ( ) are ignored in the final route |
Scoped Layouts | Different layouts for different route groups |
Auth Separation | Public, private, and API routes can be structured cleanly |
API Routes | Route groups also work for API endpoints |
- β Use Route Groups to structure large Next.js apps without messing up URLs.
- β Great for organizing dashboard layouts, public/auth pages, and API routes.
- β Easy to separate concerns while keeping a clean URL structure.
In Next.js 15, layout.tsx
is used to define persistent UI elements that wrap around multiple pages.
Layouts help us avoid code duplication and keep a consistent structure across multiple pages.
- Any
layout.tsx
file inside a folder automatically wraps all pages inside that folder. - Layouts can be nested, meaning child layouts inherit parent layouts.
- Useful for headers, sidebars, authentication layouts, dashboards, and more.
app
βββ layout.tsx β (Global Layout for the entire app)
βββ page.tsx β (Home Page)
βββ about/page.tsx β (About Page)
βββ dashboard/
β βββ layout.tsx β (Dashboard-specific Layout)
β βββ page.tsx β (Dashboard Home)
β βββ settings/page.tsx β (Dashboard Settings)
β
layout.tsx
inside app/
applies to the entire app.
β
layout.tsx
inside dashboard/
only applies to dashboard pages.
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<header>π My Website Header π</header>
<main>{children}</main>
<footer>Β© 2025 My Website</footer>
</body>
</html>
);
}
β
Applies to all pages (page.tsx
) inside app/
.
β
Ensures all pages have the same header and footer.
- A
layout.tsx
inside a folder only applies to pages within that folder. - Child layouts inherit parent layouts automatically.
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div className="dashboard-container">
<nav>π Dashboard Sidebar</nav>
<section>{children}</section>
</div>
);
}
β
This layout wraps all pages in app/dashboard/
.
β
Visiting /dashboard
or /dashboard/settings
includes this layout.
app
βββ layout.tsx β (Global Layout)
βββ page.tsx β (Home Page)
βββ dashboard/
β βββ layout.tsx β (Dashboard Layout)
β βββ page.tsx β (Dashboard Home)
β βββ settings/
β βββ layout.tsx β (Settings Layout)
β βββ page.tsx β (Settings Page)
Route | Layout Applied |
---|---|
/ |
app/layout.tsx |
/dashboard |
app/layout.tsx + app/dashboard/layout.tsx |
/dashboard/settings |
app/layout.tsx + app/dashboard/layout.tsx + app/dashboard/settings/layout.tsx |
Layouts receive a children
prop, but we can also pass additional props.
export default function RootLayout({ children }: { children: React.ReactNode }) {
const theme = "dark"; // Example: Theme state
return (
<html lang="en">
<body className={theme}>
{children}
</body>
</html>
);
}
β Useful for themes, authentication state, and global context providers.
- Layouts are great places to wrap our app with context providers (e.g., Theme, Auth).
- This ensures all pages inside the layout have access to these providers.
import { AuthProvider } from "@/context/AuthContext";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<AuthProvider>
{children} {/* All pages inside get access to Auth */}
</AuthProvider>
</body>
</html>
);
}
β
All pages can now access authentication state from AuthContext
.
Layouts can define metadata for all pages inside them using generateMetadata
.
export const metadata = {
title: "My Next.js App",
description: "This is an amazing Next.js app",
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
β All pages inside this layout automatically get this metadata.
Feature | Benefit |
---|---|
Global Layout (app/layout.tsx ) |
Wraps the entire app (header, footer, themes) |
Nested Layouts | Scoped layouts for dashboards, settings, etc. |
Layout Inheritance | Child layouts inherit parent layouts automatically |
Context Providers in Layouts | Manage authentication, themes, state globally |
Metadata in Layouts | Define SEO-friendly metadata for all pages in a layout |
- β
layout.tsx
helps us structure UI consistently across multiple pages. - β Nested layouts allow different sections to have unique designs.
- β Great for global providers, authentication, and theming.
Absolutely! Letβs deep dive into Routing Metadata in Next.js 15, which is an essential part of modern SEO, social sharing, accessibility, and performance.
Metadata in Next.js 15 refers to extra information (like title, description, Open Graph data, Twitter cards, theme color, etc.) that gets injected into your HTML <head>
section on a per-page or per-route basis. This is done using either a static metadata
object or a dynamic generateMetadata()
function.
It helps search engines, social platforms, and browsers understand and display your pages properly.
You can define metadata in:
app/layout.tsx
β Applies to all routes.app/page.tsx
β Applies only to a single route/page.app/blog/[slug]/page.tsx
β Dynamically generate metadata per route.- Nested layouts can also define their own metadata.
export const metadata = {
title: "Home | My Awesome App",
description: "This is the homepage of our cool app.",
};
export default function HomePage() {
return <h1>Welcome to the Home Page</h1>;
}
β This will inject:
<title>Home | My Awesome App</title>
<meta name="description" content="This is the homepage of our cool app." />
When you need metadata that depends on dynamic data (like a blog post title from a CMS or DB), you use generateMetadata()
.
type Props = {
params: { slug: string }
}
export async function generateMetadata({ params }: Props) {
const post = await getPostData(params.slug);
return {
title: `${post.title} | My Blog`,
description: post.summary,
openGraph: {
title: post.title,
description: post.summary,
images: [post.coverImage],
},
};
}
export default function BlogPostPage({ params }: Props) {
return <div>Blog post content for {params.slug}</div>;
}
β This lets us dynamically set metadata for each blog post!
Metadata Field | Purpose |
---|---|
title |
Sets the HTML <title> |
description |
Meta description tag |
keywords |
SEO keywords (less used today) |
robots |
Controls crawling/indexing |
themeColor |
Sets browser theme color |
viewport |
Controls mobile scaling |
openGraph |
Metadata for social sharing (FB, LinkedIn) |
twitter |
Twitter-specific metadata |
icons |
App icons |
appleWebApp , manifest , archives , etc. |
PWA-related and advanced options |
Just like layouts, metadata declared in a parent layout is inherited by children, unless overridden.
Example:
export const metadata = {
title: "My App",
description: "A universal layout for all routes",
};
Now, any page.tsx
under this layout inherits this metadata unless it overrides it.
These two often work together for static site generation (SSG) of dynamic routes.
export async function generateStaticParams() {
const posts = await fetchAllPosts();
return posts.map(post => ({ slug: post.slug }));
}
Then use generateMetadata()
to define metadata for each slug!
You're building an eCommerce product page.
export async function generateMetadata({ params }) {
const product = await fetchProductById(params.productId);
return {
title: `${product.name} - Buy Now!`,
description: product.description,
openGraph: {
title: product.name,
description: product.description,
images: [product.imageUrl],
},
twitter: {
card: "summary_large_image",
title: product.name,
description: product.description,
images: [product.imageUrl],
}
};
}
β This makes your product page SEO-friendly and social-media-ready π
- We cannot use hooks (like
useState
oruseEffect
) insidegenerateMetadata()
, since it runs on the server. - Keep metadata as lightweight and static as possible unless you really need dynamic fetching.
- Store reusable metadata templates in a utility file for DRYness.
Feature | Explanation |
---|---|
metadata |
Static metadata object |
generateMetadata() |
Dynamic metadata based on route params or fetched data |
Layout metadata | Inherited by child routes |
Dynamic routes | Supported with metadata via params |
SEO & social | Supports OG tags, Twitter cards, canonical tags |
Hereβs a complete, ready-to-use template for handling metadata in a Next.js 15 project β including static and dynamic metadata with Open Graph and Twitter support, ideal for SEO, PWA, and social sharing.
Letβs assume the following structure:
app/
βββ layout.tsx β Root layout (with global metadata)
βββ page.tsx β Homepage
βββ about/
β βββ page.tsx β Static metadata example
βββ blog/
β βββ [slug]/
β βββ page.tsx β Dynamic metadata example
This applies globally to all routes unless overridden.
// app/layout.tsx
import "./globals.css";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: {
default: "MyApp",
template: "%s | MyApp",
},
description: "A modern web app built with Next.js 15",
keywords: ["Next.js", "Web App", "SEO", "React"],
authors: [{ name: "Skyy Banerjee", url: "https://skyybbanerjee.dev" }],
icons: {
icon: "/favicon.ico",
},
themeColor: "#ffffff",
openGraph: {
type: "website",
url: "https://myapp.com",
title: "MyApp",
description: "Explore the best modern web experience",
siteName: "MyApp",
images: [
{
url: "/og-image.png",
width: 1200,
height: 630,
},
],
},
twitter: {
card: "summary_large_image",
title: "MyApp",
description: "Explore the best modern web experience",
images: ["/og-image.png"],
creator: "@skyybbanerjee",
},
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
// app/about/page.tsx
export const metadata = {
title: "About Us",
description: "Learn more about our mission and team.",
};
export default function AboutPage() {
return <h1>About Us</h1>;
}
// app/blog/[slug]/page.tsx
type Props = {
params: { slug: string };
};
async function getPostData(slug: string) {
// Simulate DB or CMS call
return {
title: `Blog: ${slug.replace("-", " ")}`,
description: "This is a dynamic blog post.",
imageUrl: `https://myapp.com/images/${slug}.jpg`,
};
}
export async function generateMetadata({ params }: Props) {
const post = await getPostData(params.slug);
return {
title: post.title,
description: post.description,
openGraph: {
title: post.title,
description: post.description,
images: [post.imageUrl],
},
twitter: {
card: "summary_large_image",
title: post.title,
description: post.description,
images: [post.imageUrl],
},
};
}
export default function BlogPost({ params }: Props) {
return <div>This is the blog post for: {params.slug}</div>;
}
To control indexing:
export const metadata = {
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
noimageindex: false,
},
},
};
We can scale this template into anything we need! π
Absolutely Skyy! Letβs dive into the Link
component in Next.js 15 β a core feature for client-side navigation in your app. Itβs lightweight, fast, and integrated with the new App Router system.
In Next.js 15, the Link
component (from next/link
) is used for navigating between routes. It enables client-side navigation, meaning that clicking a link doesnβt cause a full page reload β just the necessary route is loaded using JavaScript. This leads to faster page transitions and preserves state like scroll position (if configured).
import Link from 'next/link';
<Link href="/about">About Us</Link>
This will render:
<a href="/about">About Us</a>
β React handles routing without reloading the browser.
The Link
component:
- Uses the HTML
<a>
tag under the hood. - Intercepts clicks and loads pages via client-side navigation.
- Automatically prefetches the linked page in the background (when visible in viewport).
- Is compatible with dynamic routes, Route Groups, Catch-all routes, and Layouts in the App Router.
Feature | Description |
---|---|
href |
Required. Route path or URL to navigate to. |
prefetch |
Prefetch the linked page in the background. Enabled by default in production. |
replace |
Replaces history instead of pushing (like router.replace() ) |
scroll |
Scroll to top on navigation (default: true ) |
as |
Optional. Used in dynamic routes to mask the URL |
Nested elements | Now allowed! You can wrap complex JSX inside Link |
You can now do this (allowed in App Router):
<Link href="/products/shoes">
<div className="p-4 rounded hover:bg-gray-100">
<h2>Shoes</h2>
<p>See our latest shoes collection.</p>
</div>
</Link>
β
This was not possible in earlier versions unless you manually wrapped it with <a>
.
For a dynamic route like /blog/[slug]
:
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
You can also pass an object:
<Link href={{ pathname: '/blog/[slug]', query: { slug: post.slug } }}>
{post.title}
</Link>
By default, navigation pushes to browser history. If you want to replace the current route:
<Link href="/dashboard" replace>Go to Dashboard</Link>
By default, prefetching is enabled in production. You can disable it:
<Link href="/contact" prefetch={false}>Contact</Link>
β This saves bandwidth if the route is unlikely to be visited.
If you donβt want to scroll to top on navigation:
<Link href="/faq" scroll={false}>FAQ</Link>
Use plain <a>
tags for external links:
<a href="https://github.com/skyybbanerjee" target="_blank" rel="noopener noreferrer">
GitHub
</a>
π Always use rel="noopener noreferrer"
for security with target="_blank"
.
You can highlight the current link manually using usePathname()
:
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
export default function NavLink({ href, children }: { href: string, children: React.ReactNode }) {
const pathname = usePathname();
const isActive = pathname === href;
return (
<Link href={href} className={isActive ? "text-blue-600 font-bold" : "text-gray-700"}>
{children}
</Link>
);
}
Prop | Description |
---|---|
href |
Path to navigate to |
replace |
Replaces the current history entry |
prefetch |
Enables/disables preloading the route |
scroll |
Controls scroll-to-top behavior |
as |
Used for masking dynamic URLs |
children |
Can be text, JSX, or elements (like cards) |
Next.js 15, like 13/14 in the App Router, uses file-based routing and introduces React Server Components (RSC) by default.
When a page or layout is rendered, we can access:
- Comes from dynamic segments in the file system.
- Example: In
app/articles/[articleId]/page.tsx
,articleId
is a param.
- Comes from the URL query string, like
?lang=en
. - These donβt require any special
[param]
file setup β theyβre parsed automatically.
You can use them directly as props in your page or layout:
export default function Page({ params, searchParams }) {
console.log(params.articleId);
console.log(searchParams.lang);
}
Since Next.js doesnβt allow async props by default, we wrap them in a Promise
and use the use()
hook to resolve them.
<Link href="/articles/breaking-news-123?lang=en">Read in ENGLISH</Link>
<Link href="/articles/breaking-news-123?lang=fr">Lire en FRANΓAIS</Link>
Here:
- We're linking to a dynamic route like
/articles/[articleId]
breaking-news-123
β becomes a param (articleId
)?lang=en
β becomes a search param
"use client";
import { use } from "react"; // for async props
function NewsArticle({
params,
searchParams,
}: {
params: Promise<{ articleId: string }>;
searchParams: Promise<{ lang?: "en" | "es" | "fr" | "de" }>;
}) {
const { articleId } = use(params); // resolves to { articleId: "breaking-news-123" }
const { lang = "en" } = use(searchParams); // resolves to { lang: "fr" } etc.
We're correctly:
- Using the
use()
hook to await asyncparams
andsearchParams
props. - Getting the
articleId
from the route:/articles/[articleId]
- Getting the
lang
from query string like?lang=en
So:
URL | params.articleId |
searchParams.lang |
---|---|---|
/articles/breaking-news-123?lang=en |
"breaking-news-123" |
"en" |
/articles/breaking-news-123?lang=fr |
"breaking-news-123" |
"fr" |
- In our route folder:
app/articles/[articleId]/page.tsx
or
app/articles/[articleId]/NewsArticle.tsx
(if used as client comp)
Next.js automatically passes:
params = { articleId: "..." }
searchParams = { lang: "..." }
If you're using a client component here, the props are promises. Thatβs why you use:
const { articleId } = use(params);
const { lang } = use(searchParams);
const supportedLangs = ["en", "fr", "es", "de"];
const { lang = "en" } = use(searchParams);
if (!supportedLangs.includes(lang)) {
// show 404 or redirect
}
Concept | Meaning | Where it comes from |
---|---|---|
params |
Values from [param] in file structure |
URL path |
searchParams |
Values from ?key=value in URL query |
URL query string |
use() hook |
Needed in Client Components to resolve props | params and searchParams |
Instead of using <Link href="...">
, programmatic navigation lets us navigate between routes using JavaScript logic, like:
- After a form submission β
- On button click β
- Based on conditions or auth β
In the App Router, we use the useRouter()
hook from next/navigation
inside Client Components to navigate programmatically.
"use client"; // must be a client component
import { useRouter } from "next/navigation";
"use client";
import { useRouter } from "next/navigation";
export default function MyComponent() {
const router = useRouter();
const goToLogin = () => {
router.push("/#");
};
return <button onClick={goToLogin}>Go to Login</button>;
}
Method | Description |
---|---|
router.push(url) |
Navigate to a new route (adds to history stack) |
router.replace(url) |
Navigate without keeping the previous route in history (used for redirects) |
router.refresh() |
Re-fetches data and re-renders current route |
router.back() |
Same as browser back button |
router.forward() |
Same as browser forward button |
"use client";
import { useRouter } from "next/navigation";
export default function RegisterForm() {
const router = useRouter();
const handleSubmit = async (e) => {
e.preventDefault();
// Do API call or logic
await new Promise((r) => setTimeout(r, 1000)); // fake delay
// Navigate to dashboard after success
router.push("/dashboard");
};
return (
<form onSubmit={handleSubmit}>
<input type="text" placeholder="Name" />
<button type="submit">Register</button>
</form>
);
}
"use client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
export default function ProtectedPage() {
const router = useRouter();
const isLoggedIn = false; // assume from context/store
useEffect(() => {
if (!isLoggedIn) {
router.replace("/#");
}
}, [isLoggedIn]);
return <div>Welcome to Protected Page</div>;
}
router.push(`/profile/${userId}?ref=welcome`);
- Only use
useRouter()
inside client components. - Make sure to add
"use client"
at the top of the file.
Feature | Use |
---|---|
router.push() |
Navigate forward |
router.replace() |
Replace history entry |
router.back() |
Go back |
router.refresh() |
Refresh current route |
In Next.js 15 (App Router), we have two powerful file types for structuring our UI: layout.tsx
and template.tsx
. While they may look similar, their behavior and purpose are very different.
Letβs break this down in full detail. π
layout.tsx
is used to wrap pages and components with a consistent UI shellβfor example, a navbar, footer, sidebar, etc.
- It is shared and persistent across navigation.
- It does not remount when navigating between sibling routes.
- It is ideal for:
- Headers
- Sidebars
- Persistent navigation
- Keeping state (like dark mode toggle, open sidebar)
// app/dashboard/layout.tsx
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div>
<Sidebar />
<main>{children}</main>
</div>
);
}
- When we navigate from
/dashboard/analytics
to/dashboard/settings
, the layout doesn't rerender. It persists. - Useful for layouts that manage state (like sidebar toggle).
template.tsx
is a new concept in the App Router that renders a fresh instance on every navigation to that segment.
- It remounts on every navigation.
- Itβs not shared, unlike
layout.tsx
. - Use it when you want a clean slate every time the route loads.
- Good for modals, loading animations, one-time transitions, etc.
// app/shop/@modal/template.tsx
export default function ModalTemplate({ children }: { children: React.ReactNode }) {
return (
<div className="modal-overlay">
<div className="modal-content">{children}</div>
</div>
);
}
- Every time you go from one route to another using this
template.tsx
, it will remount all children, giving you a fresh instance.
Feature | layout.tsx |
template.tsx |
---|---|---|
π§ Shared Across Routes | β Yes | β No (fresh on each route) |
π Remounts on Nav | β No | β Yes |
πΎ Keeps State | β Yes | β No (resets every time) |
π― Use Case | Persistent UI (navbars, sidebars, etc.) | Transient UI (modals, animations, etc.) |
π§© Memory Optimization | Good for keeping components alive | Good for short-lived dynamic content |
Use Case | Use layout.tsx ? |
Use template.tsx ? |
---|---|---|
Navbar + Sidebar Shell | β Yes | β No |
Dashboard Tabs with local state | β Yes | β No |
Modals that open on different routes | β No | β Yes |
Step-by-step multi-page form | β No | β Yes (reset state each time) |
Transitions/Animations between views | β No | β Yes |
We can use both in the same folder structure!
app/
βββ dashboard/
βββ layout.tsx β
Shared layout
βββ template.tsx β
Fresh template on each visit
βββ page.tsx
βββ settings/
βββ page.tsx
So when you go from /dashboard
β /dashboard/settings
, the layout.tsx
stays, but the template.tsx
will re-render.
Aspect | layout.tsx |
template.tsx |
---|---|---|
Shared across pages? | β Yes | β No |
Remounts on nav? | β No | β Yes |
Keeps state? | β Yes | β No |
Use for? | Layout shell, persistent UI | Dynamic UIs, modals, animations |
In Next.js 15, error handling is built-in and improved thanks to the App Router architecture. Hereβs a detailed breakdown of all the ways we can handle errors in a Next.js 15 project, both server-side and client-side.
Catches rendering or async errors inside the route segment it belongs to.
app/
βββ dashboard/
β βββ page.tsx
β βββ error.tsx π
// app/dashboard/error.tsx
'use client';
export default function ErrorPage({ error, reset }: {
error: Error;
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong in Dashboard!</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>Try Again</button>
</div>
);
}
error
: the caught error.reset()
: re-attempts rendering (good for retry logic).
Catches unhandled errors in the whole app.
// app/error.tsx
'use client';
export default function GlobalError({ error, reset }: {
error: Error;
reset: () => void;
}) {
return (
<div>
<h1>App crashed π’</h1>
<p>{error.message}</p>
<button onClick={reset}>Retry</button>
</div>
);
}
When using server functions or API routes, handle expected errors explicitly.
// app/api/data/route.ts
import { NextResponse } from 'next/server';
export async function GET() {
try {
const data = await fetchData();
return NextResponse.json(data);
} catch (error) {
return NextResponse.json(
{ message: 'Something went wrong' },
{ status: 500 }
);
}
}
app/
βββ not-found.tsx
// app/not-found.tsx
export default function NotFound() {
return <h1>404 - Page Not Found</h1>;
}
import { notFound } from 'next/navigation';
export default function Page({ params }) {
if (params.id !== 'valid-id') {
notFound(); // Triggers the 404 page
}
return <div>Valid Page</div>;
}
For errors in interactive client components.
// components/ErrorBoundary.tsx
'use client';
import React from 'react';
class ErrorBoundary extends React.Component<
{ children: React.ReactNode },
{ hasError: boolean }
> {
constructor(props: any) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error) {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return <h2>Client UI broke π</h2>;
}
return this.props.children;
}
}
export default ErrorBoundary;
<ErrorBoundary>
<SomeClientComponent />
</ErrorBoundary>
When using form actions or server actions:
'use server';
export async function submitData(formData: FormData) {
try {
// Do something
} catch (err) {
throw new Error("Submission failed!");
}
}
This will bubble up to error.tsx
or be caught in the calling component.
For hybrid apps using both App Router and Pages Router:
pages/
βββ 404.tsx
βββ 500.tsx
export default function Custom404() {
return <h1>Oops! This page doesnβt exist.</h1>;
}
export default function Custom500() {
return <h1>Server-side error occurred!</h1>;
}
Situation | Use This |
---|---|
A component crashes (render error) | error.tsx or ErrorBoundary |
Dynamic route not found | notFound() |
Data fetch fails | try/catch + error.tsx |
Invalid user input | throw new Error() (handled by layout/template or form UI) |
Programmatic redirection | redirect('/some-path') |
Method | Scope | Usage |
---|---|---|
error.tsx |
Route segment | Catches render errors in that route |
app/error.tsx |
Global | Catches all uncaught errors |
notFound() |
Server components | Manually trigger 404 page |
ErrorBoundary |
Client components | Catches UI errors only in client parts |
try/catch |
API / server logic | Handle and format expected errors |
Custom 404/500 | Pages Router | Fallback UI for pages (hybrid apps) |
Deep into Parallel Routes in Next.js 15:. This is a powerful advanced feature in the App Router that helps us render multiple pages or sections in parallelβwithin the same layoutβwithout affecting each other.
Parallel Routes allow us to display multiple route segments in the same layout at the same time, where each segment is independent, like:
- A sidebar and a main content area
- Multiple tabs rendered simultaneously
- Dashboards with multiple panels
In Next.js 15, named slots using the @
symbol are used for parallel routing.
app/
βββ layout.tsx
βββ @sidebar/
β βββ page.tsx
βββ @main/
β βββ page.tsx
Note:
@sidebar
and@main
are slot names. They do not define the route, but instead define what content goes into that slot.
In the layout file (layout.tsx
), we can accept named slots as props:
// app/layout.tsx
export default function RootLayout({
children,
sidebar,
main,
}: {
children: React.ReactNode;
sidebar: React.ReactNode;
main: React.ReactNode;
}) {
return (
<div className="flex">
<aside className="w-1/4 p-4 border-r">{sidebar}</aside>
<main className="w-3/4 p-4">{main}</main>
</div>
);
}
sidebar
: will get content from the@sidebar
route.main
: will get content from the@main
route.
So, both routes can be rendered in parallel under the same layout.
You define a URL structure like this:
/dashboard?parallelRoute=main
But usually, itβs used with segment config
and navigation links, like:
<Link href="/dashboard" scroll={false}>
Dashboard Home
</Link>
And behind the scenes, Next.js renders both @sidebar and @main in their slots, based on the file structure.
app/
βββ dashboard/
β βββ layout.tsx
β βββ @overview/
β β βββ page.tsx
β βββ @settings/
β β βββ page.tsx
// app/dashboard/layout.tsx
export default function DashboardLayout({
overview,
settings,
}: {
overview: React.ReactNode;
settings: React.ReactNode;
}) {
return (
<div>
<nav>
<Link href="/dashboard">Overview</Link>
<Link href="/dashboard/settings">Settings</Link>
</nav>
<div className="grid grid-cols-2">
<section>{overview}</section>
<section>{settings}</section>
</div>
</div>
);
}
Parallel routes help us:
- Reuse layout while keeping each section isolated
- Avoid unmounting components during navigation
- Improve performance by lazy loading segments independently
If a parallel route is not loaded, Next.js will look for:
app/
βββ @main/
β βββ default.tsx
This acts like a fallback or placeholder UI until the actual page is rendered.
Each slot can have its own:
error.tsx
loading.tsx
not-found.tsx
So if just the sidebar crashes, only it is replaced with its error.tsx
componentβthe rest of the layout stays intact!
Feature | Benefit |
---|---|
Named slots | Organize layout clearly |
Independent rendering | Better performance, less flicker |
Fallback UIs | Handle partial loading gracefully |
Error isolation | One section error doesn't crash the whole page |
Smooth UX | Useful for dashboards, split screens, modals, tabs |
- Parallel Routes = multiple independent route trees rendered inside named slots (
@name
) in the same layout. - Perfect for dashboards, sidebars, tabbed interfaces, modals, etc.
- Easy to scale, clean layout-based architecture.
- Use
default.tsx
,loading.tsx
, anderror.tsx
for graceful UX in each segment.
Unmatched Routes in Next.js 15βa lesser-known but super powerful feature when working with Parallel Routes. It gives us more control when a specific slot does not have a matched route.
Unmatched routes let us detect and handle the case when no route is matched for a given named slot (used in parallel routing).
Think of it like:
- We defined a named slot (e.g.,
@modal
) - But the current route does not match anything inside that slot
- So we can render a fallback UI, show a default message, or redirect
In layouts with parallel routes like this:
app/
βββ layout.tsx
βββ @main/
βββ page.tsx
βββ @sidebar/
βββ page.tsx
If the current route matches @main
, but nothing is matched in @sidebar
, then the route is unmatched for @sidebar
.
This is where not-found.tsx
or default.tsx
(for that slot) kicks in.
Letβs say we have:
app/
βββ dashboard/
βββ layout.tsx
βββ @notifications/
βββ page.tsx
βββ not-found.tsx
And we try to visit:
/dashboard
If thereβs no matching route for @notifications
, Next.js checks:
- Is there a
page.tsx
inside@notifications
? β - Is the URL trying to render
@notifications
, but no match is found? β - β Then it will render
not-found.tsx
in that slot
app/
βββ layout.tsx
βββ @main/
βββ page.tsx
βββ @modal/
βββ not-found.tsx
export default function RootLayout({ main, modal }: { main: React.ReactNode; modal: React.ReactNode }) {
return (
<div>
<div>{main}</div>
<div>{modal}</div>
</div>
);
}
export default function ModalNotFound() {
return <div>No modal route matched.</div>;
}
So if the URL is: /home
, and thereβs nothing matched under @modal
, then ModalNotFound
is rendered in the modal
slot.
You can optionally define a default.tsx
too for graceful fallback:
// app/@modal/default.tsx
export default function ModalDefault() {
return <p>No active modal</p>;
}
This will be shown only when thereβs no match and no not-found.tsx
.
Concept | Explanation |
---|---|
π What is it? | A route slot that didn't match any route |
π§ Used In? | Parallel Routes using @slotName |
π¦ Handled By | not-found.tsx or default.tsx inside that slot |
π Fallback | Useful to show default UIs or errors |
β Benefit | Prevent blank slots, show UI when something is missing |
Intercepting routes allow us to load a different route in a specific part of the pageβwithout replacing the whole route view. Let's break down intercepting routes in Next.jsβa feature introduced in Next.js 13+ with the App Router. Itβs super useful when we want to show something like a modal or drawer on top of an existing page without navigating away from it.
π Think:
We're on /products
and click a product to see /products/1
. Instead of navigating fully to /products/1
, we want a modal to open with product detailsβstill on /products
.
Letβs say we have a list of users at /users
, and when we click on one, we want to show /users/123
as a modal.
- User clicks β navigates to
/users/123
- Whole page reloads and changes.
- User clicks β modal shows on
/users
, but content from/users/123
is loaded inside it.
Suppose you're on route /users
, and want to intercept /users/[id]
.
app/
βββ users/
β βββ page.tsx # /users
β βββ [id]/
β β βββ page.tsx # /users/123 (full page)
β βββ (modals)/ # Special folder for intercept
β β βββ users/
β β βββ [id]/
β β βββ page.tsx # Intercepted modal content
- The route
(modals)/users/[id]/page.tsx
intercepts/users/[id]
when weβre already on/users
. - Instead of full navigation, the intercepted page can show in a modal component in
/users/page.tsx
.
π (modals)
is just a parallel route segmentβyou can name it anything like (intercept)
or (drawer)
βitβs conventionally wrapped in ()
.
You then render the intercepted route with the Modal
logic inside /users/page.tsx
.
/users/page.tsx
import { useRouter } from 'next/navigation'
import Modal from '@/components/Modal'
import UserDetails from './(modals)/users/[id]/page'
export default function UsersPage() {
const router = useRouter()
const handleClick = (id: string) => {
router.push(`/users/${id}`)
}
return (
<>
<div>
<h1>All Users</h1>
<button onClick={() => handleClick('123')}>View User 123</button>
</div>
{/* Show modal if intercepted */}
{/* You can conditionally show based on route or state */}
<Modal>
<UserDetails />
</Modal>
</>
)
}
Feature | Purpose |
---|---|
Intercepting Route | Load route content (like modal) without full nav |
Parallel Routes | Needed for intercepting; wrapped in () |
Practical Use | Modals, drawers, side-panels, etc. |
Parallel Intercepting Routes let us show different routes side-by-side in specific UI areas (like modals, drawers, or side panels) without losing the original route context. Letβs dive into parallel intercepting routes in Next.js 15, especially as it builds on the App Router architecture that started with v13 and is now more powerful and flexible.
Imagine a dashboard where:
- The main content shows the user's dashboard (
/dashboard
) - A side panel opens to show user settings (
/settings
) while staying on/dashboard
β We want:
/dashboard
to remain visible/settings
to load in a side panel- Browser URL to reflect
/settings
, but without full navigation
This is what parallel routes + intercepting enable.
Concept | Description |
---|---|
Parallel Routes | Multiple UI areas (slots) that render independently |
Intercepting Routes | Route loaded into a slot instead of full navigation |
Named Slots | You define slots with keys like @modal , @drawer , etc. |
(group) folders | Group routes without affecting URL structure |
Letβs build this example:
- Main route:
/dashboard
- Intercepted side panel:
/settings
- Show
/settings
in a@panel
slot while keeping/dashboard
visible
app/
βββ layout.tsx # Defines all slots (@main, @panel)
βββ page.tsx # Default home route
βββ dashboard/
β βββ page.tsx # /dashboard
βββ settings/
β βββ page.tsx # /settings (full route if direct nav)
βββ (panel)/
β βββ settings/
β βββ page.tsx # Intercepted view for @panel
export default function RootLayout({ children, panel }: {
children: React.ReactNode
panel: React.ReactNode
}) {
return (
<div className="flex">
<main className="flex-1">
{children} {/* Default content */}
</main>
<aside className="w-96 border-l">
{panel} {/* Intercepted content */}
</aside>
</div>
)
}
Weβve created two parallel routes:
@main
β showschildren
@panel
β showspanel
(intercepted)
Behind the scenes, Next.js maps these using file conventions like
(panel)
folders.
- When we navigate from
/dashboard
to/settings
, if the current route has a@panel
slot defined, Next.js renders/settings
into the slot. - If we go directly to
/settings
, it loads as a full page (like normal).
- β¨ Seamless modals/drawers without full page reloads
- π Preserves scroll, state, context
- π Shareable URLs (since
/settings
is real) - π§± Clean architecture via named slots
To declare a specific route to a slot, use the route.js
config in the folder:
// app/(panel)/settings/route.js
export const route = {
slot: 'panel'
}
This makes it explicit that this route should be loaded into the @panel
slot.
Feature | Use |
---|---|
layout.tsx with slots |
Define multiple UI zones like main , modal , panel , etc. |
(group) folders |
Organize routes without URL impact |
Intercepting routes | Load new routes into a slot instead of full navigation |
Great for | Modals, side drawers, detail previews, split screens |
In Next.js 15, Route Handlers are how we define custom backend logic (like APIs) within the App Router. They allow us to handle HTTP requests (GET, POST, PUT, DELETE, etc.) directly inside our app directory, similar to traditional API routes but fully aligned with the App Routerβs conventions.
They are special files (typically named route.ts
or route.js
) placed inside a specific folder (usually under /app/api/...
) that export functions corresponding to HTTP methods β like GET
, POST
, etc.
β Route Handlers replace
pages/api
from the old Pages Router.
app/
βββ api/
βββ hello/
βββ route.ts β Route Handler
This defines an API endpoint at:
/api/hello
app/api/hello/route.ts
export async function GET() {
return new Response("Hello from API Route!");
}
π Now hitting http://localhost:3000/api/hello
with a GET request will return:
Hello from API Route!
export async function GET(request: Request) {
return new Response("GET Request Success");
}
export async function POST(request: Request) {
const data = await request.json();
return new Response(`Received name: ${data.name}`);
}
curl -X POST http://localhost:3000/api/hello -H "Content-Type: application/json" -d '{"name":"Skyy"}'
You can export functions like:
GET
POST
PUT
DELETE
PATCH
HEAD
OPTIONS
Example:
export async function DELETE() {
return new Response("Deleted something!");
}
You can use the Request
object to access:
export async function GET(req: Request) {
const url = new URL(req.url);
const search = url.searchParams.get("name");
return new Response(`Hi ${search}`);
}
export async function POST(req: Request) {
const body = await req.json();
return new Response(`Received ${body.name}`);
}
You can use dynamic segments like this:
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
return new Response(`User ID: ${params.id}`);
}
Now accessing
/api/user/123
will return:
"User ID: 123"
- Route handlers can run on Edge or Node.js (your choice).
- To run on Edge, use:
export const runtime = "edge";
- Custom API routes (fetching/updating DB)
- Webhooks
- Auth endpoints
- File uploads
- Payment handlers (Stripe/Razorpay)
- Server Actions support (in the future)
export async function GET() {
const data = { message: "Hello JSON" };
return Response.json(data); // Shortcut for JSON responses
}
Feature | Supported β |
---|---|
GET/POST/PUT/DELETE | β |
JSON body parsing | β |
Query/Search Params | β |
Dynamic Params ([id] ) |
β |
Edge Runtime Option | β |
Typed via TypeScript | β |
In Next.js 15, Route Handlers give us direct access to cookies and caching controls, which are super important for things like authentication, personalization, performance, and session handling.
Next.js provides cookies()
utility from next/headers
for reading and setting cookies on the server.
import { cookies } from "next/headers";
export async function GET() {
const cookieStore = cookies();
const token = cookieStore.get("auth-token");
return new Response(`Cookie value: ${token?.value ?? "not found"}`);
}
export async function POST() {
const cookieStore = cookies();
cookieStore.set("auth-token", "12345", {
httpOnly: true,
path: "/",
secure: true,
maxAge: 60 * 60 * 24 * 7, // 1 week
});
return new Response("Cookie set!");
}
export async function DELETE() {
const cookieStore = cookies();
cookieStore.delete("auth-token");
return new Response("Cookie deleted!");
}
βοΈ Cookies set with
httpOnly: true
canβt be accessed from JavaScript in the browser β great for securing tokens like JWTs.
β οΈ cookies()
only works inside Route Handlers, Server Components, and Server Actions β not in Client Components.
Caching is used to control how responses are stored and reused by the browser or a CDN. In Route Handlers, we can manage caching behavior using Response
headers.
export async function GET() {
const data = { name: "Skyy", role: "Dev" };
return new Response(JSON.stringify(data), {
headers: {
"Content-Type": "application/json",
"Cache-Control": "public, max-age=3600", // Cache for 1 hour
},
});
}
Directive | Meaning |
---|---|
no-store |
Donβt store in cache at all |
no-cache |
Must revalidate every time |
public |
Cacheable by any cache (CDN, browser) |
private |
Only cache in user's browser |
max-age=seconds |
Time in seconds before stale |
export async function GET() {
return new Response("Private data", {
headers: {
"Cache-Control": "no-store",
},
});
}
In Next.js 15:
- Static cache (default) is used when response can be cached and reused.
- Dynamic responses (e.g., with cookies or user-specific data) are opted out of static cache.
- Use:
to force dynamic behavior even when no dynamic data is present.
export const dynamic = "force-dynamic";
Example:
export const dynamic = "force-dynamic"; // or "force-static"
Feature | Method / Tool | Notes |
---|---|---|
Read Cookie | cookies().get() |
Server only |
Set Cookie | cookies().set() |
Secure, customizable |
Delete Cookie | cookies().delete() |
Easy removal |
Cache Response | Response.headers |
Fully customizable |
Control dynamic/static | export const dynamic |
Static or dynamic behavior |
In Next.js 15, Middleware is a special function that runs before a request is completed, and before rendering a route. It allows us to intercept requests, perform logic, and optionally redirect, rewrite, or modify responses.
Think of it as a gatekeeper between the user and the page.
Middleware is perfect for:
- π Authentication & Authorization (e.g., redirect if not logged in)
- π Localization (e.g., detect browser language)
- π A/B testing or feature flags
- π Rewrites and redirects
- π§ͺ Logging, analytics, etc.
In the root of the app/
or project directory, we create:
/middleware.ts
or
/middleware.js
This file runs for every request unless we filter it.
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
// Perform something with the request
return NextResponse.next(); // allow request
}
export function middleware(request: NextRequest) {
const isLoggedIn = false;
if (!isLoggedIn && request.nextUrl.pathname.startsWith("/dashboard")) {
return NextResponse.redirect(new URL("/#", request.url));
}
return NextResponse.next();
}
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname === "/old") {
return NextResponse.rewrite(new URL("/new", request.url));
}
return NextResponse.next();
}
export function middleware(request: NextRequest) {
const response = NextResponse.next();
response.headers.set("x-powered-by", "Skyy");
return response;
}
export function middleware(request: NextRequest) {
const token = request.cookies.get("auth-token")?.value;
const isAuthRoute = request.nextUrl.pathname.startsWith("/dashboard");
if (isAuthRoute && !token) {
return NextResponse.redirect(new URL("/#", request.url));
}
return NextResponse.next();
}
To restrict middleware only to specific routes, use config.matcher
.
export const config = {
matcher: ["/dashboard/:path*", "/profile", "/settings"],
};
π
:path*
means include all nested routes too.
-
NextRequest
gives access to:request.nextUrl.pathname
β URL inforequest.cookies
β read cookiesrequest.headers
β get headers
-
NextResponse
gives methods to:.redirect()
.rewrite()
.next()
(allow to continue).headers.set()
Use Case | Middleware Logic Example |
---|---|
Protect dashboard | Check cookie or session and redirect |
Auto locale redirect | Check Accept-Language and rewrite |
Block bots or IPs | Check IP/headers and return 403 |
Beta route gating | Check cookie/flag and redirect |
Analytics logging | Log request info before serving |
- β Canβt access
use client
components - β Cannot render components
- β Runs at Edge (very fast)
- β Can only use Web APIs and Next APIs
Feature | Description |
---|---|
Location | /middleware.ts at project root |
Execution | Before the route is rendered |
Key Tools | NextRequest , NextResponse |
Common Usage | Auth, redirects, rewrites, headers |
Matcher | Filters what routes middleware applies to |
// app/api/todos/[id]/route.ts
import { NextResponse } from "next/server";
import { todos } from "@/lib/data";
export async function GET(_: Request, { params }: { params: { id: string } }) {
const todo = todos.find((t) => t.id === params.id);
return todo
? NextResponse.json(todo)
: NextResponse.json({ error: "Not found" }, { status: 404 });
}
export async function PUT(request: Request, { params }: { params: { id: string } }) {
const { task, done } = await request.json();
const index = todos.findIndex((t) => t.id === params.id);
if (index === -1) {
return NextResponse.json({ error: "Todo not found" }, { status: 404 });
}
todos[index] = { ...todos[index], task, done };
return NextResponse.json(todos[index]);
}
export async function DELETE(_: Request, { params }: { params: { id: string } }) {
const index = todos.findIndex((t) => t.id === params.id);
if (index === -1) {
return NextResponse.json({ error: "Not found" }, { status: 404 });
}
const deleted = todos.splice(index, 1)[0];
return NextResponse.json(deleted);
}
Letβs explore headers in Route Handlers in Next.js 15 in depth β how to read, set, and use them within route.ts
files inside the app/api
directory.
In Next.js 15 Route Handlers (App Router), headers can be used:
- To read incoming request metadata (e.g., auth tokens, content-type, etc.)
- To set outgoing custom response headers (e.g., caching, CORS, custom info)
In any route.ts
handler, use the Request
objectβs .headers
property.
// app/api/headers-example/route.ts
import { NextResponse } from "next/server";
export async function GET(request: Request) {
const authToken = request.headers.get("authorization");
const userAgent = request.headers.get("user-agent");
return NextResponse.json({
message: "Headers received",
authToken,
userAgent,
});
}
β Use this when you want to validate API keys, check user-agents, etc.
Use the NextResponse
objectβs .headers.set()
method.
// app/api/set-headers/route.ts
import { NextResponse } from "next/server";
export async function GET() {
const res = NextResponse.json({ message: "Response with custom headers" });
res.headers.set("X-Custom-Header", "HelloFromNextJS15");
res.headers.set("Cache-Control", "no-store");
return res;
}
You can use this for:
- Security headers (CSP, CORS, etc.)
- Performance tuning (Cache-Control)
- Metadata (debugging, tracking)
// app/api/secure/route.ts
import { NextResponse } from "next/server";
export async function GET(request: Request) {
const token = request.headers.get("authorization");
if (token !== "Bearer mysecrettoken123") {
return NextResponse.json(
{ error: "Unauthorized" },
{ status: 401 }
);
}
return NextResponse.json({ message: "Welcome, authorized user" });
}
- Headers are case-insensitive.
- Always prefer
get("authorization")
, not"Authorization"
.
export async function GET(request: Request) {
const headers = Object.fromEntries(request.headers.entries());
return NextResponse.json({ allHeaders: headers });
}
Task | Code Snippet |
---|---|
Read header | request.headers.get("authorization") |
Set response header | res.headers.set("X-My-Header", "Value") |
Multiple headers | Loop: Object.fromEntries(request.headers.entries()) |
Security header | res.headers.set("X-Frame-Options", "DENY") |
Cache-Control | res.headers.set("Cache-Control", "no-store") |
Letβs break down the different types of HTTP headers β especially in the context of Next.js 15 route handlers β and how theyβre typically used.
HTTP headers are key-value pairs sent between the client and server to provide context about the request/response.
Headers are categorized into:
Sent from the client (browser or frontend) to the server.
Header Name | Purpose |
---|---|
Authorization |
Carries access tokens or API keys. |
Content-Type |
Describes the format of the body (application/json , multipart/form-data , etc.) |
Accept |
Tells the server what content types the client can handle (application/json , etc.) |
User-Agent |
Identifies the client app/browser and OS. |
Cookie |
Sends cookies to the server. |
Referer |
Tells the server which page the request came from. |
Accept-Language |
Preferred language of the client (en , fr , etc.) |
Sent by the server to the client, alongside the body.
Header Name | Purpose |
---|---|
Content-Type |
Tells the browser how to interpret the response (text/html , application/json , etc.) |
Set-Cookie |
Sends cookies to be stored in the browser. |
Cache-Control |
Caching instructions for browsers/CDNs (no-store , max-age=3600 , etc.) |
Location |
Used in redirects. |
Access-Control-Allow-Origin |
For CORS. Specifies allowed origins. |
X-Custom-* |
Custom headers for debugging or tracking. |
Apply to the body/content of the request or response.
Header Name | Purpose |
---|---|
Content-Length |
Size of the body. |
Content-Encoding |
How content is encoded (e.g., gzip). |
ETag |
Used for caching validation. |
Last-Modified |
When the resource was last changed. |
Added to protect the app and browser.
Header Name | Purpose |
---|---|
X-Frame-Options |
Prevent clickjacking (e.g., DENY , SAMEORIGIN ) |
Strict-Transport-Security |
Enforce HTTPS (max-age=... ) |
Content-Security-Policy |
Restrict resources (images, scripts, etc.) |
X-XSS-Protection |
Browser XSS protection |
Referrer-Policy |
Controls Referer header behavior |
Control cross-origin requests.
Header Name | Purpose |
---|---|
Access-Control-Allow-Origin |
Whitelisted origin(s) |
Access-Control-Allow-Methods |
Allowed HTTP methods |
Access-Control-Allow-Headers |
Allowed custom headers |
Access-Control-Allow-Credentials |
Allow cookies/credentials |
export async function GET(request: Request) {
const lang = request.headers.get("accept-language");
const auth = request.headers.get("authorization");
return Response.json({ lang, auth });
}
import { NextResponse } from "next/server";
export async function GET() {
const res = NextResponse.json({ status: "ok" });
res.headers.set("Cache-Control", "no-store");
res.headers.set("X-My-App", "Zumfass");
res.headers.set("Content-Security-Policy", "default-src 'self'");
return res;
}
In Chrome β Right-click β Inspect β Network tab β Select a request β Headers tab
Type | Examples | Usage In Next.js |
---|---|---|
Request Headers | authorization , user-agent |
request.headers.get() |
Response Headers | set-cookie , cache-control |
response.headers.set() |
Security | X-Frame-Options , CSP |
Custom security headers |
CORS | access-control-allow-origin |
Set via middleware/handler |
πͺ Let's dive deep into cookies in Route Handlers in Next.js 15 β how they work, how to use them for authentication, preferences, sessions, and more.
Cookies are small key-value pairs stored in the browser, which can be sent with every HTTP request to the server. They're essential for:
- User authentication (like tokens)
- Session management
- Storing preferences (theme, language)
In Next.js App Router (v13+), especially in Route Handlers (route.ts
, route.js
), we use the cookies()
API to read, set, and delete cookies.
β This API is server-only β works in Route Handlers, Server Components, Middleware, and Layouts.
import { cookies } from 'next/headers';
export async function GET() {
const cookieStore = cookies();
const token = cookieStore.get('token'); // returns { name, value, Path, ... }
return Response.json({
message: 'Hello',
token: token?.value || 'No Token',
});
}
export async function POST() {
const cookieStore = cookies();
cookieStore.set({
name: 'token',
value: 'abc123',
httpOnly: true, // secure from JS access
secure: true, // only over HTTPS
path: '/',
maxAge: 60 * 60 * 24, // 1 day
});
return Response.json({ message: 'Cookie Set' });
}
β You can also set it using this shorter form:
cookieStore.set('theme', 'dark');
export async function DELETE() {
const cookieStore = cookies();
cookieStore.delete('token');
return Response.json({ message: 'Cookie Deleted' });
}
// app/api/#/route.ts
import { cookies } from 'next/headers';
export async function POST(request: Request) {
const { username, password } = await request.json();
if (username === 'admin' && password === 'secret') {
cookies().set({
name: 'token',
value: 'JWT-TOKEN-HERE',
httpOnly: true,
secure: true,
maxAge: 60 * 60 * 24,
});
return Response.json({ message: 'Login successful' });
}
return Response.json({ message: 'Invalid credentials' }, { status: 401 });
}
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('token');
if (!token) {
return NextResponse.redirect(new URL('/#', request.url));
}
return NextResponse.next();
}
Flag | Purpose |
---|---|
httpOnly |
Cannot be accessed via JavaScript (document.cookie ) |
secure |
Only sent over HTTPS |
sameSite |
Controls cross-origin sending (e.g., strict , lax , none ) |
maxAge |
How long the cookie is valid (in seconds) |
path |
URL path scope of the cookie (/ = all) |
Feature | Server (cookies() ) |
Client (document.cookie ) |
---|---|---|
Access scope | Server-side only (secure) | Client-side only (JS-accessible) |
Best for | Auth tokens, user sessions | Preferences (e.g., theme) |
Tamper resistance | More secure (with httpOnly ) |
Less secure |
Operation | Code Example |
---|---|
Get Cookie | cookies().get("token") |
Set Cookie | cookies().set("token", "value") |
Delete Cookie | cookies().delete("token") |
The redirect()
function is a server-side utility provided by Next.js used to immediately redirect a user to another route.
Letβs dive deep into the redirect()
function in Next.js 15, especially within the App Router and Route Handlers. Itβs a super useful utility for controlling navigation on the server.
Itβs available in:
Where We Can Use It |
---|
Server Components (.tsx ) |
Route Handlers (route.ts ) |
Layouts / Pages (layout.tsx ) |
Middleware (middleware.ts ) |
import { redirect } from 'next/navigation';
Note: This is from next/navigation
, even when used in route.ts
.
redirect('/#');
- This immediately stops the current request/response lifecycle and sends a redirect to the browser.
Feature | Description |
---|---|
Server-Only | Canβt be used in Client Components. Will throw error if attempted. |
No return needed | Calling redirect() throws internally to break the function execution. |
Permanent/Temp? | By default, uses status code 307 (temporary). Cannot be customized. |
Blocking | It immediately halts any further code execution. |
// app/api/#/route.ts
import { redirect } from 'next/navigation';
import { cookies } from 'next/headers';
export async function GET() {
const token = cookies().get('token');
if (!token) {
redirect('/#'); // instantly redirects if not authenticated
}
return Response.json({ message: 'You are logged in' });
}
// app/dashboard/page.tsx
import { redirect } from 'next/navigation';
import { cookies } from 'next/headers';
export default function DashboardPage() {
const token = cookies().get('token');
if (!token) {
redirect('/#');
}
return <h1>Welcome to Dashboard!</h1>;
}
Mistake | Fix |
---|---|
β Using in client components | β
Use useRouter().push() instead |
β Trying to return redirect() |
β Just call it; donβt return it |
β Using after an async fetch | β Ensure condition checks before data fetch |
In middleware, we donβt use redirect()
β instead, we use:
import { NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
return NextResponse.redirect(new URL('/#', request.url));
}
In Client Components, use the useRouter
hook:
'use client';
import { useRouter } from 'next/navigation';
function MyComponent() {
const router = useRouter();
const handleClick = () => {
router.push('/about'); // Client-side navigation
};
return <button onClick={handleClick}>Go to About</button>;
}
Internally, redirect()
uses throw
to prevent rendering or returning anything else. Thatβs why we donβt write any code after it β it wonβt run.
redirect('/#');
console.log("This won't log"); // Never executes!
Action | Use |
---|---|
Redirect in server comp | redirect('/path') |
Redirect in route handler | redirect('/path') |
Redirect in middleware | NextResponse.redirect(...) |
Redirect on client | useRouter().push('/path') |
Let's go deep into caching in Next.js 15, especially with the App Router and GET()
methods in Route Handlers. We'll use an example and break everything down step-by-step.
In Next.js 15, caching helps us:
- Improve performance by reducing redundant database or API calls.
- Speed up delivery with static or semi-static responses.
- Balance freshness and performance with revalidation.
// app/api/pokemon/route.ts
// Caching in Nextjs15
// Only works with GET() methods
export const dynamic = "force-static";
export const revalidate = 20; // only changed after 20 secs, but in the BG
export async function GET() {
// This would usually come from a DB.
const pokemonTypes = [
{ id: 1, type: "Grass-type πΏ" },
{ id: 2, type: "Fire-type π₯" },
{ id: 3, type: "Water-type π¦" },
{ id: 4, type: "Fighting-type π₯" },
];
return Response.json(pokemonTypes);
}
Letβs break it down now. π
This tells Next.js:
βTreat this route as fully static. Generate it at build time.β
Options available:
Value | Meaning |
---|---|
"auto" |
Default behavior (Next decides based on code used) |
"force-dynamic" |
Always render on the server at runtime (no caching) |
"force-static" |
Always cache the result as static (fully cacheable, no dynamic data) |
So, force-static
= βmake this routeβs response cacheable and serve the cached version.β
This enables Incremental Static Regeneration (ISR) for API Routes / Route Handlers. It means:
- The first request will build and cache the response.
- Subsequent requests (within 20 seconds) serve the cached version.
- After 20 seconds, the cache is rebuilt in the background while still serving the old data.
- Once rebuilt, new requests get fresh data.
π This is sometimes referred to as stale-while-revalidate behavior.
Only the GET() method response is cached. This works perfectly for:
- Blogs
- Static product lists
- Public APIs
- Anything that doesnβt change rapidly
Caching wonβt apply if:
- You use cookies, headers, or dynamic content (e.g.,
headers()
,cookies()
) - You fetch from external APIs without proper cache config
- You're using
POST
,PUT
,DELETE
, etc. (caching only works withGET()
)
You can mix dynamic + revalidation like this:
export const dynamic = "force-dynamic"; // runtime always
export const revalidate = 10; // doesnβt matter, since itβs forced dynamic
But here, revalidate
is ignored. Only useful with static or auto.
Scenario | Recommendation |
---|---|
Public data that rarely changes | force-static + long revalidate |
Semi-frequent updates (e.g., news) | auto + revalidate = X |
User-authenticated or private data | force-dynamic , no caching |
Client-side fetching (CSR) | Control cache via SWR, React Query etc. |
Letβs say we call this from the frontend:
const res = await fetch("/api/pokemon");
const data = await res.json();
This works perfectly with cached GET()
APIs. The client gets the cached version unless 20 seconds have passed and revalidation has occurred.
Add a timestamp to your data to verify cache refresh:
const pokemonTypes = [
{ id: 1, type: "Grass-type πΏ" },
{ id: 2, type: "Fire-type π₯" },
{ id: 3, type: "Water-type π¦" },
{ id: 4, type: "Fighting-type π₯" },
{ id: 5, type: `Fetched at: ${new Date().toISOString()}` },
];
Now, request this endpoint. The timestamp will only change every 20 seconds, confirming the caching behavior!
Concept | Value/Explanation |
---|---|
dynamic |
"force-static" for caching static GET responses |
revalidate |
Time (in sec) before cache revalidates in BG |
Applies to | GET() methods only |
Server Cache | Next.js + Vercel or serverless edge caching |
Not supported | For POST , PUT , DELETE , cookies(), headers() |
Understanding Request
vs NextRequest
and Response
vs NextResponse
is crucial for working with Route Handlers, middleware, and server-side logic in Next.js 15 (App Router). Letβs break it all down clearly and deeply. π§ β¨
This is the native Fetch APIβs Request
object β just like in the browser or Node.js.
It's used in Route Handlers (like GET
, POST
, etc.) inside app/api/*
.
// app/api/hello/route.ts
export async function GET(request: Request) {
const url = request.url;
return Response.json({ message: "Hello World" });
}
This extends the native Request with extra Next.js-specific goodies like:
cookies
β to easily read cookies (request.cookies.get("token")
)nextUrl
β full parsed URL with pathname, searchParams, etc.geo
β for geolocation (on Vercel)ip
β get user's IP (on Vercel)
It is used only in middleware, or sometimes in edge functions.
// middleware.ts
import { NextRequest, NextResponse } from "next/server";
export function middleware(request: NextRequest) {
const token = request.cookies.get("token");
const lang = request.nextUrl.searchParams.get("lang");
return NextResponse.next();
}
Context | Use | Why? |
---|---|---|
Route Handlers | Request |
Native, no need for Next-specific logic |
Middleware | NextRequest |
Access Next-specific features (nextUrl , cookies, IP) |
Edge Functions | NextRequest |
Same as above |
This is the standard Web Response used for returning data from route handlers, like GET()
:
export async function GET() {
return new Response("Hello", {
status: 200,
headers: {
"Content-Type": "text/plain",
},
});
}
Or using the utility method:
return Response.json({ data: "value" });
Perfect for route handlers β clean, standard, and enough in most cases.
This is used only in middleware and includes enhancements:
NextResponse.redirect(url)
NextResponse.rewrite(url)
- Cookie manipulation:
response.cookies.set()
andget()
- Modifying headers more easily
// middleware.ts
import { NextResponse } from "next/server";
export function middleware(request: NextRequest) {
const response = NextResponse.next();
response.cookies.set("visited", "true");
return response;
}
Context | Use | Why? |
---|---|---|
Route Handlers | Response |
Simpler, native, works well in API routes |
Middleware | NextResponse |
Needed for rewrite, redirect, set headers/cookies |
Advanced headers | NextResponse |
If modifying response during middleware phase |
// app/api/user/route.ts
export async function GET(request: Request) {
return Response.json({ user: "Skyy" });
}
β Works fine because we donβt need any Next.js-specific feature here.
// middleware.ts
import { NextRequest, NextResponse } from "next/server";
export function middleware(request: NextRequest) {
const isLoggedIn = request.cookies.get("token");
if (!isLoggedIn) {
return NextResponse.redirect(new URL("/#", request.url));
}
return NextResponse.next();
}
Here, NextRequest
gives access to cookies and NextResponse.redirect()
simplifies redirection.
Use:
import { cookies } from "next/headers";
export async function GET() {
const cookieStore = cookies();
const userToken = cookieStore.get("token")?.value;
return Response.json({ token: userToken });
}
Feature | Request | NextRequest | Response | NextResponse |
---|---|---|---|---|
Based On | Web API | Extends Request | Web API | Extends Response |
Available In | Route Handlers | Middleware | Route Handlers | Middleware |
Can use nextUrl |
β | β | β | β |
Can access cookies | β (manually) | β | β | β |
Can redirect | β | β | β (manual) | β |
Modify headers | β | β | β | β (easier) |