Intro
Hi, guys!
In this post we're going to talk about Next.js routing. This time we will only cover the core concepts of routing and in another post we will learn more about advanced concepts for a complete guide.
In this guide I am going to use TypeScript. If you use JavaScript, just remove the types where necessary.
Topics
The Core Route Files
To define routes in Next.js, we use file-system based routing, which mean we use files and folders from within our app.
When installing the Next.js app using:
npx create-next-app@latest
Next.js will create a folder app. You can safely delete everything inside it to remove the noise. This is of course not necessary, but I recommend doing it just to see every file in action without those boilerplate code from Next.js.
Now, let's create our first page, adding a page.tsx file inside app folder. This is actually the index page or homepage that you will see if you access http://localhost:3000.
But don't access it right now! Because you just deleted whatever was inside the app folder (if you followed my bad advice, kidding), your app is now missing essential parts, namely layout.tsx file. If you were to try to access http://localhost:3000 now without this file, you will see this error:
Missing <html> and <body> tags in the root layout
To fix this error from above, we have to create a layout.tsx file like so inside app folder:
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
{children}
</body>
</html>
)
};
Notice the default export, it is mandatory.
This file's purpose is about sharing UI between pages. It's called this way because it's defined at the root of the app directory. They can say it better than me:
A layout is UI that is shared between multiple pages. On navigation, layouts preserve state, remain interactive, and do not rerender. The root layout is required and must contain html and body tags.
Imagine header, footer, aside, they persist between pages. They don't change no matter what page you are on.
The children of this function are pages and nested layout. The UI that persists, in our case Header and Footer components, is going to be a sibling of these children:
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<Header />
{children}
<Footer />
</body>
</html>
)
};
Keep in mind that, when creating a page, you must default export it. Otherwise, you will see a similar error:
The default export is not a React Component
export default function Page() {
return <h1>Hello Next.js!</h1>
}
To create a route to /about or /contact or any other single routes you can think of, inside app folder, you create a new folder whose name should match the route you need. Inside this folder, it is mandatory to have a file called page.tsx.
Keep in mind that a folder inside app folder is a route only if it includes a page.tsx file.
Apart from page.tsx and layout.tsx, there is also not-found.tsx. By default, if you access a page that doesn't exist, Next.js has your back and you will see the 404 status code and a message This page could not be found.
If you like to customize it, inside app folder, add a file not-found.tsx and inside it add some code:
export default function NotFound() {
return <div>Not found!!!</div>
};
You can also have not-found.tsx files inside your pages folders and style it differently. Next.js will look for the closest one.
So there you have it, three core files in Next.js: page.tsx, layout.tsx and not-found.tsx.
Dynamic Routing
Dynamic Routing allows us to render content for an infinite number of URL paths. It's essential for rendering product pages, blog posts, or user profiles or anything you could think of, as long as it will take an infinite amount to code manually. In the App Router, we define these dynamic segments using square brackets [].
For example, instead of manually creating a route for each individual blog post, you can create a dynamic segment to generate the routes based on blog post data.
- 1. Nested Routes
A nested route is a route composed of multiple URL segments. Something like this: /blog/posts/post-one. To create a nested route, for instance, just nest folders into each other:
Create a blog folder, inside it create a posts folder, then inside posts folder create post-one folder... I think you got the idea. And don't forget to include the page.tsx files inside them.
- 2. Dynamic Segment
This is the most common one.
In your file structure it will look like this: app/products/[slug]/page.tsx.
This segment will match /products/123 or /products/222 or /products/1000 or anything else... Could even match not only a number, since the url is a string anyway, but things like /products/shoes or /products/books.
The value (123 or 222 or shoes or books) is accessible in your page.tsx component via the params prop:
export default async function Product(
props: PageProps<'/products/[slug]'>
) {
const { slug } = await props.params
return <h1>Blog post: {slug}</h1>
}
Read down below about this Typescript helper: PageProps.
- 3. Catch-all Segments
We use these when we need a route to match paths with one or more segments or even none (a documentation site). To use it, just think of a simple dynamic segment and add three dots before the name like so:
app/docs/[...slug]/page.tsx
This can match /docs/v1/api or /docs/get-started or /docs/v1/api/blabla or just /docs.
The parameters are received as an array of strings and you can access them by their index. So let's say the url is /docs/v1/api:
export default async function Docs(
props: PageProps<'/docs/[...slug]'>
) {
const array = (await params).slug;
return <h1>Blog post: {slug}</h1>
}
In this case array will be ['v1', 'api'].
Something to notice, there should be at least one segment present in the URL, like /docs/api, whenever in the folder that contains the catch-all slug there is no page.tsx. So for example if in the folder docs you don't have a page.tsx, navigating to /docs will result in a 404 Not Found error. This can be fixed with an optional catch-all segment or just add a page.tsx inside the parent folder of the slug.
- 4. Optional Catch-all Segments
This is the most flexible one, allowing the route to match paths with zero or more segments.
To use it, wrap a normal catch-all segment with a new set of square brackets: app/settings/[[...slug]]/page.tsx.
This can match /settings and /settings/profile/edit or /settings/profile/edit/any-other-settings.
So what is the difference between this and a normal catch-all segment? The difference is that it also handles the base path /settings without needing a separate page.tsx file.
So to access these paths, it's not very different:
export default async function Settings(
props: PageProps<'/settings/[[...slug]]'>
) {
const array = (await params).slug;
return <h1>Blog post: {slug}</h1>
}
So in case the url is /settings/account/name, for example, the array will be ['account', 'name']. If the url consists of the base only, /settings, array will be undefined, and the same in the case of a normal catch-all segment.
These dynamic patterns are the core of building scalable URL structures in Next.js. Mastering them gives you full control over your application's routes. And the good part is that they don't look so intimidating...
Typescript Helpers
When using TypeScript, we need a way to tell our components exactly what types of data they get as props. The App Router introduces two helper types that handle this by reading your file structure: PageProps and LayoutProps.
PageProps, it is used with page.tsx files. Let's imagine this piece of code, manually typed, ugly:
export default async function Page(
{ params }:
{ params: Promise<{ slug: string }>}
) {
const slug = (await params).slug;
return <h1>Blog post: {slug}</h1>
}
Now this type is automatically aware of the [slug] segment and PageProps automatically infers { id: string } for params.
export default async function Page(props: PageProps<'/blog/[slug]'>) {
const { slug } = await props.params
return <h1>Blog post: {slug}</h1>
}
The LayoutProps helper is designed to type the components responsible for the UI structure: your layout.tsx files.
// LayoutProps ensures 'children' is correctly typed as a node.
export default function ProductsLayout({ children }: LayoutProps) {
return (
<section>
<h1>Products Navigation</h1>
{/* The entire nested route's content renders here */}
{children}
</section>
);
}
So how do helpers help?
Using these helpers is the standard and recommended practice because they keep your component types in sync with your routing, eliminate manual types and prevents errors.
These helpers ensure that if you ever change a folder name from [productId] to [itemId], TypeScript will update the required type in your page.tsx, and your code editor will send an error if you try to access the old prop name.
Conclusion
If you happened to read this far, you did notice the basics are not so hard to grasp. They are real fun, actually, I'd say. They simplify the process of routing. Compared to React Routing, where you have to install a third party dependency (React Router Dom), Next.js Routing is more friendly and effective. It's just there.
Feedback
If you did read until here, thank you so much! I hope you learned something today and I didn't waste your time.
As always, I am waiting for your feedback. I want to improve my teaching style, so you can tell me what I do wrong. I take no offense. But do offend me, I challenge you. :) Maybe we'll all learn something after all. Even tell me if you like me to write a specific post. I am happy to hear your ideas.
Connect with me
Leave me a message and I'll get in touch as soon as possible!
- Write me an Email: alexandru.ene.dev@gmail.com
- Follow me and see my work on Github: alexandru-ene-dev
- Let's be friends on: LinkedIn: alexandru-ene-dev
- Let's chat on Discord: alexandru.ene.dev
Thank you so much again for your time and I will see you in the next one soon! Happy coding! :)
Top comments (6)
Great breakdown, Alexandru! 👏
You made Next.js routing feel simple and approachable — the examples for dynamic, catch-all, and optional catch-all were especially helpful.
Would love to see an advanced part covering nested layouts and parallel routes next!
Thank you! Coming soon! :)
Great fundamental guide! You've clearly covered the distinction between page.tsx and layout.tsx, and the TypeScript helpers are a vital tip for the App Router. The dynamic routing examples were excellent!
Thank you so much! :)
You make a very good job with this post!👏🏻 Will be very useful for me in the future! 🥰
Good luck with Next.js an Keep coding! 🤗🥰
Thank you so much! :)