Dividing Your App Into Modules

Non-file based routing in NextJS: Customize how you organize your code

May 31, 2021

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

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

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

next.config.js
module.exports = {
  pageExtensions: ["page.js", "api.js"],
  // use page.tsx and api.ts for TypeScript
};

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

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:

next.config.js
module.exports = {
  ...,
  async rewrites() {
    return [
      {
        source: "/login",
        destination: "/auth/login",
      },
      {
        source: "/signup",
        destination: "/auth/signup",
      },
    ];
  },
};

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

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.

  1. Leave it as is, if you're okay with this structure.
  2. 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.
  3. 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:

auth/api/login.js
export default function handler(req, res) {
  res.status(200).json({ name: "Login API" });
}
auth/api/signup.js
export default function handler(req, res) {
  res.status(200).json({ name: "Sign up API" });
}

Now in pages/api/login.api.js and pages/api/signup.api.js, all you have to do is

  1. Import the handler from pages/auth/api/login.js or pages/auth/api/signup.js.
  2. Export it as the default export.
api/login.api.js
import handler from "../auth/api/signup";
export default handler;
auth/api/signup.api.js
import handler from "../auth/api/signup";
export default handler;

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

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

pages/blog/api/comments.js
export default (req, res) => {
  const { id } = req.query;
  res.status(200).json({ name: `Comments for Blog Post ${id}` });
};
pages/api/comments/[id].api.js
import handler from "../../blog/api/comments";
export default handler;

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

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:

pages/blog/[slug].js
import BlogPost, { getServerSideProps } from "../modules/blog/blogPost";
 
export default BlogPost;
export { getServerSideProps };
modules/blog/blogPost.js
export default function BlogPost() {
  ...
}
 
export async getServerSideProps() {
  ...
}

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.