Next.js: [RFC] Dynamic Routes

Created on 19 Jun 2019  Β·  90Comments  Β·  Source: vercel/next.js

Dynamic 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

  1. Leverage convention to provide URL Slug support that is easy to reason about
  2. Cover a majority of use-cases observed in the wild
  3. Eliminate the need of a custom server to support /blog/:post
  4. Validate <Link /> route transitions when possible
  5. Avoid an implementation that requires a route manifest
  6. 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:

  1. A filename or directory name that is wrapped with [] would be considered a named parameter
  2. Explicit route segments would take priority over dynamic segments, matched from left-to-right
  3. Route parameters would be required, never optional
  4. Route parameters will be merged into the query object (accessible from getInitialProps or router via withRouter) β€” 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).

Most helpful comment

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).

All 90 comments

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.

reserved 3

On ricardo.ch, we use a locale prefix for each route which make routing a bit more complex.

Example of valid routes:

  • / - homepage with auto-detected locale
  • /:locale - homepage with forced locale
  • /:locale/search - search page
  • /:locale/article/:id - article page

Do your think such prefix parameters could be supported?

At the moment, we use https://www.npmjs.com/package/next-routes

Another thing: for the article page, we also support a slug before the id like /de/article/example-article-123 where the id would be 123. This is done via a quite complex regex using next-routes and I don't see how this could be expressed with a file-system API.

@ValentinH the provided routes are all possible using the filesystem API -- given your provided routes:

  • / => pages/index.js
  • /:locale => pages/$locale/index.js
  • /:locale/search => pages/$locale/search.js
  • /:locale/article/:id => pages/$locale/article/$id.js

we also support a slug before the id like /de/article/example-article-123 where the id would be 123

This use case is addressed above:

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 pages to be /blog/$id.js or turn the entire URL Segment into a named parameter and handle stripping blog- in your application's code.

Does this solution not meet your needs? We'd love to learn more about your specific requirements.

Thanks a lot for the answer.

I didn't thought about using $locale/index.js both as a folder and a file, this is really neat!

Regarding the "named parameter in middle", I overlooked it because I thought having the slug being dynamic was different. However, you are completely right and this is addressed by the paragraph you mentioned. Striping the slug in the application code will be the way to go πŸ™‚

Would something like this (parse params from .hidden .files/.folders) be possible?

pages/
β”œβ”€β”€ .root.js
β”œβ”€β”€ blog/
β”‚ β”œβ”€β”€ .id/
β”‚ β”‚ β”œβ”€β”€ index.js
β”‚ β”‚ └── comments.js <-- optional?
β”œβ”€β”€ customers/
β”‚ β”œβ”€β”€ .customer/
β”‚ β”‚ β”œβ”€β”€ .post/
β”‚ β”‚ β”‚ └── index.js
β”‚ β”‚ β”œβ”€β”€ index.js
β”‚ β”‚ └── profile.js
β”‚ β”œβ”€β”€ index.js
β”‚ └── new.js
β”œβ”€β”€ index.js
└── terms.js

or leave the $ so one could find their files :D but always use $folder to indicate a param?

pages/
β”œβ”€β”€ $root.js
β”œβ”€β”€ blog/
β”‚ β”œβ”€β”€ $id/
β”‚ β”‚ β”œβ”€β”€ index.js
β”‚ β”‚ └── comments.js <-- optional?
β”œβ”€β”€ customers/
β”‚ β”œβ”€β”€ $customer/
β”‚ β”‚ β”œβ”€β”€ $post/
β”‚ β”‚ β”‚ └── index.js
β”‚ β”‚ β”œβ”€β”€ index.js
β”‚ β”‚ └── profile.js
β”‚ β”œβ”€β”€ index.js
β”‚ └── new.js
β”œβ”€β”€ index.js
└── terms.js

I used to have this use-case for optional parameters in an app that worked with npm packages. These could optionally have a scope. There are routes like:

  • /packages/express
  • /packages/express/dependencies
  • /packages/@babel/core
  • /packages/@babel/core/dependencies

So basically, the scope parameter is optional, but it's also only a scope when it starts with @.
So /packages/express/dependencies and /packages/@babel/core have the same amount of segments, but in one case it's /dependencies of express and in the other it's /index of @babel/core.

In the end it was solved in react-router with the following routes:

<Switch>
  <Route path={`/packages/`} exact component={PackagesOverview} />
  <Route path={`/packages/:name(@[^/]+/[^/]+)`} component={PackageView} />
  <Route path={`/packages/:name`} component={PackageView} />
</Switch>

I'm not sure I see a solution for this use-case in this RFC.

As for catch-all use cases, I'm thinking any deep linking into recursively nested data, like folder structures, treeviews, treemaps.

My 2 cents: dollar signs in filenames are a bad idea because they're used by shells as a sigil. You're going to confuse people trying to run rm $root.js. Underscores seem like a decent alternative.

More broadly: like many people, I've tried to leverage the file system as a solution to this in the past. Ultimately, I think the file system is never going to offer the full expressiveness you're looking for. For example, declarative routers usually let you specify a validation pattern for a dynamic parameter. In that case, part of the schema lives on the file system, and another part in the code. Separation of concerns is a good thing, but in this case, it's a technical limitation more than anything else.

Like @ValentinH we use the $locale var, but it's optional.

Should we use /page.ts and /page/$locale/page.ts?

Because we can use a "default" locale or a predefined locale ( user settings ), in those cases we don't use the $locale param.

But we have more use cases: /car/search/$optional-filter-1/$optional-filter-2/$optional-filter-3

Where optional-filter-1: color-red, optional-filter-2: brand-ford, etc...

And for optional params, something like /$required-param/ and /$$optional-param/?

Awesome that this is coming up on the roadmap!

I have to chime in supporting @timdp though. When you can't even touch $file this will lead to a lot of confusion. You need to remember escaping at every interaction. touch \$file; vim $file will open vim without a file (because $file isn't a defined variable).
Likewise tab completion in a shell will list all variables, once again bringing confusion.

I'm proposing two alternatives that I feel gives the right associations and should work in shells:

  • = It can be read as page is a customer for =customer. You can even contort it mentally to be a colon just stretched out, thus resembling the most common form for named parameters.
  • @ as it also reads somewhat well. a customer for @customer

Another option would be to use curly braces (unless they are reserved characters on some file systems). This parameter syntax is also "prior art" and is used by many other routers:

pages/
β”œβ”€β”€ {root}.js
β”œβ”€β”€ blog/
β”‚ └── {id}.js
β”œβ”€β”€ customers/
β”‚ β”œβ”€β”€ {customer}/
β”‚ β”‚ β”œβ”€β”€ {post}.js
β”‚ β”‚ β”œβ”€β”€ index.js
β”‚ β”‚ └── profile.js
β”‚ β”œβ”€β”€ index.js
β”‚ └── new.js
β”œβ”€β”€ index.js
└── terms.js

This would allow to have parameters in the middle of the route segment and multiple parameters per segment as it's clear where the parameter starts and where it ends, e.g. /product-{productId}-{productColor}.

So excited that dynamic routes is coming to Next.js!

Regarding the syntax for named parameters, this is something that has been discussed on Spectrum: https://spectrum.chat/next-js/general/rfc-move-parameterized-routing-to-the-file-system~ce289c5e-ff66-4a5b-8e49-08548adfa9c7. It might be worth using that as input for the discussion here. Personally, I like how Sapper is doing it using [brackets]. This is also something Nuxt is going to implement in version 3. Having different frameworks use the same format for dynamic filesystem-based routes sounds like a good thing.

Regarding the usage of <Link />, I think developers will easily forget to set both the href and as attributes. I get that it's not possible to "merge" these into the href attribute because it'd introduce a breaking change, but I feel like it could be solved in a more elegant way.

Curly braces are unfortunately used by Bash to group commands.

I agree with @stephan281094 regarding usage of <Link />, it will be source of mistakes.

Dynamic routing is an extremely useful feature, so it's really awesome you guys have looked into it and came up with a solution, huge props!

While on this topic, wildcard routes would also be a worthy addition to the proposal. You did mention catch-all parameters as something to investigate in the future, but it doesn't cover cases where you might want to do something like /category/*, which could have N number of levels, and you want all of them to render the category page.

Is it possible to use : safely ? If so, that'd be my vote, because everyone is already familiar with that convention from express.

Due to $ conflicting with shell variables, I personally strongly oppose it.

Is it possible to use : safely ? If so, that'd be my vote, because everyone is already familiar with that convention from express.

Apparently : is a prohibited character in Windows, so it's probably not safe. Going with _ isn't ideal either, since underscores can be used in URLs. The reason I think [brackets] are a nice solution, is because it's more future proof. If Next.js wants to support routes like post-12345 in the future, using this syntax it can be done without introducing a breaking change.

So a list of characters to avoid would be:

  • Conflicts with file systems: :, *, ", <, >, |
  • Conflicts with shell variables: $
  • Conflicts with bash brace expansion {, }

Anything else?

This wouldn't eliminate our need to have a centralised route file for a couple of reasons:

  • We have an automatically generated sitemap, and the filesystem alone isn't sufficient to define it.
  • We used named routes and destination "pages" are determined by data, rather than something that's knowable at build time. The logic for figuring out what page to load based on name and params is driven by the route config.

We also generate our pages folder for these reasons:

  • We use Relay, and this means modules involving GraphQL need to be uniquely named. For that reason we can't often can't have the route segment names be the same as the module names. index.js definitely isn't unique, and I see places where we'd have multiple common segments like edit.
  • We prefer co-locating one-off page-specific components as siblings of the page modules themselves, which Next.js doesn't permit within the pages folder.

Essentially our pattern is to use our centralised route configuration to generate our pages folder, which contains files which do nothing more than import/export modules from elsewhere in the codebase.

To that end, my focus is more on whether this proposal can work simply as an enhanced output format for our existing page generation process, so that we can at least get the benefit of not needing a custom server.

I've gone over some of my use cases elsewhere: https://gist.github.com/AndrewIngram/8d4c4ccd9bd10415a375caacade9f5ca

The main thing i'm not seeing is supporting implicit parameters that aren't expressed in the file-system, for example URL overrides.

Let's say we have a URL like this:

/some-vanity-url/

Where in current Next.js terms, we'd want it to map to a product page with a number of query parameters, e.g Product.js?id=foo&language=en.

Similarly, on our website most countries "sites" are scoped by a top-level segment eg es or ie, but the gb site is mounted without that segment. This means all the gb pages have an implicit country parameter, whilst for all other countries it's explicit.

The other downside, is that because in our case, the same 'page' can exist at multiple mount points in the URL architecture, we're going to end up with a greater number of bundles (i.e. several duplicate entry points) than we actually need in practice.

On the whole this proposal seems like it's going to work well for most common use cases, but it doesn't obviate the need for a route config or custom server in _all_ cases. But assuming this doesn't replace my ability to use the framework the way I do today, I don't have any real objection to this being the preferred happy-path API.

I support the {id} suggestion. It allows for multiple params and I think it looks a lot better. It also fits better with React.

I'm in favor of the file/&param.js character. Taken directly from urls and it doesn't look like it conflicts with files systems or bash.

I would use _ and maybe allow for an override in the next.config.js for those who reallllly need something different.

Appreciate the work on this. Been wanting it for a while! ❀️

Amazing! πŸŽ‰πŸŽ‰πŸŽ‰

My only issue here is that Link needs both href and as params.

I believe we could just write <Link to="blog/123" /> : since Nextjs already knows all the routes based on files in the pages folder, it could easily translate it into "/blog/$id".

So a list of characters to avoid would be:

& is a control operator in bash that runs the left side of the argument in an async subshell. Plaintext: open pages/&customer would run open pages/ in the background and the command customer in the foreground shell.

This looks really cool.

It does seem like this will create a significant number of single file directories (like /blog/$id in the original example). This gets even more cumbersome if you want two trailing route parameters (i.e. /git/compare/$hash1/$hash2).

I also don't love that the filename for rending a blog post would be $id.js. Having it named blog.js would be much more descriptive.

Perhaps combine with a @customRoute decorator?

// pages/blog.js
import {useRouter, @customRoute} from 'next/router'

@customRoute('/blog/:id')
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

This seems to provide a cleaner solution for the proposed catch-all parameters as well.

Decorators can't be applied to functions (maybe this changed since I last read it?) and the proposal is probably a long way away anyway

Well, suppose you go that road, you'd probably do it the way AMP is configured now:

// /pages/blog.js
export const config = {
  amp: true,
  dynamicRoute: true // adds a [blog] property to the query object
  // dynamicRoute: /\d+/ // could even support regex if you want
};

However, I think stuff like this can be added later on if it seems useful at some point. I think I'd rather see a basic support to start with, much as is described in the RFC. Get some real usage with that, then refine where it breaks. I also think the only characters that should be taken into account to avoid are the file system ones. Those are the real blockers for building this feature.

Please, make sure to use a character that is friendly with serverless solutions! (On Aws, there are some characters that could cause troubles)

Exporting a config object with a component key is something I don't hate.

You could also just use a HOC

function BlogPost(props) {
    return <div />
}

export default withCustomRoute(BlogPost, "/blog/:id")

what if we add some static field to the page (like getInitialProps)?

// pages/blog.js
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>
}

// By default it would be as it is now
BlogPost.route = '/blog/:id';

export default BlogPost

@dmytro-lymarenko What happens when you navigate to /blog in the browser? A 404?

Because this needs to be determined at compile time, I guess you'd need something that is statically analyzable. a HOC or a static property wouldn't be.

you'd need something that is statically analyzable. a HOC or a static property wouldn't be

Every static property example given so far would be statically analyzable (though you could certainly break things easily). We could just insist that you export your function and set the route property on it in a statically analyzable way. The runtime could check for route properties that are set at runtime but weren't caught by our static analyzer and issue a warning / throw an error.

What happens when you navigate to /blog in the browser? A 404?

@kingdaro - IMO, yes. If you want to use both /blog, and /blog/:blogId paths, then you use a directory. You are overloading that path, so the directory structure is justified.

pages/
β”œβ”€β”€ blog/
β”‚ β”œβ”€β”€ $id.js
β”‚ └── index.js

Well, suppose you go that road, you'd probably do it the way AMP is configured now:

// /pages/blog.js
export const config = {
  amp: true,
  dynamicRoute: true // adds a [blog] property to the query object
  // dynamicRoute: /\d+/ // could even support regex if you want
};

However, I think stuff like this can be added later on if it seems useful at some point. I think I'd rather see a basic support to start with, much as is described in the RFC. Get some real usage with that, then refine where it breaks. I also think the only characters that should be taken into account to avoid are the file system ones. Those are the real blockers for building this feature.

I think using config is a bad idea because you need to go through multiple files, to see what is actually dynamic. If you set it in the file system you can see it from the first glance.

I wonder if more than one standard routing solution should be something to consider.

Simple file-based routing is a great selling point for those new to Next/React, or anyone wanting to quickly get a simple app up and running, but it can be rather limiting. And it seems to me that trying to shoehorn dynamic routing into this pattern could ruin that simplicity and lead to unnecessary complexity, all in the name of keeping everything file-based.

After reading this discussion and thinking about my own usage of Next.js, I think first class support for an alternative (supplementary) routing system could be the best way to solve this.

I like some of the out-of-the-box thinking in this thread (such as the proposal to use decorators) but those ideas definitely have their own problems. I hope we can come up with something great πŸ‘

Exporting a config object with a component key is something I don't hate.

You could also just use a HOC

function BlogPost(props) {
    return <div />
}

export default withCustomRoute(BlogPost, "/blog/:id")

That’s pretty cool, but I wonder if having route information split across many files like
this could become hard to manage.

My original thinking with proposing a local config (in the file) vs a global one (route.js), was to address the specific scenarios mentioned in my first comment (deeply nested files that are the only file in their directory, non-semantic file names, and catch-all-params).

If used strictly in those contexts, it's far less confusing, because the URL maps directly onto the file system, and only "extra" params are addressed by the local config.

That said, I'm not sure I would even try to to restrict users from doing it however they want. We can pretty print the calculated routing table to the console, or even save it to some predetermined file. That should be enough to aid troubleshooting routes

@merelinguist I don't believe = is prohibited in Windows as you've written in the summary table. You are linking back to how : is prohibited, but according to Microsoft Windows file naming docs the equal character is allowed.

I'm already porting with dynamic routes in a project that I use in production (hopefully I can get it live this week).

Specific question though, will the new next@canary API feature _also_ support dynamic routing?

{ path: '/api/:customer', page: '/api/$customer/index.js' }

I've just tried it with [email protected] and I get a 404 not found, so I suspect it's not there yet. Just seems like it makes sense for these two features (API + dynamic routes) to have parity on URL routing.

@remy it's not yet implemented it's on my list to do it soon

We also should take into account not only Windows and Linux systems, but others too:
https://en.wikipedia.org/wiki/Filename#Comparison_of_filename_limitations

I'd like to add more info about my proposal:

what if we add some static field to the page (like getInitialProps)?

// pages/blog.js
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>
}

// By default it would be as it is now
BlogPost.route = '/blog/:id';

export default BlogPost
  1. Developer can't use runtime variable for that route property
const route = `/blog/${somethingElse}`;
BlogPost.route = route; // is not allowed
  1. When we build page manifest with this current RFC (where folder contains some character to identify it is dynamic) I don't see the difference if we build this page manifest with reading the file and find the static route property on the page. In the same way lingui works: they don't allow id for Trans to be dynamic
<Trans id="msg.docs" /* id can only be static string */>
   Read the <a href="https://lingui.js.org">documentation</a>
   for more info.
 </Trans>

Going by the list of prefixes already listed - I wonder if there's any strong reason _not_ to use a @ symbol prefix?

I doubt if it's of value, but you get parity with Nuxt - which means someone switching from one or the other will immediately know how it works.

Alternatively, has anyone thought about making the prefix a user option? It makes it harder for people to understand one project from another, but it means if I wanted, I could make the prefix query__{...} or something.

Just a thought.

Following on from @remy's suggestion, why not completely open up the API for how Next parses routes from the file system. Giving users as much (or as little) flexibility as they need, and inspiring reliable third-party routing solutions.

@scf4 I had a library which is a PoC , which use now.json routes config to do universal routing with nextjs too here

I hope that Zeit team also open source the route parser on client side library, too.

Looking at Nuxt I think _id.js is not too bad. Yes, we already use _app and _document.js as you mentioned and it's not publicly routable. But a dynamic route can also be viewed as not routable as this is a template for many pages

How would this be handled for static site exports?

(Never mind this one)

I also think it would be helpful if Next.js were to print the generated routes to a single file (perhaps hidden by default). At the very least it would serve as a useful reference to people working on a project, but it could also open the door for some powerful dynamic routing later on.

I.e., if it uses that file for route handling at runtime, it would be very easy for users to add/change routes (e.g., for complex pattern matching) without losing the benefits of the filesystem-based API.

This would create a few challenges regarding how to keep track of routes which have been manually changed, but if solved I think that would be the best solution by far.

@scf4 Next.js already has the ability to do complex routes using the custom server option. What you are proposing is achieved in nearly the same amount of code with already available tooling.

Ah yeah, fair enough.

I think having a single routes file which can be edited is a much better option anyway!

I wrote up some thoughts on routing with the filesystem, but can summarize my findings here:

  • [param] appears safest (and is used by Sapper).
  • : is familiar to Express users, but I could've _sworn_ I had issues on Windows FS.
  • $ and {param} are used for variables & brace expansion in shells, so this can be more problematic when in the CLI.
  • _ _could_ work, but it's too common as a "private" indicator.

I've personally had better experiences with whitelisting files for routes (/^index\.) vs. a blacklist (/^_/), but that would be a backwards compatibility problem with /pages.

With recent discussions to support API routes (#7297), this could be an opportunity to support /api and /pages both under a new home of /routes.

However, _and it's a strong "however"_, the Next.js ecosystem is large enough to warrant _incremental_ feature additions, vs. a "hey, if we had to do this over again we would do it _this_ way" design.

Square brackets ([example]) are used by zsh for pattern matching, so that wouldn't be viable either.

See examples in Filename Generation

Brackets [] are used by zsh for pattern matching, so that wouldn't be viable either.

Seems like they just made it in https://github.com/zeit/next.js/pull/7623

Thanks for the heads up. I posted a comment in there, too.

I tried [id] and just using it in paths is a pain (e.g. cd \[id\]/view.js). Seems to me double underscores __id (e.g. cd __id/view.js) works just as well and can be distinguished (albite maybe slightly confusing still) from internal files/folders (e.g. _app.js).

@AaronDDM are you using zsh? You do not need to escape [ or ] in bash.

Yeah this also happens for me with zsh - super annoying to interact with these directories.

$ mkdir [asdf]
zsh: no matches found: [asdf]
$ mkdir \[asdf\]
$ cd [asdf]
zsh: no matches found: [asdf]
$ cd \[asdf\]

And since zsh will become default shell in macOS Catalina, maybe something should be done about this after all...

agree with __id.js

Hm, really don’t love the __, just doesn’t look great to me.

@merelinguist em, Jest use __tests__ for default test folder, I think __ make sense in some case.

@YUFENGWANG Perhaps, but I’d prefer a single character if possible. Ultimately, I think the best solution would be:

  1. Sensible, cross-platform default, like =
  2. Option in next.config.js to customise the special route character which is used
  3. Documentation on which characters are problematic in which situations

Agreed with a single character but I'd prefer to have a zero configuration. and my guess is that many people will go through all issues even if you describe them in a documentation

Note also = is reserved by zsh. From the docs:

If a word begins with an unquoted β€˜=’ and the EQUALS option is set, the remainder of the word is taken as the name of a command. If a command exists by that name, the word is replaced by the full pathname of the command.

Just an idea; what about using a suffix? For example [email protected], or the like could suffice. This may solve the issue with having to escape and work across shells and file systems as long as the character is valid.

These work in zsh and bash without the need to escape, so far:

[email protected]
example~.js
example=.js

Ooh. Not a suffix but a way to denote trailing URL params.

So [email protected] becomes blog/:id.

compare@[email protected] becomes compare/:a/:b.

This could solve the deeply nested single file directories I was objecting to above, and keep the entire routing definition file system based.

It doesn't look as fancy, but how about something along the lines of:

/blogs/_var_blog-id/index.js
/blogs/_var_blog-id.js

a prefix _var_ Which kind of tries to mimic JS variable declarations. Or does it have to be a super short, one character thing?

How about ~ character ?

Like /blogs/~id.

Using ~ as a prefix is also not viable, as it is used to expand to the home folder in POSIX-compliant shells.

Any character that doesn't match [0-9a-zA-Z-._] (regex) cannot be considered safe as a prefix across operating systems, shells and file systems.

Some characters aren't safe inline, either. See zsh's docs about substitutions

Also I think we shouldn't strive for whether it looks fancy, but rather be intuitive, readable and easy to communicate.

  • using brackets for [params].js seem more elegant and widely used. (sapper, nuxt v3?).
  • underscore prefix pages/_helper.js usually for a private function and maybe this should be not rendered. this allows us to create helper components within the pages folder

imho: this feels like a temporary solution to the greater problem. While having routes based on file structure is very nice to have to begin with, it doesn't scale well when you have hundreds of routes, params, etc. Having a routes config file (maybe have a routes.js file in each directory) is a better long term solution. I am personally drawn to nextjs because of the features out-of-the-box (SSR, speed, etc) it provides, not the ease of creating routes from files.

@mmahalwy you hit the nail on the head.

Next.js already generates a routes config (based on the filesystem). I believe that making this config more explicit and/or allowing the user to "eject" it if they wish would be the most seamless solution here

@mmahalwy @scf4 FWIW, a significant justification for filesystem routes is to remove the need to have a centralised file. In fact, one could easily argue that the entirety of Next.js's API for links and routing is designed around this constraint.

The problem with a route config is that you end up having to ship it to the client, which can mean quite a hefty code bundle if you have routes numbering from hundreds to thousands.

However, there are quite a few common use cases that (as far as I've been able to tell, from discussing this issue with @timneutkens numerous times over the past few months) can't really be solved without a centralised config. I listed some of them in my earlier comment, but there are more.

The simplest one is having a CMS-driven blog where authors can create links to pages on the site. They'll just be creating links with a plain old URL, with no knowledge of what the underlying page module is. With a centralised route config, it's pretty easy to reverse match a URL and work out which page to load (my own library, next-route-resolver is designed to support this use cases, and all the others i've come up with).

I don't see how I can make the site I'm working on work without a route config, so my focus has just been on finding ways to keep the route config within filesize tolerances. For other people, filesystem routing may be more than sufficient. I don't think routing is a problem where there's a single solution that solves everything, it's all about balancing trade-offs.

So as I mentioned before, as far as this proposal is concerned, it seems fine as long as it's sold as solving the routing problem entirely, because that would be a little misleading :)

@AndrewIngram I understand where you are coming from but this limitation is limiting the power that nextjs has. Nextjs offers so much out of the box that it should be a no-brainer for any new project or company to use it. The challenge though is it's hard opinion on routing that make it unejectable in the future (and as a large company, you're always considering the exit strategy should projects lose interest or maintenance).

@mmahalwy I think you misunderstood my point. I'm in agreement with you, I don't think file-system routing is sufficient to call the routing problem solved, and would be disappointed if it was presented as such. I do think it offers an improvement for a particular set of use cases, but I also think there should also be some kind of route manifest format for those willing to opt-in to a different set of trade-offs (e.g. you and me).

For those wishing for a centralized or advanced routing config, isn't it well handled by using the custom server and/or external packages? What are you hoping gets added here?

It all seems off topic from this RFC. I don't think anyone, including the OP, has suggested this is the end-all solution for routing. This just improves on the filesystem based routing.

I've been using the dynamic routes for a mini project for the last few weeks (using $ though I note that it's moved to [param] 3 days ago in the canary repo, but anyway).

I _just_ started using getRequestHandler and I think it's not picking up the dynamic routing on the server side.

Is that a bug, or intentional (i.e. some change to getRequestHandler), something else, or does using getRequestHandler completely turn off the dynamic routing (which would make sense now I think about it…)?

For those wishing for a centralized or advanced routing config, isn't it well handled by using the custom server and/or external packages? What are you hoping gets added here?

One of the goals here is to avoid the need to create a custom server, if only to make it easier to use with services like Now (which currently requires all the dynamic routes to be part of its config).

It all seems off topic from this RFC. I don't think anyone, including the OP, has suggested this is the end-all solution for routing. This just improves on the filesystem based routing.

There actually is some additional context here. This proposal has been a long time coming, and based on many of the discussions i've seen related to it (including ones i've been directly involved with), this was being hyped to some degree as removing the need for using these route management libraries like next-routes and my own. I don't think it's off-topic to highlight the use cases which aren't fulfilled by this RFC. Some of them might conceivably be fulfilled by some changes to the proposal, others might not. But either way, surely it's valuable to raise awareness of the limits of what's being proposed?

FWIW we use [param] style FS-based routes at Pinterest (though not Next). It's scaled really well so far. The biggest criticism is that Jest interprets [] as regexp pairs so it can be difficult to target tests for param-ful handlers.

@chrislloyd What are your experiences with creating and managing files using this format for paths/files in different environments, considering anyone is using zsh, or a tool that interprets these differently?

Seen as the [] is using for pattern matching in zsh (and, as you say with Jest) you will need to escape these paths. This isn't much of a problem if you _know_ this, but given that it should be usable and understood by beginners, I have doubts that this is the right format to go with.

I have an idea on using ! as required parameter, like /pages/id!.js and ? for optional parameter, like in /pages/posts/id?.js .

It doesn't have any issue with prefix like above discussions, and it's familiar on how ! represents required params, and ? represents optional parameters.

Windows doesn't allow question marks in file names, and both ? and ! have a special meaning in Bash.

API routes now supports dynamic params #7629 πŸš€

@remy getRequestHandler is expected to handle dynamic routing -- I just confirmed locally it does. Could you please file a separate bug/issue with reproduction steps so we can investigate? :pray:

Hi everyone! Thanks for the incredible response to this RFC.

This RFC has been implemented and released as stable in Next.js 9.
You can read more about it in the blog post.

We're going to publish a new RFC in the future to address all the advanced feedback given here. We'll post an update here when it's available.

Was this page helpful?
0 / 5 - 0 ratings