Nextjs uses file-based routing. Files in the pages/
router are automatically mapped to page URLs. While this is great for most sites, especially static sites like blogs, it can become difficult to deal with as the project grows in size and complexity.
File-based routing is great, but it may make more sense to divide your app into multiple modules, with all related code closer together.
The issue
Take a look at this Nextjs app structure:
- lib/
- auth.js
- admin.js
- mdx.js
- comments.js
- pages/
- api/
- login.js
- signup.js
- comment.js
- admin.js
- index.js
- login.js
- signup.js
- blog/
- [slug].js
- admin.js
- api/
This is the structure of a basic Nextjs application. The colors represent the different parts of the app: blog, authentication, and admin-related.
As you can see, files relating to a single part of the app are spread across many folders. I would much prefer storing each part under a new folder ...
- pages/
- index.js
- modules/
- auth/
- login.js
- signup.js
- api/
- login.js
- admin.js.js
- lib/
- auth.js
- blog/
- [slug.js]
- lib/
- mdx.js
- comments.js
- admin/
- index.js
- api/
- admin.js
- lib/
- admin.js
- auth/
... like this. This feels a lot easier to read and manage, even with larger and complex applications. Each "module" has its own folder.
There are a few ways to achieve a folder structure like this.
Custom page extensions
Within the pages folder, custom extensions for pages can be specified. For example, we can have all pages end .page.js
and .api.js
. Other files will just have a .js
extensions, and therefore are not pages:
What this means is we can put files that are not pages inside the pages/
directory, so our file structure can be slightly improved:
- pages/
- api/
- login.api.js
- signup.api.js
- comment.api.js
- admin.api.js
- index.js
- login.page.js
- signup.page.js
- auth-lib/
- auth.js
- blog/
- [slug].page.js
- lib/
- mdx.js
- comments.js
- admin
- index.page.js
- lib/
- admin.js
- api/
While this is slightly better, there are issues.
- The API routes are still in the
api/
directory. - The authentication pages (login and signup) are at the same level as the index, so they do not have their own "module" folder like blog and admin do. Rewrites can help us fix this.
Rewrites
Rewrites allow you to map an incoming request path to a different destination path. Unlike redirects, the user stays on the same page.
The issue is that each "module" apart from auth has its own folder in the pages/
directory. So what we can do is change the file structure slightly:
- Move /pages/login.page.js to /pages/auth/login.page.js
- Move /pages/signup.page.js to /pages/auth/signup.page.js
Now the auth pages are at /auth/login
/auth/signup
, while we want them at /login
and /signup
. Add the following to the rewrites
section in next.config.js
:
Restart your dev server and visit localhost:3000/login and localhost:3000/signup to confirm that the rewrites are working properly.
SEO and duplicate content with rewrites
See this comment for more information.
Our file structure currently looks like this now:
- pages/
- api/
- login.api.js
- signup.api.js
- comment.api.js
- admin.api.js
- index.js
- auth/
- login.page.js
- signup.page.js
- lib/
- auth.js
- blog/
- [slug].page.js
- lib/
- mdx.js
- comments.js
- admin
- index.page.js
- lib/
- admin.js
- api/
We have solved two problems: dividing the app into modules, and putting non-page routes in the pages/
directory. The last problem is to move the API routes into their modules.
There are three things we can do at this stage.
- Leave it as is, if you're okay with this structure.
- Use a custom server (like Express). Write all your API routes with Express and let Nextjs handle the page routes. While this allows for customization, we lose out on many Nextjs features, such as hot reload by default, TypeScript support, and have to set them up ourselves. A Nextjs app with a custom server cannot be deployed on Vercel.
- Import your API functions from other files: write your APIs in the file you want, and import it into a file in
pages/api
directory. Let's explore this method in more detail.
Importing and exporting
From the above file structure, let's take auth
module as an example. Create an API folder inside the auth/
directory like this:
-
pages/
- ...
-
auth/
- login.page.js
- signup.page.js
-
lib/
- auth.js
-
api/
- login.js
- login.js
In them, create and export the handler function as your normally would:
Now in pages/api/login.api.js
and pages/api/signup.api.js
, all you have to do is
- Import the
handler
frompages/auth/api/login.js
orpages/auth/api/signup.js
. - Export it as the default export.
After doing this with the APIs from the other modules, the final directory should look something like this:
- pages/
- api/
- login.api.js
- signup.api.js
- comment.api.js
- admin.api.js
- index.js
- auth/
- login.page.js
- signup.page.js
- lib/
- auth.js
- api/
- auth.js
- login.js
- blog/
- [slug].page.js
- lib/
- mdx.js
- comments.js
- api/
- comment.js
- admin
- index.page.js
- lib/
- admin.js
- api/
- admin.js
- api/
We still have files inside the pages/api
directory, but all of them only have 2 lines of code (importing and exporting the handler function).
Dynamic API routes
Dynamic API routes work, but you have to rename the file inside the api/
directory.
For example, the blog comments API takes in a URL parameter (/api/comments/34
, where 34 is the blog post ID). In that case you will only need to change the files inside pages/api
. You do not need to rename the files in pages/blog/api/
.
The file structure looks like this:
-
pages/
- api/
- ...
- comments/
- [id].api.js This filename was changed and it was put under the comments/ directory
-
blog/
- [slug].page.js
-
lib/
- mdx.js
- comments.js
- api/
- comment.js This remained the same
- ...
- api/
See a full example of this structure: nextjs-custom-routing
Pages
Similar to API routes, if you want to keep your pages in a different folder, you can import and export them from the appropriate file.
For example, I want to have a page at the route /blog/[slug].js
, but I want to keep the file inside the directory /modules/blog/blogPost.js
. I can easily do it like this:
Note that you will have to keep the files pages/blog/[slug].js
and modules/blog/blogPost.js
in sync. If you want to change the name of the URL parameter slug
, you will have to rename pages/blog/[slug].js
. Also, remember to import end export getServerSideProps
, getStaticeProps
, or getStaticPaths
if you have defined them.
Multi-zones
Another way to divide your app into modules is to use use "multi-zones". This involves having multiple Nextjs projects (example). This may work out for you, but I personally would like the entire app to be under the same project so that I do not have to configure rewrites.
Which method should I use?
I would first go for custom page extensions. If that isn't helpful (say you want to rename your pages to something other than [slug].js
, for example), use the importing and exporting method with page routes or API routes.