[RFC] Dynamic Routes
See original GitHub issueDynamic Routes
Background
Dynamic routing (also known as URL Slugs or Pretty/Clean URLs) has been a long-time requested feature of Next.js.
Current solutions involve placing a L7 proxy, custom server, or user-land middleware in-front of your application. None of these solutions offer a sufficiently ergonomic developer experience.
Additionally, users reaching for a custom server inadvertently opt-out of advanced framework-level features like per-page serverless functions.
Goals
- Leverage convention to provide URL Slug support that is easy to reason about
- Cover a majority of use-cases observed in the wild
- Eliminate the need of a custom server to support
/blog/:post - Validate
<Link />route transitions when possible - Avoid an implementation that requires a route manifest
- Routes must be expressible through the filesystem
Proposal
Next.js should support named URL parameters that match an entire URL segment. These routes would be expressed via the filesystem:
- A filename or directory name that is wrapped with
[]would be considered a named parameter - Explicit route segments would take priority over dynamic segments, matched from left-to-right
- Route parameters would be required, never optional
- Route parameters will be merged into the
queryobject (accessible fromgetInitialPropsorrouterviawithRouter) — these parameters can not be overridden by a query parameter
To help understand this proposal, let’s examine the following file tree:
pages/
├── [root].js
├── blog/
│ └── [id].js
├── customers/
│ ├── [customer]/
│ │ ├── [post].js
│ │ ├── index.js
│ │ └── profile.js
│ ├── index.js
│ └── new.js
├── index.js
└── terms.js
Next.js would produce the following routes, registered in the following order:
;[
{ path: '/', page: '/index.js' },
{ path: '/blog/:id', page: '/blog/[id].js' },
{ path: '/customers', page: '/customers/index.js' },
{ path: '/customers/new', page: '/customers/new.js' },
{ path: '/customers/:customer', page: '/customers/[customer]/index.js' },
{
path: '/customers/:customer/profile',
page: '/customers/[customer]/profile.js',
},
{ path: '/customers/:customer/:post', page: '/customers/[customer]/[post].js' },
{ path: '/terms', page: '/terms.js' },
{ path: '/:root', page: '/[root].js' },
]
Usage Examples
These examples all assume a page with the filename pages/blog/[id].js:
Navigating to the Page with <Link />
<Link href="/blog/[id]" as="/blog/how-to-use-dynamic-routes">
<a>
Next.js: Dynamic Routing{' '}
<span role="img" aria-label="Party Popper">
🎉
</span>
</a>
</Link>
The above example will transition to the /blog/[id].js page and provide the following query object to the Router:
{
id: 'how-to-use-dynamic-routes'
}
Reading Named Parameters from Router
import { useRouter } from 'next/router'
function BlogPost() {
const router = useRouter()
// `blogId` will be `'how-to-use-dynamic-routes'` when rendering
// `/blog/how-to-use-dynamic-routes`
const blogId = router.query.id
return <main>This is blog post {blogId}.</main>
}
export default BlogPost
Note: you can also use withRouter.
Reading Named Parameters in getInitialProps
function BlogPost({ blogText }) {
return <main>{blogText}</main>
}
BlogPost.getInitialProps = async function({ query }) {
// `blogId` will be `'how-to-use-dynamic-routes'` when rendering
// `/blog/how-to-use-dynamic-routes`
const blogId = query.id
const { text } = await fetch(
'/api/blog/content?id=' + encodeURIComponent(blogId)
).then(res => res.json())
return { blogText: text }
}
export default BlogPost
Caveats
Optional route parameters are not expressible through the filesystem.
You can emulate an optional route parameter by creating a stub page that exports the parameter version (or vice versa). This increases the visibility of your application’s routes when inspecting the filesystem.
// pages/blog/comments.js
// (the optional version of `pages/blog/[id]/comments.js`)
export { default } from './[id]/comments.js'
Named parameters cannot appear in the middle of a route name.
This means a page named blog-[id].js would be interpreted literally and not matched by /blog-1. You can either restructure your page to be /blog/[id].js or turn the entire URL Segment into a named parameter and handle stripping blog- in your application’s code.
Alternatives
Denote URL Slugs with insert symbol here instead of []
There are very few symbols available for use to represent a named parameter on the filesystem. Unfortunately, the most recognized way of defining a named parameter (:name) is not a valid filename.
While surveying prior art, the most common symbols used to denote a parameter were _, $ and [].
We ruled out _ because _ is typically indicative of an internal route that is not publicly routable (e.g. _app, _document, /_src, /_logs).
We also ruled out $ because it is a sigil in bash for parameter expansion.
Leverage path-to-regexp for comprehensive support
Most of the symbols required to express regex are not valid filenames. Additionally, complex regexes are sensitive to route ordering for prioritization. The filesystem cannot express order nor contain regex symbols.
In the future, we may allow path-to-regexp routes defined in next.config.js or similar. This is currently out of scope for this proposal.
Future Exploration
Catch-All Parameters
In the future, we may consider adding catch-all parameters. With what we know thus far, these parameters must be at the end of the URL and would potentially use % to denote a catch-all route (e.g. pages/website-builder/[customerName]/%.tsx).
Issue Analytics
- State:
- Created 4 years ago
- Reactions:296
- Comments:90 (42 by maintainers)

Top Related StackOverflow Question
Poll: To express interest in optional parameters, please react with a “+1” this comment.
Note: Optional parameters are already possible with this RFC, they just do not have an explicit syntax (see Caveats section).
Poll: To express interest in catch-all parameters, please react with a “+1” this comment.
Note: Please share your use case for catch-all parameters in this thread! We’d love to understand the problem-space more.