Next.js Routing

Routing is the most fundamental feature for any web application framework, allowing the users to navigate within the app by mapping URLs to webpages.

Next.js offers a file based routing system, where the folders and files under the app directory automatically translates to the routes for your application.

For example, pay attention to the page.jsx files, each of them corresponds to a webpage, and their location indicates the route to access these pages.

text
1app
2├── blog
3│   └── page.jsx       <===== /blog
4├── dashboard
5│   └── users
6│       └── page.jsx   <===== /dashboard/users
7├── favicon.ico
8├── fonts
9├── globals.css
10├── layout.js
11└── page.js            <===== /

Creating your first page (page.jsx)

Before we move on, let's take a closer look at the page.jsx. There are several special files in Next.js, and page.jsx is one of them. As we've mentioned above, each of them defines a webpage in your application.

Each page is a React component, which means it should export a function that returns a piece of JSX code, which will then be translated into HTML during rendering.

app/blog/page.jsx

jsx
1export default function Blog() {
2  return <div>This is the blog page.</div>;
3}

And to access this page, open the browser and go to http://localhost:3001/blog.

Next blog page

Dynamic routes with parameters

Next.js allows us to create dynamic routes as well.

Imagine you are creating a blog with different articles. Each of these articles corresponds to a different route. Of course, you can hardcode each page, but there is an easier way.

Instead of manually creating individual routes for each article, you can define a dynamic route to capture a segment of the URL as a parameter, such as slug, and then use the slug to dynamically fetch and display the content for that specific article.

In Next.js, you can use the square brackets to define dynamic routes:

text
1src
2├── app
3│   ├── blog
4│   │   └── [slug]         <===== Captures the URL segment
5│   │       └── page.jsx   <=====
6│   ├── dashboard
7│   │   └── users
8│   │       └── page.jsx
9│   ├── favicon.ico
10│   ├── fonts
11│   ├── globals.css
12│   ├── layout.js
13│   └── page.js
14└── components

With this configuration, the last segment of the URL will be captured and assigned to slug, which will then be passed to page.jsx.

For example, when you visit http://localhost:3001/blog/article1, the URL segment article1 will be captured as the parameter slug, and passed to the page.jsx file, which can be accessed via the params prop:

app/blog/[slug]/page.js

jsx
1export default async function BlogPost({ params }) {
2  const slug = (await params).slug;
3  return <div>Displaying Blog Post: {slug}</div>;
4}

Note that the params prop is a promise, which means you should use the async await syntax to access it.

This feature is recently introduced in Next.js 15, and for now, in order to maintain backward compatibility, you can still access it synchronously as a regular variable like this:

jsx
1export default function BlogPost({ params }) {
2  const slug = params.slug;
3  return <div>Displaying Blog Post: {slug}</div>;
4}

But this behavior is not recommended as it will be deprecated in the future.

Catch multiple segments with dynamic routes

In some cases, you might need to catch more than one URL segments. This can be done by creating a nested folder structure.

text
1app
2├── blog
3│   └── [category]         <===== Captures the URL segment category
4│       └── [slug]         <===== Captures the URL segment slug
5│           └── page.jsx   <===== Webpage
6├── dashboard
7├── favicon.ico
8├── fonts
9├── globals.css
10├── layout.js
11└── page.js

app/blog/[category]/[slug]/page.jsx

jsx
1export default async function BlogPost({ params }) {
2  const slug = (await params).slug;
3  const category = (await params).category;
4  return (
5    <div>
6      Displaying Blog Post {slug} under category {category}.
7    </div>
8  );
9}

Both category and slug can be accessed using the same syntax.

You can also capture all subsequent URL segments by adding an ellipsis, which is very similar to the spread syntax in JavaScript.

text
1app
2├── blog
3│   └── [...slug]      <===== Captures all subsequent URL segments and assign to slug
4│       └── page.jsx   <===== Webpage
5├── dashboard
6├── favicon.ico
7├── fonts
8├── globals.css
9├── layout.js
10└── page.js

In this case, this route will match /blog/a, /blog/a/b, /blog/a/b/c, and so on. The parameter slug will return an array of matched URL segments.

app/blog/[...slug]/page.jsx

jsx
1export default async function BlogPost({ params }) {
2  const slug = (await params).slug;
3
4  console.log(typeof slug);
5  console.log(slug);
6
7  return <div>Displaying Blog Post {slug}.</div>;
8}

Open the browser and visit http://localhost:3001/blog/author1/category2/article3/comment4, and the following output should be logged to the console:

text
1object
2[ 'author1', 'category2', 'article3', 'comment4' ]

This catch all syntax can also be made optional by wrapping the parameter name inside double square brackets.

text
1app
2├── blog
3│   └── [[...slug]]    <===== Optional catch all, matches /blog as well
4│       └── page.jsx   <===== Webpage
5├── dashboard
6├── favicon.ico
7├── fonts
8├── globals.css
9├── layout.js
10└── page.js

app/blog/[[...slug]]/page.jsx

jsx
1export default async function BlogPost({ params }) {
2  const slug = (await params).slug;
3
4  console.log(typeof slug);
5  console.log(slug);
6
7  return <div>Displaying Blog Post {slug}.</div>;
8}

The difference is that it matches /blog as well, in which case slug will return undefined.

Open the browser and visit http://localhost:3001/blog/author1/category2/article3/comment4, and the same output should be logged to the console:

text
1object
2[ 'author1', 'category2', 'article3', 'comment4' ]

But when you visit http://localhost:3001/blog, the same route will be matched, and the following output will be logged:

text
1undefined
2undefined