NextJS 15: Your Detailed Guide to Mastering It
Table of contents
- Chapter 1: Introduction to Next.js 15 and the App Router
- Chapter 2: Creating Your First Next.js 15 App
- Chapter 3: Client-Side and Server-Side Components in Next.js 15
- Chapter 4: Data Fetching in Next.js 15: Best Practices and Techniques
- Chapter 5: Handling Forms and User Input in Next.js 15
- Chapter 7: Dynamic Routes, Nested Routing and Route Groups in Next.js 15
- Chapter 8: How To Access Multiple Route Parameters and Query Parameters Both On The Client-Side and Server-Side In Next.js 15
- Chapter 9: Middleware and Edge Functions
- Chapter 10: Authentication and Authorization
- Chapter 11: Caching and Optimizing Data Fetching
- Chapter 12: API Routes and Route Handlers
- Chapter 13: API Rate Limiting and Security in Next.js 15
- Chapter 14: Environment Variables in Next.js 15
- Chapter 15: Deployment to Vercel
- Chapter 16: UI File Conventions in Next.js 15
Chapter 1: Introduction to Next.js 15 and the App Router
Welcome to the first step of your journey to mastering Next.js 15 with the App Router. If you’ve been coding in React for a while, you’ll love the simplicity Next.js brings to building full-fledged, production-ready apps. And with the latest updates in Next.js 15, things have gotten even more powerful, especially with the new App Router!
So, let's get started.
What is Next.js?
Next.js is a React framework that provides out-of-the-box solutions for building fast, scalable, and feature-rich web applications. It simplifies the entire process of setting up client-side routing, server-side rendering (SSR), static site generation (SSG), and much more. It takes care of the complex parts of building web apps while allowing you to focus on writing your actual features.
If you’re building web apps that need fast page loading, SEO optimization, or a backend API, Next.js has got you covered. In fact, it's perfect for modern web development with minimal boilerplate code.
What’s New in Next.js 15?
Next.js 15 introduces several new features, but the App Router is probably the biggest change you'll notice. The App Router brings a new way to structure and manage pages in your app, making things more flexible and modular. Here’s what it brings to the table:
File-based routing: You define routes by creating files inside the
app/
directory. Forget about manually setting up a router; the file structure does it for you.Client and Server Components: Components can either run on the client or server, depending on where they’re needed, giving you control over performance.
Layouts: You can now create layouts that are shared across multiple pages, reducing repetition and enhancing reusability.
Overview of the App Router
The App Router introduces a new app/
directory. Inside it, you define routes simply by creating folders and files. Each folder becomes a route in your app, and the files inside them represent the content for those routes.
Here’s a simple example of what the folder structure might look like:
my-app/
├── app/
├── layout.tsx
├── page.tsx
├── about/
├── page.tsx
├── contact/
├── page.tsx
app/
– This is the new directory where your app routes live.layout.tsx
– This defines a layout for your entire app. You can think of it as a template that wraps every page.page.tsx
– This represents the content for each route. Soapp/about/page.tsx
is the/about
page.
Code Example: Setting Up Your First Next.js App
Let’s set up a basic Next.js 15 project and add some routes using the App Router.
Install Next.js 15: Start by creating a new Next.js app. You can do this by running:
npx create-next-app@latest my-nextjs-app
Create the App Router Structure: Inside the
app/
directory, let’s create some basic pages.Here’s the file structure we’ll start with:
my-app/ ├── app/ ├── layout.tsx ├── page.tsx ├── about/ ├── page.tsx
App Layout (layout.tsx): This layout file wraps all pages with shared components (like headers, footers, or navigation bars). Here’s an example:
// app/layout.tsx export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> <header> <h1>My Next.js 15 App</h1> </header> <main>{children}</main> </body> </html> ); }
In this code:
We define a
RootLayout
component that wraps all pages in a basic layout. It includes a<header>
and<main>
section where the actual content of the page will be rendered.The
{children}
prop allows us to dynamically inject page content into the layout. This makes the layout reusable across all pages.
Homepage (page.tsx): Next, let’s create our homepage.
// app/page.tsx export default function HomePage() { return ( <div> <h2>Welcome to the Home Page</h2> <p>This is the homepage of our Next.js 15 app.</p> </div> ); }
This is a basic React component that will be displayed when you visit the
/
route.About Page (about/page.tsx): Let’s also add an about page.
// app/about/page.tsx export default function AboutPage() { return ( <div> <h2>About Us</h2> <p>We are building a modern app with Next.js 15.</p> </div> ); }
This will be shown at the
/about
route.
How Routing Works with the App Router
With the App Router, the file structure in your app/
directory dictates the routing structure. For example:
app/page.tsx
becomes the homepage (/
).app/about/page.tsx
becomes the/about
route.You can nest folders to create deeper routes, like
app/blog/post/page.tsx
for/blog/post
.
No need to configure a router manually anymore—Next.js handles that based on the folder structure.
Wrapping It Up
In this chapter, we’ve covered the basics of what Next.js 15 is, why it’s awesome, and how the new App Router simplifies routing. You’ve also seen how to set up a basic project structure with layouts and pages, which are automatically wired into the routing system.
Chapter 2: Creating Your First Next.js 15 App
Now that you’ve got a solid understanding of what Next.js 15 is and how the App Router works, let’s actually dive in and create our first full-blown Next.js app. This chapter will focus on getting the basics right: setting up a project, creating pages, and understanding how layouts work in practice. So, let’s roll up our sleeves and get to work.
Setting Up a Next.js 15 Project
If you haven’t already set up a Next.js project, it’s super simple. Just follow these steps:
Create a New Next.js Project: Open your terminal and run:
npx create-next-app@latest my-nextjs-app
This command scaffolds a new Next.js project in the
my-nextjs-app
directory. You can replacemy-nextjs-app
with whatever name you like.Install Dependencies: After running the above command, navigate to the project directory:
cd my-nextjs-app
Now install the dependencies:
npm install
Alternatively, if you’re using Yarn, you can run:
yarn install
Now you’re all set with your basic Next.js 15 project! 🎉
App Router: Pages and Layouts
In the previous chapter, we touched on the App Router and the new app/
directory. Now, we’ll dig deeper into how to create pages and how layouts help with reusability.
Creating a Home Page:
Every Next.js app starts with a homepage. You create this by defining a
page.tsx
file in theapp/
directory. Here’s the basic structure:// app/page.tsx export default function HomePage() { return ( <div> <h1>Welcome to My First Next.js 15 App</h1> <p>This is your homepage. Feel free to explore!</p> </div> ); }
This component will render whenever you visit the root URL (
/
) of your app. Notice that there’s no need for route configuration—Next.js handles that for you based on the file location.Adding an About Page:
Now let’s add another page, the About page. This page will live at
/about
.To create this page, we’ll need to create a folder named
about/
insideapp/
and add apage.tsx
file:my-nextjs-app/ ├── app/ ├── about/ ├── page.tsx
Now, add the following code inside the
about/page.tsx
file:// app/about/page.tsx export default function AboutPage() { return ( <div> <h1>About Us</h1> <p>We are building something amazing with Next.js 15.</p> </div> ); }
With this setup, the
/about
route will now display this page.
Layouts with the App Router
One of the coolest features of Next.js 15 is Layouts. Layouts allow you to share common components (like navigation, headers, footers) across multiple pages. This avoids duplication and keeps your codebase clean.
Creating a Layout:
A layout is defined inside the
layout.tsx
file in theapp/
directory. Every page that lives insideapp/
will automatically inherit the layout you define here.Let’s create a simple layout that includes a header and a footer:
// app/layout.tsx export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> <header> <h1>My Cool Next.js App</h1> <nav> <ul> <li><a href="/">Home</a></li> <li><a href="/about">About</a></li> </ul> </nav> </header> <main>{children}</main> <footer> <p>© 2025 My Cool Next.js App. All rights reserved.</p> </footer> </body> </html> ); }
Here’s what this layout does:
It defines a common
<header>
and<footer>
that will wrap every page in your app.The
{children}
prop represents the content of each individual page. For example, if you navigate to/about
, the content ofabout/page.tsx
will be rendered where{children}
is placed.There’s also a simple navigation
<nav>
with links to the home and about pages.
How Layouts Work:
The layout is applied to every page in your app automatically. You don’t have to manually import it in each page file. For instance, both the
app/page.tsx
(Home page) andapp/about/page.tsx
(About page) will be wrapped with this layout.Try running your app:
npm run dev
Now, when you navigate to
/
or/about
, you’ll see the layout with the header, navigation, and footer appearing around your page content.
Route Handlers (API Routes)
Next.js 15 also makes it super easy to create API routes that handle backend logic directly inside your app. With the App Router, you can define API endpoints by creating files in the app/
directory that map to API routes.
Creating an API Route:
Let’s create a simple API route that responds with a JSON object. First, create a new folder
api/hello/
and add aroute.ts
file:my-nextjs-app/ ├── app/ ├── api/ ├── hello/ ├── route.ts
Now, add the following code to
api/hello/route.ts
:// app/api/hello/route.ts import { NextResponse } from 'next/server'; export async function GET() { return NextResponse.json({ message: 'Hello, Next.js 15!' }); }
This is how the code works:
GET()
is an API route handler that runs when aGET
request is made to/api/hello
.It uses
NextResponse.json()
to return a JSON response with the messageHello, Next.js 15!
.
Accessing the API Route:
Now, if you navigate to
/api/hello
in your browser or use a tool like Postman orcurl
, you’ll see the JSON response:{ "message": "Hello, Next.js 15!" }
That’s it! You just created an API route without needing an Express.js server or any additional setup.
Wrapping It Up
In this chapter, we’ve walked through setting up your first Next.js 15 app using the App Router. You’ve seen how to:
Create pages inside the
app/
directory.Set up a layout that wraps all pages.
Create an API route to handle backend logic.
This foundation will help you build complex, scalable apps without breaking a sweat. Next up, we’ll explore the difference between Client and Server Components, and how you can leverage them to optimize your app’s performance.
Chapter 3: Client-Side and Server-Side Components in Next.js 15
In Next.js 15, a significant feature is the ability to use Client-Side and Server-Side components, thanks to the App Router. Understanding the distinction between these two types of components and when to use each is crucial for optimizing the performance of your app. This chapter will help you get comfortable with both, explaining what they are, when to use them, and providing examples.
Understanding the Difference
Server Components:
Server components are rendered on the server and then sent to the client as HTML.
They don’t include any JavaScript for the client. This means they are faster to load because they don’t need to download unnecessary JavaScript.
They are ideal for static content or data fetching that doesn’t need to run on the client.
Server components run on the server for every request. They’re rendered and serialized as HTML to the client.
Client Components:
Client components, on the other hand, are rendered in the browser. This means they run as JavaScript in the client environment.
They are ideal for interactivity, event handling, and other client-side behaviors like animations, form handling, or dynamic UI updates.
Client components can be used to make the app interactive without requiring a full-page reload.
So, when should you use each?
Use Server Components when you need static content or data fetching that can be done on the server. It’s especially useful for SEO.
Use Client Components for features that rely on user interactions, like buttons, forms, modals, etc.
Now that we understand the concepts, let’s dive into creating both types of components.
Server Components in Action
In Next.js 15, server components are the default behavior. If you create a .tsx
file in the app/
directory, it’s automatically treated as a server component.
Here’s how you can create a server component:
Create a Server Component:
Let’s create a simple server component that fetches some data from a placeholder API and displays it.
First, create a
posts/
folder in theapp/
directory and add apage.tsx
file inside it:my-nextjs-app/ ├── app/ ├── posts/ ├── page.tsx
Fetch Data in the Server Component:
Inside
page.tsx
, use thefetch
API to get data from a placeholder API like JSONPlaceholder. This data will be fetched on the server before rendering the page:// app/posts/page.tsx import React from 'react'; async function fetchPosts() { const res = await fetch('https://jsonplaceholder.typicode.com/posts'); const data = await res.json(); return data; } export default async function PostsPage() { const posts = await fetchPosts(); return ( <div> <h1>All Posts</h1> <ul> {posts.map((post: { id: number; title: string }) => ( <li key={post.id}> <h2>{post.title}</h2> </li> ))} </ul> </div> ); }
What’s Happening Here?:
The
fetchPosts
function is called on the server side before the page is rendered.Once the data is fetched, it’s passed to the component, which renders the posts in a list.
Since this is a server component, it doesn’t send any JavaScript to the client for rendering. Only the HTML is sent.
Now, when you visit /posts
, Next.js will fetch the posts data on the server, render the page with that data, and send it to the client.
Client Components in Action
To create a Client-Side Component, you need to explicitly declare it as a client component. You can do this by adding 'use client'
at the top of your component file.
Let’s say you want to add a like button to each post that updates dynamically on the client side. Here's how you can do it:
Create a Client Component:
In the same
posts/
folder, create aLikeButton.tsx
component:// app/posts/LikeButton.tsx 'use client'; import React, { useState } from 'react'; export default function LikeButton() { const [likes, setLikes] = useState(0); const handleLike = () => setLikes(likes + 1); return ( <button onClick={handleLike}> 👍 {likes} Likes </button> ); }
Notice the
'use client'
at the top. This tells Next.js that this component will run on the client side.Integrate Client Component into the Server Component:
Now, let’s integrate the
LikeButton
inside the server-rendered posts. Update theposts/page.tsx
to include the client-sideLikeButton
component:// app/posts/page.tsx import React from 'react'; import LikeButton from './LikeButton'; async function fetchPosts() { const res = await fetch('https://jsonplaceholder.typicode.com/posts'); const data = await res.json(); return data; } export default async function PostsPage() { const posts = await fetchPosts(); return ( <div> <h1>All Posts</h1> <ul> {posts.map((post: { id: number; title: string }) => ( <li key={post.id}> <h2>{post.title}</h2> <LikeButton /> </li> ))} </ul> </div> ); }
Why Split Between Client and Server?
So why is this split important? The main advantage is performance. Server components allow you to fetch data on the server, reducing the JavaScript sent to the client. Client components allow you to handle interactions without the overhead of unnecessary server calls.
Here are some practical use cases:
Server Components:
Fetching data from APIs or databases that don’t need to change dynamically.
Static content such as blog posts, product pages, etc.
Client Components:
Dynamic content like buttons, forms, and anything that needs to interact with the user on the client side.
Handling things like authentication, modals, or other interactivity.
Conclusion
In this chapter, we’ve learned how to:
Create Server-Side Components that fetch data and render HTML on the server.
Create Client-Side Components that allow for dynamic, interactive functionality on the client.
Combine both to optimize performance and enhance the user experience.
The key takeaway is that the App Router in Next.js 15 gives you the power to mix and match these two types of components, making your app more performant and efficient. Next up, we’ll dive into Data Fetching Techniques in Next.js 15, including how to fetch data both on the server and client, and when to use each.
Chapter 4: Data Fetching in Next.js 15: Best Practices and Techniques
One of the most important aspects of building applications with Next.js is how you handle data fetching. In Next.js 15, there are multiple ways to fetch data, whether it’s on the server during rendering, on the client-side after the page loads, or even on both. This chapter will walk you through all the different data fetching techniques in Next.js 15, their use cases, and how to implement them.
Overview of Data Fetching in Next.js 15
In Next.js, there are three main approaches to fetching data:
Server-Side Data Fetching (Server Components): Data is fetched and rendered on the server, then sent as static HTML to the client.
Client-Side Data Fetching (Client Components): Data is fetched directly in the browser after the page is loaded (e.g., through API calls).
Static Site Generation (SSG) / Incremental Static Regeneration (ISR): The page is pre-rendered at build time, and you can use incremental regeneration for updates.
Each of these methods has its use cases depending on the nature of your data and the need for interactivity. Let’s dive into each of these techniques and see when to use them.
Server-Side Data Fetching with Server Components
As we discussed in the previous chapter, Server Components fetch data server-side and don’t require any JavaScript to be sent to the client. This is great for static data that doesn’t change often.
Example: Fetching Data in a Server Component
Let’s fetch some posts from a public API, just like we did in the previous chapter, but with some improvements. This time, we’ll use async/await
to handle the fetching.
Create a Server Component to Fetch Posts:
In your
app/posts/page.tsx
:// app/posts/page.tsx import React from 'react'; async function fetchPosts() { const res = await fetch('https://jsonplaceholder.typicode.com/posts'); const data = await res.json(); return data; } export default async function PostsPage() { const posts = await fetchPosts(); return ( <div> <h1>All Posts</h1> <ul> {posts.map((post: { id: number; title: string }) => ( <li key={post.id}> <h2>{post.title}</h2> </li> ))} </ul> </div> ); }
In this example:
fetchPosts
fetches data from the API on the server side before rendering.The data is fetched and passed into the
PostsPage
component, which is then rendered and sent as HTML to the client.
Client-Side Data Fetching with Client Components
Sometimes, you need to fetch data after the page loads, especially when the data needs to change dynamically based on user interactions. This is where Client-Side Data Fetching comes in.
Example: Fetching Data in a Client Component
Let’s say we want to fetch user comments dynamically using client-side fetching.
Create a Client Component for Fetching Comments:
First, we need a new client component (
CommentSection.tsx
) to fetch and display comments after the page loads:
// app/posts/CommentSection.tsx
'use client';
import React, { useState, useEffect } from 'react';
export default function CommentSection({ postId }: { postId: number }) {
const [comments, setComments] = useState([]);
useEffect(() => {
const fetchComments = async () => {
const res = await fetch(`https://jsonplaceholder.typicode.com/comments?postId=${postId}`);
const data = await res.json();
setComments(data);
};
fetchComments();
}, [postId]);
return (
<div>
<h3>Comments</h3>
<ul>
{comments.map((comment: { id: number; name: string; body: string }) => (
<li key={comment.id}>
<strong>{comment.name}</strong>: {comment.body}
</li>
))}
</ul>
</div>
);
}
In this example:
We use
useEffect
to fetch comments once the component has mounted on the client.The
fetchComments
function makes the API request to retrieve comments based on thepostId
prop.
Integrate Client Component in the Post Page:
Now, we’ll integrate this
CommentSection
component inside the server-rendered posts page:
// app/posts/page.tsx
import React from 'react';
import CommentSection from './CommentSection';
async function fetchPosts() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
const data = await res.json();
return data;
}
export default async function PostsPage() {
const posts = await fetchPosts();
return (
<div>
<h1>All Posts</h1>
<ul>
{posts.map((post: { id: number; title: string }) => (
<li key={post.id}>
<h2>{post.title}</h2>
<CommentSection postId={post.id} />
</li>
))}
</ul>
</div>
);
}
Static Site Generation (SSG) with getStaticProps
For content that doesn’t change frequently, you can pre-render the page at build time using Static Site Generation. In Next.js 15, you can use getStaticProps
to fetch data during the build phase and pre-render the page.
Example: Using getStaticProps
to Fetch Posts
Modify the Post Page to Use
getStaticProps
:To fetch data at build time, you can add the
getStaticProps
function:// app/posts/page.tsx import React from 'react'; async function fetchPosts() { const res = await fetch('https://jsonplaceholder.typicode.com/posts'); const data = await res.json(); return data; } export async function getStaticProps() { const posts = await fetchPosts(); return { props: { posts }, }; } export default function PostsPage({ posts }: { posts: any[] }) { return ( <div> <h1>All Posts</h1> <ul> {posts.map((post: { id: number; title: string }) => ( <li key={post.id}> <h2>{post.title}</h2> </li> ))} </ul> </div> ); }
getStaticProps
fetches the posts data at build time and passes it as props to the component.This is useful for data that doesn’t change frequently but still needs to be pre-rendered for SEO and performance.
Incremental Static Regeneration (ISR)
If you want to regenerate static pages after deployment, Incremental Static Regeneration (ISR) is the way to go. ISR allows you to update static content on-demand without needing to rebuild the whole app.
Example: Using ISR
To enable ISR, simply add the revalidate
option in getStaticProps
:
// app/posts/page.tsx
import React from 'react';
async function fetchPosts() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
const data = await res.json();
return data;
}
export async function getStaticProps() {
const posts = await fetchPosts();
return {
props: { posts },
revalidate: 60, // Revalidate after 60 seconds
};
}
export default function PostsPage({ posts }: { posts: any[] }) {
return (
<div>
<h1>All Posts</h1>
<ul>
{posts.map((post: { id: number; title: string }) => (
<li key={post.id}>
<h2>{post.title}</h2>
</li>
))}
</ul>
</div>
);
}
revalidate: 60
means the page will be re-generated every 60 seconds.
Conclusion
In this chapter, we’ve learned how to handle data fetching in Next.js 15 with various techniques:
Server-Side Data Fetching in Server Components: Ideal for static data or data that doesn’t change frequently.
Client-Side Data Fetching in Client Components: Ideal for dynamic data and interactions.
Static Site Generation (SSG): Perfect for data that can be pre-rendered at build time.
Incremental Static Regeneration (ISR): A powerful feature that allows you to update static content without rebuilding the entire app.
Next, we’ll explore how to handle forms and user input, including validation and submission techniques.
Chapter 5: Handling Forms and User Input in Next.js 15
Forms and user input are fundamental to building interactive applications. Whether you're collecting data from users, managing form validation, or submitting forms to a backend, it's important to understand how to handle forms in Next.js 15 efficiently. This chapter will guide you through the process of building and managing forms, handling user input, and performing form validation.
Why Forms Are Important in Web Applications
Forms are the primary method of gathering input from users. They are used for a wide variety of tasks, including:
Registration and login
Feedback collection
Data submission (e.g., contact forms)
Dynamic interactions (e.g., filters or search)
In Next.js 15, you can handle forms seamlessly with both client-side and server-side capabilities, depending on the needs of your app.
Basic Form Handling in Next.js 15
Let’s start by creating a simple form that collects user data like name and email. We’ll handle the form input using React's controlled components.
Example: Creating a Basic Form
Create the Form Component:
In
app/contact/page.tsx
, let's create a form that collects a user’s name and email:```typescript 'use client';
import React, { useState } from 'react';
export default function ContactForm() { const [formData, setFormData] = useState({ name: '', email: '' });
const handleChange = (e: React.ChangeEvent) => { setFormData({ ...formData,
}); };
const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); // Here you would send formData to the server, e.g., via fetch or API call console.log('Form Submitted:', formData); };
return (
Contact Us
Here’s a breakdown of what’s happening:
* `useState` is used to manage the form data. The `formData` state holds the values of the form fields.
* `handleChange` updates the state when the user types into the form fields.
* `handleSubmit` handles the form submission. In this case, we’re just logging the `formData` to the console. In a real app, you would send this data to a backend or API.
---
## Handling Form Validation
For more complex forms, you'll need to validate user input before submitting it. Next.js doesn’t provide built-in form validation, but we can easily handle it with JavaScript.
Let’s enhance the contact form to include some basic validation, such as checking if the email is in a valid format.
##### Example: Adding Simple Validation
1. **Update the Form Component with Validation**:
We’ll validate the email field before allowing the form to be submitted.
```typescript
'use client';
import React, { useState } from 'react';
export default function ContactForm() {
const [formData, setFormData] = useState({ name: '', email: '' });
const [error, setError] = useState<string | null>(null);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData({
...formData,
[e.target.name]: e.target.value,
});
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Simple email validation
const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
if (!emailRegex.test(formData.email)) {
setError('Please enter a valid email address');
return;
}
setError(null);
// Submit the form data
console.log('Form Submitted:', formData);
};
return (
<div>
<h1>Contact Us</h1>
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="name">Name</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
required
/>
</div>
<div>
<label htmlFor="email">Email</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
required
/>
</div>
{error && <p style={{ color: 'red' }}>{error}</p>}
<button type="submit">Submit</button>
</form>
</div>
);
}
In this updated version:
emailRegex
is used to validate the email format.If the email is invalid, an error message is displayed.
If the email is valid, the form data is submitted without any errors.
Sending Form Data to a Backend
Next, let’s implement a way to actually send the form data to a backend API.
Set up a Backend API Route:
In Next.js, you can create API routes inside the
app/api
directory. Let’s create a route to handle form submissions.Create the file
app/api/contact/route.ts
:// app/api/contact/route.ts import { NextResponse } from 'next/server'; export async function POST(request: Request) { const { name, email } = await request.json(); // Here you can process the data, e.g., save it to a database or send an email console.log('Received form data:', { name, email }); return NextResponse.json({ message: 'Form submitted successfully!' }); }
In this file:
We’re using
NextResponse.json
to send a JSON response back to the client.The form data is extracted from the request body and logged (in a real app, you would store or send this data somewhere).
Updating the Frontend to Call the API
We’ll now modify the form submission logic to send the form data to the backend API we just created. We'll use the
fetch
API to send the data to thePOST
route atapi/contact
.Here’s the continuation of the form component:
```typescript 'use client';
import React, { useState } from 'react';
export default function ContactForm() { const [formData, setFormData] = useState({ name: '', email: '' }); const [error, setError] = useState(null); const [successMessage, setSuccessMessage] = useState(null);
const handleChange = (e: React.ChangeEvent) => { setFormData({ ...formData,
}); };
const handleSubmit = async (e: React.FormEvent) => { e.preventDefault();
// Simple email validation const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$/; if (!emailRegex.test(formData.email)) { setError('Please enter a valid email address'); return; }
setError(null);
// Sending form data to the backend API try { const response = await fetch('/api/contact', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(formData), });
if (!response.ok) { throw new Error('Something went wrong'); }
const result = await response.json(); setSuccessMessage(result.message); } catch (error) { console.error(error); setError('Failed to submit the form'); } };
return (
Contact Us
{error}
} {successMessage &&{successMessage}
} Submit
### Breakdown of the Code:
* `fetch` API:
* We use the `fetch` function to send a `POST` request to our backend API (`/api/contact`).
* We pass the form data (`formData`) as the request body, converting it to a JSON string using `JSON.stringify`.
* We also set the `Content-Type` header to `application/json` so that the backend knows we’re sending JSON data.
* **Error Handling**:
* If the response from the backend is not `ok` (i.e., the status code is not 200), an error is thrown.
* If the form submission fails (e.g., network issues), we display an error message.
* **Success Handling**:
* If the backend responds successfully, we extract the message from the response and display it in the UI using the `setSuccessMessage` state.
* **Conditional Rendering**:
* The form includes conditionally rendered error and success messages, so users get feedback about the form submission status.
---
### **Conclusion**
In this chapter, we covered the basics of handling forms in Next.js 15. We learned how to:
* Build a simple form and handle user input using React’s `useState`.
* Perform form validation to ensure data integrity.
* Send form data to a backend API using the `fetch` API and handle both success and error scenarios.
Forms are a critical part of many web applications, and mastering how to manage them in Next.js will help you build more interactive and user-friendly experiences.
# Chapter 6: **Working with Dynamic Data: Server-Side Rendering (SSR) and Static Site Generation (SSG) in Next.js 15**
Next.js provides powerful methods for fetching and rendering dynamic content on the server or at build time. This chapter will focus on two key concepts that help you handle dynamic data: **Server-Side Rendering (SSR)** and **Static Site Generation (SSG)**.
---
#### What Are SSR and SSG?
Before we dive into the implementation, let's break down the two rendering strategies:
* **Server-Side Rendering (SSR)**:
* With SSR, your page content is generated on the server for each request. When a user requests a page, Next.js will generate the HTML on the server and send it to the browser.
* SSR is useful when data changes frequently or depends on the user's request.
* **Static Site Generation (SSG)**:
* With SSG, the content is generated at build time. The HTML for each page is pre-rendered and served from a static file.
* SSG is great when the content doesn’t change often, like blog posts or marketing pages.
Next.js 15 simplifies the usage of both strategies with powerful hooks and methods. Let’s take a look at both approaches in this chapter.
---
## **Server-Side Rendering (SSR)** with `getServerSideProps`
SSR allows you to fetch dynamic data on each request. When the page is requested, the server generates the content on the fly, making it ideal for data that changes frequently (e.g., user profiles, real-time data, etc.).
In Next.js, SSR is handled by the `getServerSideProps` function.
##### Example: Fetching Data with SSR
Let’s say we want to display user details on a page. We'll fetch this data from an API on each request using `getServerSideProps`.
1. **Create the SSR Page**:
In `app/user/[id]/page.tsx`, we’ll create a page that fetches a user’s information based on their `id`:
```typescript
'use client';
import React from 'react';
type User = {
id: string;
name: string;
email: string;
};
export async function getServerSideProps({ params }: { params: { id: string } }) {
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${params.id}`);
const user: User = await res.json();
// If the user is not found, return a 404 status
if (!user) {
return {
notFound: true,
};
}
return {
props: { user },
};
}
export default function UserProfile({ user }: { user: User }) {
return (
<div>
<h1>User Profile</h1>
<p>ID: {user.id}</p>
<p>Name: {user.name}</p>
<p>Email: {user.email}</p>
</div>
);
}
Breakdown of the Code:
getServerSideProps
:This function is called on each request to the page. It allows you to fetch data dynamically and return it as props to the page component.
params.id
is extracted from the URL, and we fetch user data from thejsonplaceholder
API.If the user is not found, we return
notFound: true
to show a 404 page.
UserProfile
:This component receives the
user
data as props fromgetServerSideProps
.It displays the user's
id
,name
, andemail
on the page.
Benefits of SSR:
The page is generated dynamically for each request, making it perfect for content that changes often.
SEO-friendly because the page is fully rendered on the server before being sent to the client.
Static Site Generation (SSG) with getStaticProps
SSG, on the other hand, generates pages at build time. It’s useful for pages with content that doesn’t change frequently and can be pre-rendered once and served as static files.
In Next.js, you can fetch data at build time using getStaticProps
.
Example: Fetching Data with SSG
Let’s say we want to list all users on a page. Since the list doesn’t change frequently, we can fetch this data at build time.
Create the SSG Page:
In
app/users/page.tsx
, we’ll fetch the list of users at build time usinggetStaticProps
:'use client'; import React from 'react'; type User = { id: string; name: string; email: string; }; export async function getStaticProps() { const res = await fetch('https://jsonplaceholder.typicode.com/users'); const users: User[] = await res.json(); return { props: { users }, revalidate: 10, // Regenerate the page every 10 seconds }; } export default function UsersList({ users }: { users: User[] }) { return ( <div> <h1>Users List</h1> <ul> {users.map((user) => ( <li key={user.id}> {user.name} - {user.email} </li> ))} </ul> </div> ); }
Breakdown of the Code:
getStaticProps
:This function fetches the list of users from the
jsonplaceholder
API at build time.It returns the
users
as props to theUsersList
component.The
revalidate: 10
property tells Next.js to regenerate the page every 10 seconds, ensuring the data is relatively fresh but without rebuilding the page on every request.
UsersList
:- This component receives the
users
data as props and maps over the list of users to display them in an unordered list.
- This component receives the
Benefits of SSG:
Pages are generated at build time, meaning they are served instantly and with minimal load on the server.
Great for static content, such as blogs, product listings, or documentation.
SEO-friendly as the content is pre-rendered and fully available when crawlers visit.
Incremental Static Regeneration (ISR)
Next.js also supports Incremental Static Regeneration (ISR), which allows you to regenerate static pages on-demand without rebuilding the entire site. You can set up revalidate
to specify how frequently a page should be regenerated.
revalidate
: This is a time in seconds that defines how often a page should be regenerated.
For example, if you want the page to be regenerated every 10 seconds, you would set revalidate: 10
in getStaticProps
.
Conclusion
In this chapter, we covered the essentials of SSR and SSG in Next.js 15:
Server-Side Rendering (SSR) allows you to fetch dynamic data on each request, making it perfect for frequently changing content.
Static Site Generation (SSG) generates static pages at build time, making it ideal for content that doesn’t change often.
Incremental Static Regeneration (ISR) allows you to update static pages without a full rebuild, offering a great balance between static content and dynamic updates.
Understanding when and how to use SSR, SSG, and ISR is key to building efficient and performant Next.js applications.
Chapter 7: Dynamic Routes, Nested Routing and Route Groups in Next.js 15
Next.js makes it incredibly easy to work with dynamic routes, enabling you to create pages that can render different content based on the URL parameters. In this chapter, we will dive into how dynamic routes work, including how to set them up, pass data to them, and handle nested routing.
Dynamic Routes
Dynamic routes in Next.js allow you to create pages where parts of the URL are dynamic, meaning the content of the page changes based on these dynamic parts. You can create dynamic routes using bracket notation ([param]
).
Basic Dynamic Routes
To create a dynamic route, you’ll create a file with square brackets around the dynamic part of the URL.
For example, if you want a route that shows a user’s profile based on their id
, the URL would look like /user/1
, /user/2
, and so on.
File Structure:
To create a dynamic route, you’d place a file in your app like this:
app/user/[id]/page.tsx
Fetching Data Server-Side
Next.js allows you to fetch data for dynamic routes either server-side or client-side. When you need to fetch data before rendering a page, you can use
getServerSideProps
(SSR).Here’s an example of fetching user data based on the dynamic
id
:'use server'; import React from 'react'; type User = { id: string; name: string; email: string; }; export async function getServerSideProps({ params }: { params: { id: string } }) { const res = await fetch(`https://jsonplaceholder.typicode.com/users/${params.id}`); const user: User = await res.json(); if (!user) { return { notFound: true }; // If no user is found, show 404 } return { props: { user } }; } export default function UserProfile({ user }: { user: User }) { return ( <div> <h1>{user.name}'s Profile</h1> <p>Email: {user.email}</p> </div> ); }
getServerSideProps
: This function fetches the user data for a specific user based on theid
from the URL and passes it as a prop to the component.Accessing
params
: You access the dynamicid
fromparams.id
.
Client-Side Dynamic Routing
On the client-side, you can use Next.js’s
useRouter
hook to access the dynamic parts of the route.
'use client';
import React, { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
export default function UserProfileClient() {
const router = useRouter();
const { id } = router.query; // Access the dynamic id from the URL
const [user, setUser] = useState(null);
useEffect(() => {
if (id) {
fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
.then((res) => res.json())
.then((data) => setUser(data));
}
}, [id]);
if (!user) return <div>Loading...</div>;
return (
<div>
<h1>{user.name}'s Profile</h1>
<p>Email: {user.email}</p>
</div>
);
}
useRouter
: You can use theuseRouter
hook to access the dynamicid
from the URL on the client-side.router.query
: This object contains all the dynamic parameters from the URL.
Route Groups
In Next.js 15, Route Groups help you organize your routes more efficiently, especially when you have deeply nested routes or if you need to apply specific layouts and configurations to groups of routes.
What are Route Groups?
Route groups allow you to create nested routes without affecting the structure of your URL. A route group is a folder that starts with a (
symbol and it is not part of the final URL path.
For example, you could have the following structure:
app
├── blog
│ ├── (auth)
│ │ └── login
│ └── post
│ └── [slug]
│ └── page.tsx
Route Group
(auth)
: Theauth
group will not be part of the final URL. It’s used to organize related routes (like authentication routes).Nested
post/[slug]
: This is a dynamic route where each post’s content is loaded based on itsslug
.
Using Route Groups
File Structure for Route Groups:
Here’s an example structure that includes a route group for
auth
and a nested post route with dynamicslug
:app ├── blog │ ├── (auth) │ │ └── login │ └── post │ └── [slug] │ └── page.tsx
Dynamic Route in Post (
app/blog/post/[slug]/page.tsx
):
'use server';
import React from 'react';
export async function getServerSideProps({ params }: { params: { slug: string } }) {
const res = await fetch(`https://jsonplaceholder.typicode.com/posts?slug=${params.slug}`);
const post = await res.json();
if (!post) {
return { notFound: true };
}
return { props: { post } };
}
export default function PostPage({ post }: { post: { title: string, body: string } }) {
return (
<div>
<h1>{post.title}</h1>
<p>{post.body}</p>
</div>
);
}
The route
/blog/post/[slug]
will render a specific post dynamically based on theslug
in the URL.- The
auth
group allows for organizing authentication-related routes, such as login or registration pages, under/blog/(auth)/login
.
- The
Accessing Route Parameters in Client-Side and Server-Side
Client-Side Dynamic Routes:
You can access dynamic route parameters (like
id
orslug
) usinguseRouter
.For example, to get the
id
parameter from/user/[id]
:
'use client';
import { useRouter } from 'next/router';
const UserProfile = () => {
const router = useRouter();
const { id } = router.query;
return <div>User ID: {id}</div>;
};
Server-Side Dynamic Routes:
- On the server-side, you can access route parameters in
getServerSideProps
orgetStaticProps
depending on your rendering strategy.
- On the server-side, you can access route parameters in
'use server';
export async function getServerSideProps({ params }: { params: { id: string } }) {
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${params.id}`);
const user = await res.json();
return { props: { user } };
}
Conclusion
Dynamic Routes in Next.js make it easy to create pages that respond to changing parts of the URL (like
/user/[id]
or/post/[slug]
).Route Groups help you organize routes and apply specific layouts or configurations without affecting the final URL structure.
You can access dynamic route parameters both client-side (using
useRouter
) and server-side (usinggetServerSideProps
orgetStaticProps
).
With this knowledge, you're ready to manage complex routes in your Next.js app.
Chapter 8: How To Access Multiple Route Parameters and Query Parameters Both On The Client-Side and Server-Side In Next.js 15
Sure, let’s dive into how to access multiple route parameters and query parameters both on the client-side and server-side in Next.js 15. This will help you handle more complex routes and URLs that pass multiple dynamic values.
Accessing Multiple Params in Dynamic Routes
Client-Side:
When dealing with multiple dynamic route parameters, you can access them using the useRouter
hook.
Let’s say you have a route like /post/[category]/[id]
where:
category
andid
are the dynamic route parameters.
Example Route:
pages/post/[category]/[id].tsx
Accessing Multiple Params Client-Side:
'use client';
import { useRouter } from 'next/router';
const Post = () => {
const router = useRouter();
const { category, id } = router.query; // Destructure the multiple params
if (!category || !id) return <div>Loading...</div>;
return (
<div>
<h1>Category: {category}</h1>
<h2>Post ID: {id}</h2>
</div>
);
};
export default Post;
router.query
contains all the dynamic parameters passed in the URL (in this case,category
andid
).- The
category
andid
values are accessed fromrouter.query
.
- The
Server-Side:
To access multiple parameters on the server side, you would use getServerSideProps
in Next.js, where the route parameters are passed via the params
object.
Example Route:
app/post/[category]/[id]/page.tsx
Accessing Multiple Params Server-Side:
'use server';
export async function getServerSideProps({ params }: { params: { category: string, id: string } }) {
const { category, id } = params; // Destructure the multiple params
// Fetch data based on category and id
const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
const post = await res.json();
if (!post) {
return { notFound: true }; // If no post found, show 404
}
return {
props: { post, category },
};
}
export default function PostPage({ post, category }: { post: any, category: string }) {
return (
<div>
<h1>Category: {category}</h1>
<h2>Post Title: {post.title}</h2>
<p>{post.body}</p>
</div>
);
}
The
params
object containscategory
andid
as part of the URL path.- You can fetch data based on these parameters and pass them as props to the component.
Accessing Query Params (URL Query Parameters)
Query parameters are values that appear after the ?
in the URL (e.g., /post/[category]/[id]?sort=asc&filter=latest
).
Client-Side:
On the client-side, you can access query parameters using the useRouter
hook, and they will be in the router.query
object.
Example URL:
/post/[category]/[id]?sort=asc&filter=latest
Accessing Query Params Client-Side:
'use client';
import { useRouter } from 'next/router';
const Post = () => {
const router = useRouter();
const { category, id, sort, filter } = router.query; // Access multiple params and query params
if (!category || !id) return <div>Loading...</div>;
return (
<div>
<h1>Category: {category}</h1>
<h2>Post ID: {id}</h2>
<p>Sort: {sort}</p>
<p>Filter: {filter}</p>
</div>
);
};
export default Post;
router.query
now holds both the dynamic parameters (category
,id
) and the query parameters (sort
,filter
).- We’re using
sort
andfilter
to display the query parameters that came after the?
.
- We’re using
Server-Side:
You can also access query parameters server-side using getServerSideProps
. They are available in the query
object of the context
.
Example URL:
/post/[category]/[id]?sort=asc&filter=latest
Accessing Query Params Server-Side:
'use server';
export async function getServerSideProps({ params, query }: { params: { category: string, id: string }, query: { sort: string, filter: string } }) {
const { category, id } = params; // Access dynamic params
const { sort, filter } = query; // Access query parameters
// Fetch data based on category and id, you can also use query parameters to customize fetching
const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
const post = await res.json();
if (!post) {
return { notFound: true }; // If no post found, show 404
}
return {
props: { post, category, sort, filter },
};
}
export default function PostPage({ post, category, sort, filter }: { post: any, category: string, sort: string, filter: string }) {
return (
<div>
<h1>Category: {category}</h1>
<h2>Post Title: {post.title}</h2>
<p>{post.body}</p>
<p>Sort: {sort}</p>
<p>Filter: {filter}</p>
</div>
);
}
The
params
object contains the dynamiccategory
andid
, and thequery
object contains the query parameters (sort
,filter
).- The query parameters (
sort
,filter
) are passed alongside the dynamic route parameters (category
,id
) as props.
- The query parameters (
Key Points to Remember:
Client-Side (using
useRouter
):Dynamic route parameters are accessed via
router.query
.Query parameters are also accessed via
router.query
(similar to dynamic params).
Server-Side (using
getServerSideProps
):Dynamic route parameters are accessed via
params
.Query parameters are accessed via
query
.
Example URL:
/post/[category]/[id]?sort=asc&filter=latest
category
andid
are route params, andsort
andfilter
are query params.
Conclusion
Now you have a clear understanding of how to:
Access multiple route parameters on both the client and server-side.
Access query parameters (the part after the
?
in the URL) on both the client and server-side.
This should give you more flexibility when building routes and handling URLs with dynamic data in Next.js 15.
Let’s continue with the next chapter: "Middleware and Edge Functions".
Chapter 9: Middleware and Edge Functions
Middleware and edge functions in Next.js allow you to intercept and modify requests as they come in, giving you fine control over the user experience and security. With middleware, you can handle tasks like authentication, rate limiting, and request redirects. Edge functions allow you to run JavaScript at the edge, near the users, improving the performance of your application by reducing latency.
In this chapter, we will cover:
Writing and Using Middleware
Using Edge Functions for Performance
1. Writing and Using Middleware
Middleware in Next.js 15 lets you run code before a request is completed. You can use middleware for a variety of purposes such as:
Authentication: Check if a user is authenticated before allowing them to access a page.
Redirects: Redirect users based on their location or device.
Modifying Requests: Add headers, change request parameters, or transform requests before they hit your server.
How to Create Middleware
Middleware in Next.js is placed inside the middleware.ts
file at the root of your app. Here’s an example of basic middleware that redirects users if they aren’t authenticated.
Example:
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(req: NextRequest) {
const { pathname } = req.nextUrl;
/*
The line `const { pathname } = req.nextUrl;` is a JavaScript
destructuring assignment that extracts the `pathname` property from the `nextUrl` object,
which is a property of the `req` (request) object.
In the context of a Next.js application, `req.nextUrl` is typically used
in middleware to access the URL of the incoming request.
The `pathname` represents the path part of the URL,
which is the section after the domain name and before any query parameters or hash fragments.
This is useful for routing and handling requests based on the specific path being accessed.
*/
// If user is not authenticated and is trying to access a protected route
if (pathname.startsWith('/dashboard') && !req.cookies.has('authToken')) {
return NextResponse.redirect(new URL('/login', req.url));
/*
new URL('/login', req.url): This constructs a new URL object.
The first argument '/login' is the path to which you want to redirect the user.
The second argument req.url is the base URL, which ensures that the redirect is relative
to the current request's URL.
*/
}
// Continue with the request
return NextResponse.next();
}
// Define paths to apply middleware to
export const config = {
matcher: ['/dashboard/:path*'], // Apply to dashboard routes
};
Explanation:
middleware
is an exported function that runs on every request.The
pathname
is checked to see if it matches a protected route like/dashboard
. If the user is not authenticated (noauthToken
cookie), they are redirected to the login page.The
NextResponse
object is used to control the flow of requests, either continuing them withNextResponse.next
()
or redirecting/rewriting them.matcher
is used to specify which routes the middleware applies to. Here, it’s protecting the/dashboard
route.
Key Use Cases for Middleware:
Protecting routes based on authentication.
Performing A/B testing by splitting users into groups dynamically.
Redirecting users based on country or device type.
2. Using Edge Functions for Performance
Edge functions allow you to run code closer to the user, reducing latency and speeding up response times. Edge functions are executed at the CDN edge, making them a powerful tool for improving performance in regions far from your origin server.
Example:
export const config = {
runtime: 'edge', // Specify this page as an Edge function
};
export default async function handler(req: Request) {
const res = await fetch('https://api.example.com/data');
const data = await res.json();
return new Response(JSON.stringify(data), {
headers: { 'Content-Type': 'application/json' },
});
}
Explanation:
The
runtime: 'edge'
in the config ensures that the code runs on the CDN edge.The
handler
function fetches data from an external API, processes it, and returns it as a response.This dramatically reduces latency and speeds up data fetching when the user is geographically distant from the origin server.
Chapter 10: Authentication and Authorization
When building full-stack apps, handling user authentication and authorization is a critical aspect. In Next.js 15, the App Router makes it easier to integrate authentication and authorization mechanisms while maintaining a scalable structure.
In this chapter, we'll cover:
Handling Authentication with the App Router
Middleware for Authorization
1. Handling Authentication with the App Router
In Next.js, you can use popular authentication services like Clerk, Auth0, or NextAuth.js to handle user authentication. These services allow you to authenticate users using social logins (Google, GitHub), password-based logins, or even token-based authentication like OAuth and JWT.
Setting Up Authentication in Next.js
Let’s see an example using NextAuth.js, a widely used library for authentication.
Step-by-Step Example:
Install NextAuth.js: First, you need to install the NextAuth.js package:
npm install next-auth
Create API Route for Authentication: Create a file
app/api/auth/[...nextauth]/route.ts
to configure NextAuth.js.// app/api/auth/[...nextauth]/route.ts import NextAuth from 'next-auth'; import GoogleProvider from 'next-auth/providers/google'; export const authOptions = { providers: [ GoogleProvider({ clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, }), ], secret: process.env.NEXTAUTH_SECRET, }; const handler = NextAuth(authOptions); export { handler as GET, handler as POST };
Explanation:
GoogleProvider: Configures Google as the authentication provider using client credentials.
The
route.ts
file acts as an API route that handles authentication logic.We configure both GET and POST requests to be handled by NextAuth.
Create a Login Button:
After setting up authentication, you can create a simple login button using the
signIn
function from NextAuth.// components/LoginButton.tsx 'use client'; import { signIn } from 'next-auth/react'; const LoginButton = () => { return ( <button onClick={() => signIn('google')}> Sign in with Google </button> ); }; export default LoginButton;
Explanation:
The
signIn('google')
method triggers the Google sign-in process.The
use client
directive at the top makes this component a client-side component that can use hooks and trigger user interactions.
Protecting Routes (Client-Side Authentication):
Once users log in, you can protect certain routes using the
useSession
hook from NextAuth. This checks if a user is authenticated before allowing them to view protected pages.// app/dashboard/page.tsx 'use client'; import { useSession } from 'next-auth/react'; const Dashboard = () => { const { data: session } = useSession(); if (!session) { return <p>Loading...</p>; } return ( <div> <h1>Welcome, {session.user?.name}!</h1> <p>Email: {session.user?.email}</p> </div> ); }; export default Dashboard;
Explanation:
useSession
retrieves the session data for the logged-in user.If the user is not authenticated, it displays a loading message until the session is fetched.
2. Middleware for Authorization
Authorization determines whether an authenticated user has the right permissions to access a resource. Middleware can be used to check if a user is logged in or has the correct permissions before accessing protected routes.
Example: Protecting Pages with Middleware
To protect specific routes, you can implement custom authorization logic in middleware.ts
:
// middleware.ts
import { NextResponse } from 'next/server';
import { getToken } from 'next-auth/jwt';
import type { NextRequest } from 'next/server';
export async function middleware(req: NextRequest) {
const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET });
// If there's no token and user is trying to access a protected route, redirect to login
if (!token && req.nextUrl.pathname.startsWith('/admin')) {
return NextResponse.redirect(new URL('/login', req.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/admin/:path*'], // Protect admin routes
};
Explanation:
getToken
is used to extract the user’s authentication token from the request.If there’s no token and the user tries to access
/admin
or any admin-related routes, they are redirected to the login page.
What did we do?:
We integrated NextAuth.js for handling authentication using Google OAuth.
We used the
useSession
hook to manage authenticated users in client-side components.Middleware helped us protect routes and implement custom authorization logic.
Chapter 11: Caching and Optimizing Data Fetching
One of the key benefits of Next.js is its ability to optimize data fetching. In this chapter, we will explore various caching strategies and how to optimize data fetching in Next.js 15 using techniques like fetch
caching and ISR (Incremental Static Regeneration).
We will cover:
Caching with fetch
Revalidation with ISR
1. Caching with fetch
In Next.js 15, caching is built into the data fetching process, allowing for improved performance and better resource management. The fetch
API can automatically cache responses to avoid redundant network calls.
How Caching Works with fetch
The fetch
function in Next.js 15 supports built-in caching behavior. You can specify the cache
and next
options to configure caching strategies.
Example: Static Caching with fetch
// app/dashboard/page.tsx
export default async function DashboardPage() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts', {
cache: 'force-cache', // Cache the response
});
const posts = await res.json();
return (
<div>
<h1>Dashboard</h1>
<ul>
{posts.map((post: any) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
Explanation:
cache: 'force-cache'
: Forces the fetch request to use the cached version of the data if it exists, otherwise, it fetches from the network.The data from
https://jsonplaceholder.typicode.com/posts
is cached on the server and reused on subsequent requests.
Cache Options:
cache: 'no-store'
: Skips the cache and fetches fresh data on every request.cache: 'force-cache'
: Forces the use of cached data if available, otherwise fetches it.next: { revalidate: number }
: Specifies a revalidation period for cached data.
Example: Revalidation with fetch
You can also use stale-while-revalidate caching by setting the next.revalidate
option. This tells Next.js to serve stale cached data while fetching the latest version in the background.
// app/dashboard/page.tsx
export default async function DashboardPage() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts', {
next: { revalidate: 60 }, // Revalidate every 60 seconds
});
const posts = await res.json();
return (
<div>
<h1>Dashboard</h1>
<ul>
{posts.map((post: any) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
Explanation:
next: { revalidate: 60 }
: The cached data will be considered fresh for 60 seconds. After 60 seconds, a new version will be fetched, but users will still see the cached version until the new data arrives.This ensures the page is highly performant and avoids constant network calls.
2. Revalidation with ISR (Incremental Static Regeneration)
Incremental Static Regeneration (ISR) allows you to update static pages after they've been generated without rebuilding the entire app. You can specify how often a page should be revalidated using the revalidate
option in your server-side fetching logic.
Example: ISR in a Next.js Page
// app/blog/[id]/page.tsx
export async function generateStaticParams() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
const posts = await res.json();
return posts.map((post: any) => ({
id: post.id.toString(),
}));
}
export default async function BlogPost({ params }: { params: { id: string } }) {
const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${params.id}`, {
next: { revalidate: 120 }, // Revalidate this page every 2 minutes
});
const post = await res.json();
return (
<div>
<h1>{post.title}</h1>
<p>{post.body}</p>
</div>
);
}
Explanation:
generateStaticParams
: Pre-generates all the possible pages for each blog post. This works well with ISR.next: { revalidate: 120 }
: This specific page will be revalidated every 2 minutes.The combination of static generation and ISR allows you to generate pages at build time but still keep them up to date over time.
Why Use ISR?
ISR is perfect for pages that are mostly static but need occasional updates, such as blogs, product catalogs, or dashboard statistics. It combines the performance benefits of static generation with the flexibility of server-side rendering.
What did we do?:
We explored caching with fetch, including different caching strategies like
force-cache
andno-store
.We saw how revalidation can keep your data up to date with
next.revalidate
while optimizing performance.ISR (Incremental Static Regeneration) allows you to statically generate pages and keep them updated by revalidating data at intervals.
Chapter 12: API Routes and Route Handlers
In Next.js 15, API routes and route handlers offer a powerful way to create backend functionality directly within your application. This chapter will cover the following topics:
Understanding Route Handlers
File-based API Routing
Handling Dynamic API Routes
Let’s break it down using an e-commerce app example, as it will give you a more realistic use case. We will cover the following:
API Route Handlers for managing products and orders
Dynamic API Routes for fetching specific products or orders by ID
Handling Query Parameters for filtering products (e.g., by category, price range)
1. API Route Handlers for Managing Products and Orders
In an e-commerce app, you would need API routes to handle products, users, and orders. Let’s start by setting up routes for products.
Creating an API Route for Products (GET & POST)
To manage products, you would typically need a route to get a list of products and another route to add a new product to the inventory. We can achieve this with a single route that handles both GET and POST requests.
File: app/api/products/route.ts
// app/api/products/route.ts
import { NextResponse } from 'next/server';
const products = [
{ id: 1, name: 'Laptop', price: 1000 },
{ id: 2, name: 'Phone', price: 500 },
];
export async function GET() {
// Return the list of products
return NextResponse.json({ products });
}
export async function POST(request: Request) {
// Extract product details from the request body
const newProduct = await request.json();
// Add the new product to the inventory (in a real app, you'd store it in the database)
products.push({ id: products.length + 1, ...newProduct });
// Return the newly added product
return NextResponse.json({ product: newProduct });
}
Explanation:
GET
: Fetches and returns the list of available products.POST
: Adds a new product to the list of products. The product data is extracted from the request body, added to theproducts
array, and returned in the response.
Example:
To fetch all products, send a GET request to /api/products
.
To add a new product, send a POST request to /api/products
with the product details in the body (e.g., name and price).
2. Dynamic API Routes for Fetching Specific Products
Now, let’s say you want to fetch a specific product by its ID, or fetch the details of an order placed by a user. This is where dynamic routes come into play.
Dynamic Route for Fetching a Product by ID
In your e-commerce app, you’d want to allow users to see a product’s details when they click on it. We can create a dynamic route for that.
File: app/api/products/[id]/route.ts
// app/api/products/[id]/route.ts
import { NextResponse } from 'next/server';
const products = [
{ id: 1, name: 'Laptop', price: 1000 },
{ id: 2, name: 'Phone', price: 500 },
];
export async function GET(request: Request, { params }: { params: { id: string } }) {
const { id } = params;
// Find the product by its ID
const product = products.find((p) => p.id === parseInt(id));
if (!product) {
return NextResponse.json({ error: 'Product not found' }, { status: 404 });
}
// Return the product details
return NextResponse.json({ product });
}
Explanation:
params
: Contains the dynamic route parameters, in this case, theid
of the product.The route fetches the product by ID from the
products
array. If the product is found, it is returned; otherwise, a 404 error is returned.
Example:
To get the details of the product with ID 1, you would send a GET request to /api/products/1
.
3. Handling Query Parameters for Filtering Products
Let’s say you want to filter products by a specific category (e.g., "electronics") or filter by price range (e.g., products cheaper than $1000). Query parameters are perfect for this.
API Route for Filtering Products by Category
You can modify your products API to accept query parameters for filtering:
File: app/api/products/route.ts
// app/api/products/route.ts
import { NextResponse } from 'next/server';
const products = [
{ id: 1, name: 'Laptop', price: 1000, category: 'electronics' },
{ id: 2, name: 'Phone', price: 500, category: 'electronics' },
{ id: 3, name: 'Shoes', price: 150, category: 'fashion' },
];
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
// Get the category and price filter from the query params
const category = searchParams.get('category');
const maxPrice = searchParams.get('maxPrice');
let filteredProducts = products;
// Apply category filter if provided
if (category) {
filteredProducts = filteredProducts.filter((p) => p.category === category);
}
// Apply price filter if provided
if (maxPrice) {
filteredProducts = filteredProducts.filter((p) => p.price <= parseInt(maxPrice));
}
// Return the filtered products
return NextResponse.json({ products: filteredProducts });
}
Explanation:
searchParams
: Extracts query parameters from the URL (likecategory
andmaxPrice
).Filters the
products
array based on the category and price, if these query parameters are present.
Example:
To fetch all electronics products, send a GET request to
/api/products?category=electronics
.To fetch products under $500, send a GET request to
/api/products?maxPrice=500
.To combine filters, send
/api/products?category=electronics&maxPrice=1000
.
Accessing Params and Query Params in Client-Side and Server-Side
Client-Side: You can access URL parameters and query parameters using the
useParams()
anduseSearchParams()
hooks in the client components.Example: Accessing Query Params Client-Side
import { useSearchParams } from 'next/navigation'; const ProductList = () => { const searchParams = useSearchParams(); const category = searchParams.get('category'); const maxPrice = searchParams.get('maxPrice'); return ( <div> <h1>Products</h1> <p>Category: {category}</p> <p>Max Price: {maxPrice}</p> </div> ); };
Server-Side: You can access dynamic and query parameters in API routes using the
params
andsearchParams
objects as shown in the examples.
// app/api/products/route.ts
import { NextResponse } from 'next/server';
const products = [
{ id: 1, name: 'Laptop', price: 1000, category: 'electronics' },
{ id: 2, name: 'Phone', price: 500, category: 'electronics' },
{ id: 3, name: 'Shoes', price: 150, category: 'fashion' },
];
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
// Get query params
const category = searchParams.get('category');
const minPrice = searchParams.get('minPrice');
const maxPrice = searchParams.get('maxPrice');
let filteredProducts = products;
// Apply category filter if provided
if (category) {
filteredProducts = filteredProducts.filter((p) => p.category === category);
}
// Apply price range filter if provided
if (minPrice) {
filteredProducts = filteredProducts.filter((p) => p.price >= parseInt(minPrice));
}
if (maxPrice) {
filteredProducts = filteredProducts.filter((p) => p.price <= parseInt(maxPrice));
}
// Return the filtered list of products
return NextResponse.json({ products: filteredProducts });
}
Explanation:
searchParams
extracts query parameters from the request URL.You can access each query parameter using
searchParams.get()
.In this example, we filter the products based on the
category
and the price range (minPrice
andmaxPrice
).
Example:
A request to /api/products?category=electronics&maxPrice=800
would return the products under the electronics category with a price less than or equal to $800.
Accessing Query and Dynamic Params in Server-Side Page Components
If you want to access both query parameters and dynamic route parameters in server-side rendered pages (page.tsx
or layout.tsx
), you can do so using the params
and searchParams
objects in Next.js 15.
Example: Accessing Route Params and Query Params in page.tsx
File: app/products/[category]/[id]/page.tsx
// app/products/[category]/[id]/page.tsx
import { useSearchParams } from 'next/navigation';
interface ProductPageProps {
params: { category: string; id: string };
searchParams: { [key: string]: string | undefined };
}
const ProductPage = ({ params, searchParams }: ProductPageProps) => {
const { category, id } = params;
const color = searchParams.color;
return (
<div>
<h1>Product Details</h1>
<p>Category: {category}</p>
<p>Product ID: {id}</p>
<p>Selected Color: {color}</p>
</div>
);
};
export default ProductPage;
Explanation:
params
: Contains the dynamic route parameters likecategory
andid
.searchParams
: Contains the query parameters likecolor
, which are passed in the URL.
Example:
A request to /products/electronics/1?color=red
would display:
Category: electronics
Product ID: 1
Selected Color: red
Recap:
In this chapter, we explored:
How to create API routes for managing products and orders.
Setting up dynamic API routes to fetch product details by ID.
Handling query parameters to filter products by category and price.
Chapter 13: API Rate Limiting and Security in Next.js 15
When building applications that handle a large number of requests, rate limiting and securing your APIs become essential to prevent misuse and ensure smooth operation. In Next.js 15, you can implement rate limiting and improve security by leveraging middleware, API routes, and additional security features.
Let’s dive into how you can integrate rate limiting and security mechanisms into your Next.js 15 app.
API Rate Limiting
Rate limiting helps prevent abuse of your API by limiting the number of requests a user or client can make in a specified period. This is especially important when dealing with external services, public APIs, or apps that are exposed to a large number of users.
How Rate Limiting Works:
The server keeps track of the number of requests made by a client within a given time window (e.g., 100 requests per minute).
If the client exceeds the limit, they are blocked or throttled.
Implementing Rate Limiting with Middleware
In Next.js, you can write a middleware to track the number of requests and apply limits. Let’s use an in-memory store (like a Map
) for simplicity, but for production, you would typically use a more robust store like Redis.
Here’s how to implement basic rate limiting:
- Create a Middleware for Rate Limiting
File: middleware.ts
// middleware.ts
import { NextResponse } from 'next/server';
import { NextRequest } from 'next/server';
// In-memory store to track IPs and request counts
const requestCounts = new Map<string, { count: number; timestamp: number }>();
const RATE_LIMIT = 100; // Max requests per minute
const TIME_WINDOW = 60 * 1000; // 1 minute
export function middleware(req: NextRequest) {
const ip = req.ip || 'unknown'; // Get the client's IP address
const currentTime = Date.now();
const data = requestCounts.get(ip) || { count: 0, timestamp: currentTime };
// Reset count if the time window has passed
if (currentTime - data.timestamp > TIME_WINDOW) {
data.count = 0;
data.timestamp = currentTime;
}
// If the limit is exceeded, return a 429 status
if (data.count >= RATE_LIMIT) {
return NextResponse.json(
{ message: 'Too many requests, please try again later' },
{ status: 429 }
);
}
// Increment the request count
data.count++;
requestCounts.set(ip, data);
return NextResponse.next();
}
export const config = {
matcher: '/api/*', // Apply rate limiting to all API routes
};
Explanation:
In-memory store: We use a simple
Map
to track request counts for each IP address.Rate limit check: We check if the number of requests exceeds the limit (
RATE_LIMIT
). If so, we return a429 Too Many Requests
response.Time window: We reset the count for each IP address after a certain time window (in this case, 1 minute).
NextResponse.next
()
: If the limit is not exceeded, the request proceeds to the next handler.
Note: This is a simple implementation. In production, using a more persistent and scalable solution like Redis is recommended.
Securing Your API Routes
API security involves protecting your routes from unauthorized access, misuse, and attacks like SQL injection, XSS, CSRF, and more. Here’s how you can secure your APIs in Next.js 15.
1. API Authentication with JWT Tokens
One of the most common ways to secure your API routes is by requiring authentication tokens (like JWT) for users to access certain endpoints.
Here’s a simple example of how you might implement JWT-based authentication in an API route:
// app/api/secure-endpoint/route.ts
import { NextResponse } from 'next/server';
import { verifyJwt } from '@/utils/jwt'; // A utility function to verify JWT
export async function GET(req: Request) {
// Get the JWT from the Authorization header
const token = req.headers.get('Authorization')?.split(' ')[1]; // "Bearer <token>"
if (!token) {
return NextResponse.json({ error: 'Authorization token missing' }, { status: 401 });
}
try {
const decoded = verifyJwt(token); // Assuming verifyJwt verifies the token
return NextResponse.json({ message: 'Access granted', user: decoded });
} catch (error) {
return NextResponse.json({ error: 'Invalid or expired token' }, { status: 403 });
}
}
Explanation:
JWT verification: We extract the JWT from the
Authorization
header and verify it using a utility function (verifyJwt
).Error handling: If the token is missing, invalid, or expired, we return the appropriate error response with a
401
or403
status.
2. Protecting Routes with Middleware
You can also use middleware to restrict access to certain routes based on user authentication or authorization.
Here’s how you could secure an API route with middleware:
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
export function middleware(req: NextRequest) {
const token = req.headers.get('Authorization')?.split(' ')[1];
if (!token) {
return NextResponse.json({ message: 'Unauthorized' }, { status: 401 });
}
// Here, you would verify the JWT token, for example
const isValid = verifyJwt(token); // Utility function
if (!isValid) {
return NextResponse.json({ message: 'Forbidden' }, { status: 403 });
}
return NextResponse.next(); // Proceed if token is valid
}
3. Protecting Sensitive Data
You can use environment variables to store sensitive information (e.g., API keys, database credentials). In Next.js, these environment variables can be accessed both server-side and client-side (when exposed).
Example: Using Environment Variables for API Keys
// .env.local
NEXT_PUBLIC_API_URL="https://api.example.com"
SECRET_API_KEY="your-secret-api-key"
// server-side code to use API key
const apiKey = process.env.SECRET_API_KEY; // Server-side access only
- Make sure to prefix sensitive environment variables (like
SECRET_API_KEY
) withoutNEXT_PUBLIC_
to ensure they’re only available server-side.
Other Security Best Practices
Cross-Origin Resource Sharing (CORS): Ensure that only trusted domains can access your API.
CSRF Protection: Use anti-CSRF tokens to protect against cross-site request forgery.
Content Security Policy (CSP): Define a strict CSP header to mitigate XSS attacks.
Input Validation: Always validate and sanitize user input to prevent SQL injection, XSS, and other common security vulnerabilities.
Recap:
Rate Limiting: Helps to prevent abuse by limiting the number of requests from a client.
Implemented via middleware.
Stores request counts in-memory (or Redis in production).
API Security:
Use JWT tokens to authenticate and authorize users.
Secure routes via middleware and JWT validation.
Protect sensitive data using environment variables and secure headers.
By combining rate limiting and security measures, you can ensure that your API is both protected and performant, providing a better experience for legitimate users while preventing abuse.
Chapter 14: Environment Variables in Next.js 15
Environment variables are essential for storing sensitive information (like API keys, database credentials) and configuration settings that differ between environments (development, staging, production). In Next.js 15, handling environment variables is straightforward and provides a layer of security and flexibility in configuring your application.
How Environment Variables Work in Next.js 15
Next.js 15 allows you to define environment variables both locally in your project and through Vercel (or any deployment platform). You can access these variables both server-side and client-side, but the rules differ slightly depending on whether you want the variable to be exposed to the client.
- Defining Environment Variables
You define environment variables in the .env.local
file for local development or .env.production
for production.
Server-side environment variables should be defined without the
NEXT_PUBLIC_
prefix to ensure that they are not exposed to the client.Client-side environment variables should be prefixed with
NEXT_PUBLIC_
, making them accessible on the client side as well.
Example: .env.local
# For server-side use only
DB_PASSWORD=mysecretpassword
API_SECRET_KEY=my-secret-api-key
# For client-side use
NEXT_PUBLIC_API_URL=https://api.example.com
NEXT_PUBLIC_MAP_API_KEY=your-google-map-api-key
- Accessing Environment Variables
- Server-side: You can access variables directly in API routes or any server-side functions using
process.env
.
// Accessing a server-side variable
const dbPassword = process.env.DB_PASSWORD;
- Client-side: Use the
NEXT_PUBLIC_
prefix to access environment variables in components.
// Accessing a client-side variable
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
- Security Considerations
Sensitive information should never be exposed to the client. Always use the
NEXT_PUBLIC_
prefix for variables that should be client-accessible.Private keys, tokens, and other sensitive data should only be used on the server side.
Chapter 15: Deployment to Vercel
Deploying your Next.js app to Vercel is one of the simplest and most seamless experiences, as Vercel is the platform created by the same team behind Next.js. Vercel automatically configures your environment to work with Next.js apps out of the box.
Steps to Deploy a Next.js App to Vercel
Create a Vercel Account
Go to Vercel and create an account if you don’t have one.
You can sign up with GitHub, GitLab, or your email.
Link Your Repository
After logging into Vercel, click on New Project and link your GitHub (or GitLab) repository containing your Next.js app.
Vercel will automatically detect your Next.js project and configure the deployment.
Deploy
Vercel will start the build process as soon as you link your repository. It will detect your Next.js app, install dependencies, and build the project.
After the build, Vercel will deploy your app and provide a URL for the live site.
Environment Variables on Vercel
You can set environment variables directly in the Vercel dashboard by navigating to the Settings tab of your project and then to Environment Variables.
These can be defined for Development, Preview, and Production environments.
Automatic Deployments
Vercel automatically deploys every change made to your repository. Every push to branches like
main
orproduction
will trigger a production deployment.For feature branches or pull requests, Vercel creates a preview deployment for testing.
Custom Domain
- You can add a custom domain in the Domains section of the Vercel dashboard.
Chapter 16: UI File Conventions in Next.js 15
Next.js 15 introduces several special UI file conventions that help in optimizing your app's performance, handling errors gracefully, and providing better user experiences. These files are used in specific cases like handling loading states, displaying errors, and more.
1. loading.tsx
/ loading.js
The loading.tsx
file is used to display a loading state while waiting for a server-side operation (like fetching data) to complete. It is especially useful in server-side rendering (SSR) or static site generation (SSG) for pages that need to load dynamic content.
Where it is used: It's typically used in server-side routes or server components.
Purpose: It provides a smooth user experience by showing a loading indicator (e.g., a spinner) while the content is being fetched.
Example: loading.tsx
// app/products/loading.tsx
export default function Loading() {
return (
<div className="spinner">
<p>Loading products...</p>
</div>
);
}
How it works: Next.js will automatically render the
loading.tsx
file in place of the content while the page is being fetched or server-side data is being loaded.Why it's useful: Instead of leaving a blank page or a flickering content area, users see a loading spinner, improving the user experience.
2. error.tsx
/ error.js
The error.tsx
file is used to handle errors that occur during rendering or fetching data. You can customize this page to show a user-friendly error message when something goes wrong.
Where it is used: It's used in specific pages or components that may encounter errors.
Purpose: It allows you to handle unexpected errors gracefully without crashing the entire page.
Example: error.tsx
// app/products/error.tsx
export default function ErrorPage() {
return (
<div>
<h2>Something went wrong!</h2>
<p>Please try again later.</p>
</div>
);
}
How it works: If an error occurs during rendering or data fetching in the component or page, Next.js will automatically display the content in
error.tsx
.Why it's useful: It improves the user experience by ensuring that users see an appropriate error message instead of a broken page.
3. not-found.tsx
The not-found.tsx
file is used to handle "404 Not Found" errors. This file is rendered when a user visits a page that doesn’t exist.
Where it is used: In the root directory or within a specific route.
Purpose: To provide a customized 404 page for your application.
Example: not-found.tsx
// app/not-found.tsx
export default function NotFoundPage() {
return (
<div>
<h1>Page Not Found</h1>
<p>Sorry, the page you are looking for does not exist.</p>
</div>
);
}
How it works: This page is automatically rendered when a user tries to visit a page that doesn't exist in the app.
Why it's useful: A custom 404 page helps users understand the error and possibly navigate to other parts of your site.
Recap of Key Points
Environment Variables:
Store sensitive information securely and access them using
process.env
.Use
NEXT_PUBLIC_
for client-side variables and leave sensitive information on the server-side.
Deployment to Vercel:
Vercel simplifies deployment and provides a seamless integration with Next.js.
Environment variables are configurable on the Vercel dashboard for different environments.
UI File Conventions:
loading.tsx
: Displays a loading state when fetching data.error.tsx
: Handles errors gracefully when something goes wrong.not-found.tsx
: Provides a custom 404 error page when a user visits a non-existent page.
These features help improve performance, security, and the overall user experience of your Next.js 15 app.