TL;DR: Unlock the secrets of Next.js routing with this comprehensive guide! Discover how to effortlessly create routes with its file-based system—no external libraries needed, unlike React. Learn to master dynamic routes for flexible URLs and catch-all routes for complex subpaths. Dive into API routes to integrate powerful server-side logic right into your app. Plus, uncover how middleware can transform your request and response handling. Explore advanced techniques like client-side navigation with the Link component, performance-boosting prefetching, custom error pages, and seamless internationalization with sub-routing or domain-routing. Ready to elevate your web development game?
Next.js was introduced to extend React.js’s capabilities to server-side rendering and help fill in all the capabilities that it was missing.
Apart from server-side rendering and all other server-side capabilities that Next.js offers, what makes it stand out is its unique approach to handling routing through its file-based routing system.
Since its inception, React has relied on third-party libraries like React-Router and navigation capabilities. Next.js addresses this problem and comes with inbuilt routing capabilities that use the file structure to automatically create routes in the directory by mapping the files to the route using the filename.
This simplifies the development process by leveraging convention over configuration, cutting down the need for explicitly handling the routes through external libraries.
In this article, we will learn all the conventions by exploring the core concepts of the routing that you will need to know in Next.js.
Next.js uses file-based routing, where each page defined in the directory corresponds to the application routes.
Example:
/pages ├─ contact.js → "localhost.com/contact" (Contact page) ├─ services.js → "localhost.com/services" (Services page) ├─ article/index.js → "localhost.com/article" (Article list page) ├─ article/[id].js → "localhost.com/article/random-article" (Dynamic article page)
A JavaScript file .js, a JSX file .jsx, a TypeScript file .ts, a TSX file .tsx in Next.js under the pages directory will be treated as a route. The routing system automatically associates the respective files with the route without the need for extra configuration, making it easier for the devs to define the routes and abstract and isolate the logic inside the file.
This declarative approach of defining the routes improves the code maintainability and reduces the complexity of handling the navigation.
In Next.js, a file or folder placed under the square brackets like [id].js will be treated as the dynamic route that matches for all the values under its parent route, and the same page will be called for all these routes.
Dynamic routing is helpful in cases when you are not aware of the segment upfront. It helps to define a flexible route where one or many parts of the URL can be dynamic, and these routes are scalable.
Example:
/pages ├─ article/index.js → "localhost.com/article" (Article list page) ├─ article/[id].js → "localhost.com/article/article-1" (Dynamic article page) ├─ article/[article-category]/[article-id].js → "localhost.com/article/random-category/random-article" (Dynamic category-wise article page)
The segment defined under the square brackets [id] is accessible as a key with the help of the useRouter hook on the client-side and getServerSideProps or getInitialProps on the server-side.
Example: (useRouter)
/pages/article/[id].js → “localhost.com/article/random-article-1”, “localhost.com/article/random-article-2”
// Filepath - /pages/article/[id].js: import { useRouter } from 'next/router'; const Article = () => { const router = useRouter(); const { id } = router.query; return <h1>Article ID: {id}</h1>; }; export default Article;
Example: (getInitialProps)
Next.js also allows us the capability to define nested dynamic routes. Consider the following file structure as an example for the dynamic nested routes.
/pages ├── article │ ├── index.js │ └── [category] │ └── [id].js
// filepath - /pages/article/[category]/[id].js export default function Article({ id, category }) { return <h1>Category ID: {category}, Article ID: {id}</h1>; } Article.getInitialProps = ({ query: { id, category } }) => { return { id, category }; }
There will often be a case where you want to handle all the nested dynamic routes at once in a single file, rather than having the dynamic routes defined at every level. We can do so by using the rest operator to aggregate all the route paths […slug].js. This will capture all the subroutes as an array.
Example:
With the catch-all routes, dynamic routes can be handled at a single level in a much better way.
/pages ├── article │ ├── index.js │ └── [...slug].js
This will match every route except for the index route.
/pages/article/[…slug].js → “localhost.com/article/1”, “localhost.com/article/category-1/1″,”localhost.com/article/category-2/tag-2/2”
// filepath - /pages/article/[...slug].js: import { useRouter } from 'next/router'; const Article = () => { const router = useRouter(); const { slug } = router.query; return <p>URL path: {slug?.join(',')}</p>; }; export default Article;
If you wrap the dynamic routes in an additional square bracket like [[…slug]].js, then the catch-all routes become optional where the URL param is not mandatory.
With optional catch-all routes, as the URL params become optional, thus, this also matches the index path.
Example:
This is how we can define the nested optional catch-all routes.
/pages ├── article │ └── [[...slug]].js
/pages/article/[[…slug]].js → “localhost.com/article”,”localhost.com/article/1″, “localhost.com/article/category-1/1”,“localhost.com/article/category-2/tag-2/2”
In the order of sequence, the static route takes precedence over the dynamic route, and the dynamic route takes precedence over catch-all routes.
Next.js is a server-side rendered framework. Thus, it provides capability to build server-side logic like authentication, database operations, custom business logic with the same Next.js application codebase without the need for an external backend.
The folder /pages/api is the holding directory for the API routes, and all the routes under it are mapped to the api routes /api/*. They will be excluded from the normal page rendering and the client-side bundle, and will be treated as an API endpoint instead and will be part of server-side bundle.
We still follow the same file structure, the only difference is that the file under /pages/api will not be rendered as a page.
Example:
/pages/api/hello.js -> “localhost.com/api/hello”
// Filepath: /pages/api/hello.js async function apiHandler(req, res) { try { const result = await someAsyncOperationHandler(); res.status(200).json({ result }); } catch (err) { res.status(500).json({ error: 'Failed to process data' }); } } export default apiHandler;
We can also do dynamic routing with API routes along with catch-all routes.
/pages/api/article/[id].js -> “localhost.com/api/article/1”
// Filepath: /pages/api/article/[id].ts import type { NextApiRequest, NextApiResponse } from 'next'; async function apiHandler(req: NextApiRequest, res: NextApiResponse) { try { const { id } = req.query; const result = await getProductWithId(id as string); res.status(200).json({ result }); } catch (err) { res.status(500).json({ error: 'Failed to get product details' }); } } export default apiHandler;
We can also handle different HTTP methods efficiently in the API routes using the req parameter.
// Filepath: /pages/api/article/[id].ts import type { NextApiRequest, NextApiResponse } from 'next'; async function apiHandler(req: NextApiRequest, res: NextApiResponse) { try { if (req.method === 'POST') { // create action res.status(200).json({ message: 'Create action executed' }); } else if (req.method === 'PUT') { // update action res.status(200).json({ message: 'Update action executed' }); } else { // handle other HTTP methods res.status(405).json({ error: `Method ${req.method} not allowed` }); } } catch (err) { res.status(500).json({ error: 'Failed to get product details' }); } } export default apiHandler;
For the API routes, we can do a custom configuration, where we can set the max timeout limit, the body parser limit, etc.
Middleware helps us intercept the request before they reach the desired page and modify the response before it is sent to the browser.
Middleware is a powerful tool that helps with authentication and authorization, redirection, request logging, A/B testing, security checks, and other important activities that need to be done before the request reaches the application code.
Create a file named middleware.js in your root directory that will act as a middleware for your application code.
Example: Check if a user is currently authenticated to the system
import { isAuthenticated } from '@lib/auth'; export function middleware(request) { if (!isAuthenticated(request)) { // Return the error message in response when the user is not authenticated return Response.json( { message: 'authentication failed', success: false }, { status: 401 } ); } }
Example: Check if a user is authorized for route access
Middleware can be restricted to certain paths by changing the configuration.
// Restrict the middleware to paths starting with `/private/` export const config = { matcher: '/private/*', }; export function middleware(request) { if (!isAuthorized(request)) { // Return the error message in response if the user is not authorized return Response.json( { success: false, message: `You don't have permission to access this page` }, { status: 401 } ); } }
Example: Handle asynchronous operation
We can also handle asynchronous operations in the middleware and use the helper function to extend the lifetime of the middleware until the async operation is completed.
import { NextResponse } from 'next/server'; export function middleware(req, event) { event.waitUntil( fetch('https://large-product-list.com', { method: 'POST', body: JSON.stringify({ pathname: req.nextUrl.pathname }), }) ); return NextResponse.next(); }
There are multiple ways to handle redirects in Next.js.
Example: useRouter() hook for client-side redirects
import { useRouter } from 'next/router'; import { useEffect } from 'react'; export default function Article() { const router = useRouter(); useEffect(() => { router.push('/article-list'); }, []); return ( <h1>Article-list</h1> ); }
Example: Redirecting in the middleware
In the middleware function, we can check which path the user is trying to access and then redirect to the correct one.
For example, here we redirect the user to the login page when they are visiting the dashboard.
import { NextResponse } from 'next/server'; export function middleware(req) { if (req.nextUrl.pathname === '/dashboard') { return NextResponse.redirect('/login'); } return NextResponse.next(); }
Example: Redirect and Rewrites in the next.config.js
Custom redirects (permanent and temporary) and Rewrites can be defined at the universal level in the next.config.js, which will result in better route handling as we don’t have to do the configuration and manual handling at the code level, and it also improve the SEO experience by letting the bots be aware of the new paths upfront.
import type { NextConfig } from 'next'; const nextConfig: NextConfig = { async redirects() { return [ // Permanent redirect { source: '/old-route', destination: '/', permanent: true, }, // Temporary redirection for all the routes under old-route { source: '/old-route/:slug', destination: '/new-route/:slug', permanent: false, }, ]; }, async rewrites() { return [ { source: '/blog/:slug', destination: '/news/:slug', }, ]; }, }; export default nextConfig;
Next.js comes with an inbuilt Link component, which allows for client-side navigation similar to how you do it in a single-page application. This Link component is different from using the native <a> HTML element as it helps to preserve the component state, making the page transition and navigation smoother.
import Link from 'next/link'; const Sidebar = () => { return ( <div> <h1>Welcome to Syncfusion</h1> <Link href="/https/www.syncfusion.com/about-us">Syncfusion's About Page</Link> </div> ); }; export default Sidebar;
Next.js automatically prefetches the pages linked with the <Link/> component that are in viewport initially or when the page is scrolled. Prefetching helps to improve the user experience as it reduces the load times of the pages resulting in smoother navigation.
Next.js prefetches the page in the background when you hover over any link, considering it an indication that the user is about to click that page, and when clicked, the page will load faster. This behaviour can be controlled in case you need to disable it for performance issues.
<Link href="/https/www.syncfusion.com/about-us" prefetch={false}>Syncfusion's About Page</Link>
You can dynamically link to paths by mapping over the link and setting the correct path. Port link from next/link.
import Link from 'next/link'; function Articles({ articles }) { const getPath = (slug) => `/blog/${encodeURIComponent(slug)}`; return ( <ul className="sidenav"> {articles.map((article) => ( <li key={article.id}> <Link href={getPath(article.slug)}> {article.title} </Link> </li> ))} </ul> ); } export default Articles;
We can create custom error pages in next.js by creating a file named with the error code or group in the root directory.
Example: Custom 404 pages
Not found errors are usually thrown from the client-side.
// filepath - /pages/404.js const MyCustom404 = () => { return ( <div> <h1>404 - Page Not Found</h1> <p>Oops!, looks like we don't have what you are looking for, try exploring the website</p> </div> ); }; export default MyCustom404;
Example: Custom 500 pages
Servers usually throw a 500 error, and we can have a custom page for the same.
// filepath - /pages/500.js const MyCustom500 = () => { return ( <div> <h1>500 - Something went wrong!</h1> <p>Oops!, Looks like we are having some issues with our server</p> </div> ); }; export default MyCustom500;
Example: Universal error handler
You can also create universal error handler that could be used to handle unexpected error or all the types of error.
// filepath - /pages/_error.js function Error({ statusCode }) { return ( <p> {!statusCode ? 'There was an error on the client-side' : `There was an error with ${statusCode} occurred on server`} </p> ) } Error.getInitialProps = ({ res, err }) => { const statusCode = res ? res.statusCode : err ? err.statusCode : 404; return { statusCode } } export default Error;
Next.js comes with built-in support for the internationalization through two different ways to handle the locales.
We update the configuration and handle it accordingly.
Example: Sub-routing
// next.config.js module.exports = { i18n: { locales: ['en-IN', 'nl-NL', 'fr'], defaultLocale: 'en-IN', }, }
Defining the locales in the config will put the locales as a sub-route in the URL path.
For example, articles page with the file structure like pages/articles.js will be accessed like this for different locales.
Default locale - /articles 'nl-NL' - /nl-NL/articles 'fr' - /fr/articles
Example: Domain-routing
You can also configure the next.js to use different domains for different locales.
// next.config.js module.exports = { i18n: { locales: ['en-IN', 'nl-NL', 'nl-BE', 'fr', 'it-IT'], defaultLocale: 'en-IN', domains: [ { domain: 'localhost.com', defaultLocale: 'en-IN', }, { domain: 'localhost.fr', defaultLocale: 'fr', }, { domain: 'localhost.nl', defaultLocale: 'nl-NL', locales: ['nl-BE'], // other locales that could be accessed on this same domain }, { domain: 'localhost.italy.com', // subdomain defaultLocale: 'it-IT', }, ], }, }
You can access multiple locales on the same domain for locales that are somehow similar, like Urdu and Arabic.
Similarly, you will have to specify the subdomains in case you want to access the locale on that subdomain.
Default locale - localhost.com/articles 'nl-NL' - localhost.nl/articles 'nl-BE' - localhost.nl/articles 'fr' - localhost.fr/articles
Next.js drastically tries to improve the developer experience with its robust and scalable file-based dynamic routing system. With features like Middleware, API Routes, Internationalization, and custom error handling, makes it the ideal choice to create an enterprise-grade application.
You can check practical demonstrations regarding using Syncfusion with React NextJS in our demos. These demonstrations are designed to guide you through the integration process, showcasing various features and capabilities to enhance your projects.
Additionally, check the step-by-step guide for setting up a Next.js app and integrating the Syncfusion React components in the UG documentation.
Syncfusion’s React UI components library is the only suite you will ever need to build an app. It contains over 80 high-performance, lightweight, modular, and responsive UI components in a single package.
The latest version of Essential Studio® is available from the license and downloads page for our customers. If you’re not a Syncfusion® customer, try our 30-day free trial to evaluate our components.
If you have any questions or need assistance, you can reach us through our support forum, support portal, or feedback portal. We’re always here to help!