Unlock the power of Next.js App Router with our in-depth guide to file-based routing. Learn how to structure your application, create dynamic routes, handle layouts, and more.
Next.js App Router: A Comprehensive Guide to File-Based Routing
The Next.js App Router, introduced in Next.js 13 and becoming the standard in later versions, revolutionizes how we structure and navigate applications. It introduces a powerful and intuitive file-based routing system that simplifies development, improves performance, and enhances the overall developer experience. This comprehensive guide will delve deep into the App Router's file-based routing, providing you with the knowledge and skills to build robust and scalable Next.js applications.
What is File-Based Routing?
File-based routing is a routing system where the structure of your application's routes is directly determined by the organization of your files and directories. In the Next.js App Router, you define routes by creating files within the `app` directory. Each folder represents a route segment, and special files within those folders define how that route segment will be handled. This approach offers several advantages:
- Intuitive Structure: The file system mirrors the application's route structure, making it easy to understand and navigate.
- Automatic Routing: Next.js automatically generates routes based on your file structure, eliminating the need for manual configuration.
- Code Collocation: Route handlers and UI components are located together, improving code organization and maintainability.
- Built-in Features: The App Router provides built-in support for layouts, dynamic routes, data fetching, and more, simplifying complex routing scenarios.
Getting Started with the App Router
To use the App Router, you need to create a new Next.js project or migrate an existing project. Make sure you are using Next.js version 13 or later.
Creating a New Project:
You can create a new Next.js project with the App Router using the following command:
npx create-next-app@latest my-app --example with-app
Migrating an Existing Project:
To migrate an existing project, you need to move your pages from the `pages` directory to the `app` directory. You might need to adjust your routing logic accordingly. Next.js provides a migration guide to help you with this process.
Core Concepts of File-Based Routing
The App Router introduces several special files and conventions that define how your routes are handled:
1. The `app` Directory
The `app` directory is the root of your application's routes. All files and folders within this directory will be used to generate routes. Anything outside the `app` directory (like the `pages` directory if you are migrating) will be ignored by the App Router.
2. The `page.js` File
The `page.js` (or `page.jsx`, `page.ts`, `page.tsx`) file is the most fundamental part of the App Router. It defines the UI component that will be rendered for a specific route segment. It's a **required** file for any route segment you want to be directly accessible.
Example:
If you have a file structure like this:
app/
about/
page.js
The component exported from `app/about/page.js` will be rendered when a user navigates to `/about`.
// app/about/page.js
import React from 'react';
export default function AboutPage() {
return (
<div>
<h1>About Us</h1>
<p>Learn more about our company.</p>
</div>
);
}
3. The `layout.js` File
The `layout.js` (or `layout.jsx`, `layout.ts`, `layout.tsx`) file defines a UI that is shared across multiple pages within a route segment. Layouts are useful for creating consistent headers, footers, sidebars, and other elements that should be present on multiple pages.
Example:
Let's say you want to add a header to both the `/about` page and a hypothetical `/about/team` page. You can create a `layout.js` file in the `app/about` directory:
// app/about/layout.js
import React from 'react';
export default function AboutLayout({ children }) {
return (
<div>
<header>
<h1>About Our Company</h1>
</header>
<main>{children}</main>
</div>
);
}
The `children` prop will be replaced with the UI rendered by the `page.js` file in the same directory or in any nested directories.
4. The `template.js` File
The `template.js` file is similar to `layout.js`, but it creates a new instance of the component for each child route. This is useful for scenarios where you want to maintain component state or prevent re-renders when navigating between child routes. Unlike layouts, templates will re-render on navigation. Using templates is great for animating elements on navigation.
Example:
// app/template.js
'use client'
import { useState } from 'react'
export default function Template({ children }) {
const [count, setCount] = useState(0)
return (
<main>
<p>Template: {count}</p>
<button onClick={() => setCount(count + 1)}>Update Template</button>
{children}
</main>
)
}
5. The `loading.js` File
The `loading.js` (or `loading.jsx`, `loading.ts`, `loading.tsx`) file allows you to create a loading UI that is displayed while a route segment is loading. This is useful for providing a better user experience when fetching data or performing other asynchronous operations.
Example:
// app/about/loading.js
import React from 'react';
export default function Loading() {
return <p>Loading about information...</p>;
}
When a user navigates to `/about`, the `Loading` component will be displayed until the `page.js` component is fully rendered.
6. The `error.js` File
The `error.js` (or `error.jsx`, `error.ts`, `error.tsx`) file allows you to create a custom error UI that is displayed when an error occurs within a route segment. This is useful for providing a more user-friendly error message and preventing the entire application from crashing.
Example:
// app/about/error.js
'use client'
import React from 'react';
export default function Error({ error, reset }) {
return (
<div>
<h2>An error occurred!</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>Try again</button>
</div>
);
}
If an error occurs while rendering the `/about` page, the `Error` component will be displayed. The `error` prop contains information about the error, and the `reset` function allows the user to attempt to reload the page.
7. Route Groups
Route Groups `(groupName)` allow you to organize your routes without affecting the URL structure. They are created by wrapping a folder name in parentheses. This is particularly helpful for organizing layouts and shared components.
Example:
app/
(marketing)/
about/
page.js
contact/
page.js
(shop)/
products/
page.js
In this example, `about` and `contact` pages are grouped under the `marketing` group, and the `products` page is under the `shop` group. The URLs remain `/about`, `/contact`, and `/products`, respectively.
8. Dynamic Routes
Dynamic routes allow you to create routes with variable segments. This is useful for displaying content based on data fetched from a database or API. Dynamic route segments are defined by wrapping the segment name in square brackets (e.g., `[id]`).
Example:
Let's say you want to create a route for displaying individual blog posts based on their ID. You can create a file structure like this:
app/
blog/
[id]/
page.js
The `[id]` segment is a dynamic segment. The component exported from `app/blog/[id]/page.js` will be rendered when a user navigates to a URL like `/blog/123` or `/blog/456`. The value of the `id` parameter will be available in the `params` prop of the component.
// app/blog/[id]/page.js
import React from 'react';
export default async function BlogPost({ params }) {
const { id } = params;
// Fetch data for the blog post with the given ID
const post = await fetchBlogPost(id);
if (!post) {
return <p>Blog post not found.</p>;
}
return (
<div>
<h1>{post.title}</h1>
<p>{post.content}</p>
</div>
);
}
async function fetchBlogPost(id) {
// Simulate fetching data from a database or API
return new Promise((resolve) => {
setTimeout(() => {
const posts = {
'123': { title: 'My First Blog Post', content: 'This is the content of my first blog post.' },
'456': { title: 'Another Blog Post', content: 'This is some more exciting content.' },
};
resolve(posts[id] || null);
}, 500);
});
}
You can also use multiple dynamic segments in a route. For example, you could have a route like `/blog/[category]/[id]`.
9. Catch-all Segments
Catch-all segments allow you to create routes that match any number of segments. This is useful for scenarios like creating a CMS where the URL structure is determined by the user. Catch-all segments are defined by adding three dots before the segment name (e.g., `[...slug]`).
Example:
app/
docs/
[...slug]/
page.js
The `[...slug]` segment will match any number of segments after `/docs`. For example, it will match `/docs/getting-started`, `/docs/api/users`, and `/docs/advanced/configuration`. The value of the `slug` parameter will be an array containing the matched segments.
// app/docs/[...slug]/page.js
import React from 'react';
export default function DocsPage({ params }) {
const { slug } = params;
return (
<div>
<h1>Docs</h1>
<p>Slug: {slug ? slug.join('/') : 'No slug'}</p>
</div>
);
}
Optional catch-all segments can be created by adding the segment name in double square brackets `[[...slug]]`. This makes the route segment optional. Example:
app/
blog/
[[...slug]]/
page.js
This setup will render the page.js component both at `/blog` and `/blog/any/number/of/segments`.
10. Parallel Routes
Parallel Routes allow you to simultaneously render one or more pages in the same layout. This is particularly useful for complex layouts, such as dashboards, where different sections of the page can be loaded independently. Parallel Routes are defined using the `@` symbol followed by a slot name (e.g., `@sidebar`, `@main`).
Example:
app/
@sidebar/
page.js // Content for the sidebar
@main/
page.js // Content for the main section
default.js // Required: Defines the default layout for parallel routes
The `default.js` file is required when using parallel routes. It defines how the different slots are combined to create the final layout.
// app/default.js
export default function RootLayout({ children: { sidebar, main } }) {
return (
<div style={{ display: 'flex' }}>
<aside style={{ width: '200px', backgroundColor: '#f0f0f0' }}>
{sidebar}
</aside>
<main style={{ flex: 1, padding: '20px' }}>
{main}
</main>
</div>
);
}
11. Intercepting Routes
Intercepting Routes allow you to load a route from a different part of your application within the current layout. This is useful for creating modals, image galleries, and other UI elements that should appear on top of the existing page content. Intercepting Routes are defined using the `(..)` syntax, which indicates how many levels up the directory tree to go to find the intercepted route.
Example:
app/
(.)photos/
[id]/
page.js // The intercepted route
feed/
page.js // The page where the photo modal is displayed
In this example, when a user clicks on a photo in the `/feed` page, the `app/(.)photos/[id]/page.js` route is intercepted and displayed as a modal on top of the `/feed` page. The `(.)` syntax tells Next.js to look one level up (to the `app` directory) to find the `photos/[id]` route.
Data Fetching with the App Router
The App Router provides built-in support for data fetching using Server Components and Client Components. Server Components are rendered on the server, while Client Components are rendered on the client. This allows you to choose the best approach for each component based on its requirements.
Server Components
Server Components are the default in the App Router. They allow you to fetch data directly in your components without the need for separate API routes. This can improve performance and simplify your code.
Example:
// app/products/page.js
import React from 'react';
export default async function ProductsPage() {
const products = await fetchProducts();
return (
<div>
<h1>Products</h1>
<ul>
{products.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
</div>
);
}
async function fetchProducts() {
// Simulate fetching data from a database or API
return new Promise((resolve) => {
setTimeout(() => {
const products = [
{ id: 1, name: 'Product A' },
{ id: 2, name: 'Product B' },
{ id: 3, name: 'Product C' },
];
resolve(products);
}, 500);
});
}
In this example, the `fetchProducts` function is called directly within the `ProductsPage` component. The component is rendered on the server, and the data is fetched before the HTML is sent to the client.
Client Components
Client Components are rendered on the client and allow you to use client-side features like event listeners, state, and browser APIs. To use a Client Component, you need to add the `'use client'` directive at the top of the file.
Example:
// app/counter/page.js
'use client'
import React, { useState } from 'react';
export default function CounterPage() {
const [count, setCount] = useState(0);
return (
<div>
<h1>Counter</h1>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
In this example, the `CounterPage` component is a Client Component because it uses the `useState` hook. The `'use client'` directive tells Next.js to render this component on the client.
Advanced Routing Techniques
The App Router offers several advanced routing techniques that can be used to create complex and sophisticated applications.
1. Route Handlers
Route Handlers allow you to create API endpoints within your `app` directory. This eliminates the need for a separate `pages/api` directory. Route Handlers are defined in files named `route.js` (or `route.ts`) and export functions that handle different HTTP methods (e.g., `GET`, `POST`, `PUT`, `DELETE`).
Example:
// app/api/users/route.js
import { NextResponse } from 'next/server'
export async function GET(request) {
// Simulate fetching users from a database
const users = [
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Doe' },
];
return NextResponse.json(users);
}
export async function POST(request) {
const body = await request.json()
console.log('Received data:', body)
return NextResponse.json({ message: 'User created' }, { status: 201 })
}
This example defines a route handler at `/api/users` that handles both `GET` and `POST` requests. The `GET` function returns a list of users, and the `POST` function creates a new user.
2. Route Groups with Multiple Layouts
You can combine route groups with layouts to create different layouts for different sections of your application. This is useful for scenarios where you want to have a different header or sidebar for different parts of your site.
Example:
app/
(marketing)/
layout.js // Marketing layout
about/
page.js
contact/
page.js
(admin)/
layout.js // Admin layout
dashboard/
page.js
In this example, the `about` and `contact` pages will use the `marketing` layout, while the `dashboard` page will use the `admin` layout.
3. Middleware
Middleware allows you to run code before a request is handled by your application. This is useful for tasks like authentication, authorization, logging, and redirecting users based on their location or device.
Middleware is defined in a file named `middleware.js` (or `middleware.ts`) at the root of your project.
Example:
// middleware.js
import { NextResponse } from 'next/server'
export function middleware(request) {
// Check if the user is authenticated
const isAuthenticated = false; // Replace with your authentication logic
if (!isAuthenticated && request.nextUrl.pathname.startsWith('/admin')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
// See "Matching Paths" below to learn more
export const config = {
matcher: '/admin/:path*',
}
This example defines middleware that checks if the user is authenticated before allowing them to access any route under `/admin`. If the user is not authenticated, they are redirected to the `/login` page.
Best Practices for File-Based Routing
To make the most of the App Router's file-based routing system, consider the following best practices:
- Keep your file structure organized: Use meaningful folder names and group related files together.
- Use layouts for shared UI: Create layouts for headers, footers, sidebars, and other elements that are shared across multiple pages.
- Use loading UIs: Provide loading UIs for routes that fetch data or perform other asynchronous operations.
- Handle errors gracefully: Create custom error UIs to provide a better user experience when errors occur.
- Use route groups for organization: Use route groups to organize your routes without affecting the URL structure.
- Leverage server components for performance: Use server components to fetch data and render UI on the server, improving performance and SEO.
- Use client components when necessary: Use client components when you need to use client-side features like event listeners, state, and browser APIs.
Examples of Internationalization with Next.js App Router
The Next.js App Router simplifies internationalization (i18n) through file-based routing. Here's how you can implement i18n effectively:
1. Sub-path Routing
Organize your routes based on locale using sub-paths. For example:
app/
[locale]/
page.tsx // Home page for the locale
about/
page.tsx // About page for the locale
// app/[locale]/page.tsx
import { getTranslations } from './dictionaries';
export default async function HomePage({ params: { locale } }) {
const t = await getTranslations(locale);
return (<h1>{t.home.title}</h1>);
}
// dictionaries.js
const dictionaries = {
en: () => import('./dictionaries/en.json').then((module) => module.default),
es: () => import('./dictionaries/es.json').then((module) => module.default),
};
export const getTranslations = async (locale) => {
try {
return dictionaries[locale]() ?? dictionaries.en();
} catch (error) {
console.error(`Failed to load translations for locale ${locale}`, error);
return dictionaries.en();
}
};
In this setup, the `[locale]` dynamic route segment handles different locales (e.g., `/en`, `/es`). The translations are loaded dynamically based on the locale.
2. Domain Routing
For a more advanced approach, you can use different domains or subdomains for each locale. This often involves additional configuration with your hosting provider.
3. Middleware for Locale Detection
Use middleware to automatically detect the user's preferred locale and redirect them accordingly.
// middleware.js
import { NextResponse } from 'next/server';
import { match } from '@formatjs/intl-localematcher';
import Negotiator from 'negotiator';
let locales = ['en', 'es', 'fr'];
function getLocale(request) {
const negotiatorHeaders = {};
request.headers.forEach((value, key) => (negotiatorHeaders[key] = value));
let languages = new Negotiator({ headers: negotiatorHeaders }).languages();
try {
return match(languages, locales, 'en'); // Use "en" as the default locale
} catch (error) {
console.error("Error matching locale:", error);
return 'en'; // Fallback to English if matching fails
}
}
export function middleware(request) {
const pathname = request.nextUrl.pathname;
const pathnameIsMissingLocale = locales.every(
(locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`
);
if (pathnameIsMissingLocale) {
const locale = getLocale(request);
return NextResponse.redirect(
new URL(
`/${locale}${pathname.startsWith('/') ? '' : '/'}${pathname}`,
request.url
)
);
}
}
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
};
This middleware checks if the requested path has a locale prefix. If not, it detects the user's preferred locale using the `Accept-Language` header and redirects them to the appropriate locale-specific path. Libraries like `@formatjs/intl-localematcher` and `negotiator` are used to handle locale negotiation.
Next.js App Router and Global Accessibility
Creating globally accessible web applications requires careful consideration of accessibility (a11y) principles. The Next.js App Router provides a solid foundation for building accessible experiences, but it's essential to implement best practices to ensure your application is usable by everyone, regardless of their abilities.
Key Accessibility Considerations
- Semantic HTML: Use semantic HTML elements (e.g., `<article>`, `<nav>`, `<aside>`, `<main>`) to structure your content. This provides meaning to assistive technologies and helps users navigate your site more easily.
- ARIA Attributes: Use ARIA (Accessible Rich Internet Applications) attributes to enhance the accessibility of custom components and widgets. ARIA attributes provide additional information about the role, state, and properties of elements to assistive technologies.
- Keyboard Navigation: Ensure that all interactive elements are accessible via keyboard. Users should be able to navigate through your application using the `Tab` key and interact with elements using the `Enter` or `Space` key.
- Color Contrast: Use sufficient color contrast between text and background to ensure readability for users with visual impairments. The Web Content Accessibility Guidelines (WCAG) recommend a contrast ratio of at least 4.5:1 for normal text and 3:1 for large text.
- Image Alt Text: Provide descriptive alt text for all images. Alt text provides a text alternative for images that can be read by screen readers.
- Form Labels: Associate form labels with their corresponding input fields using the `<label>` element. This makes it clear to users what information is expected in each field.
- Screen Reader Testing: Test your application with a screen reader to ensure that it is accessible to users with visual impairments. Popular screen readers include NVDA, JAWS, and VoiceOver.
Implementing Accessibility in Next.js App Router
- Use Next.js Link Component: Use the `<Link>` component for navigation. It provides built-in accessibility features, such as prefetching and focus management.
- Focus Management: When navigating between pages or opening modals, ensure that focus is properly managed. The focus should be set to the most logical element on the new page or modal.
- Accessible Custom Components: When creating custom components, ensure that they are accessible by following the principles outlined above. Use semantic HTML, ARIA attributes, and keyboard navigation to make your components usable by everyone.
- Linting and Testing: Use linting tools like ESLint with accessibility plugins to identify potential accessibility issues in your code. Also, use automated testing tools to test your application for accessibility violations.
Conclusion
The Next.js App Router's file-based routing system offers a powerful and intuitive way to structure and navigate your applications. By understanding the core concepts and best practices outlined in this guide, you can build robust, scalable, and maintainable Next.js applications. Experiment with the different features of the App Router and discover how it can simplify your development workflow and improve the user experience.