Development-only pages in Next.js
Tags: nextjs, react, javascript
Published: 2022-01-25 02:41:25

---
title: Development-only pages in Next.js
published: true
description: Build pages in Next.js that are only viewable in a development environment and are excluded from the production site.
tags: nextjs, react, javascript
---

If you practice continuous delivery on your projects, your main branch should always be in a deployable state. On a Next.js project, this means you must be able to commit incomplete pages to version control without them becoming publicly accessible when you deploy your website.

Next.js's usage of file-based routing makes excluding incomplete pages difficult: there's no central routes file like Django where you can [conditionally include routes](https://dev.to/tylerlwsmith/development-only-routes-in-django-2dpp). You can return a 404 from `getServerSideProps`, but Next also has `getInitialProps` and `getStaticProps`.  Next.js allows [server redirects](https://nextjs.org/docs/api-reference/next.config.js/redirects), but redirects don't work on statically-exported sites. Neither does middleware. Ideally, we want a single solution that works 100% of the time.

We can make incomplete pages inaccessible on the production site by getting creative with Next's `pageExtensions` setting. Let's dig in.

## Leveraging page extensions

By default, Next.js will assume that anything in the `pages/` directory with a `js|jsx|ts|tsx` extension is a renderable page. However, this behavior is [configurable](https://nextjs.org/docs/api-reference/next.config.js/custom-page-extensions).

We can leverage Next's page extension behavior to make separate extensions for development-only routes and production routes. 

This post will show two different implementations of dev-only routes using page extensions.

**Implementation 1:**

- We will use `.dev.js` and `.dev.jsx` extensions for pages that will only be accessible when running the development server.
- We will use `.prod.js` and `.prod.jsx` extensions for pages that will be accessible both in development and production.

**Implementation 2:**

- We will use `.dev.js` and `.dev.jsx` extensions for pages that will only be accessible when running the development server.
- We will use `.js` and `.jsx` extensions for pages that will be accessible both in development and production.

## Implementation 1

We will be making our changes in `next.config.js`. Here's what the file looks like on a brand new Next.js project:

```js
module.exports = {
  reactStrictMode: true,
}
``` 

Here's what we want it to look like:

```js
const { PHASE_DEVELOPMENT_SERVER } = require("next/constants");

module.exports = (phase, { defaultConfig }) => ({
  ...defaultConfig,
  reactStrictMode: true,
  pageExtensions: ["ts", "tsx", "js", "jsx"]
    .map((extension) => {
      const isDevServer = phase === PHASE_DEVELOPMENT_SERVER;
      const prefixes = isDevServer ? ["dev", "prod"] : ["prod"];
      return prefixes.map((prefix) => `${prefix}.${extension}`);
    })
    .flat(),
});

```

**After updating `next.config.js`, change the filenames for all pages that should be viewable in production**. The site's homepage filename will change from `index.js` to `index.prod.js`. After you've changed all the page filenames to use the `prod.js` extension, run `npm run dev` and click through the site to make sure your pages work.

You've now finished implementing development-only routes. Continue reading if you want to know how this code works, or if you want to see an alternative implementation that doesn't require the `.prod.js` extension.

## Understanding the updated configuration

Let's look at the update `next.config.js` file one more time. I've added line numbers to make it easier to point to specific sections.

```js
1  const { PHASE_DEVELOPMENT_SERVER } = require("next/constants");
2
3  module.exports = (phase, { defaultConfig }) => ({
4    ...defaultConfig,
5    reactStrictMode: true,
6    pageExtensions: ["ts", "tsx", "js", "jsx"]
7      .map((extension) => {
8        const isDevServer = phase === PHASE_DEVELOPMENT_SERVER;
9        const prefixes = isDevServer ? ["dev", "prod"] : ["prod"];
10       return prefixes.map((prefix) => `${prefix}.${extension}`);
11     })
12     .flat(),
13 });

```

- On **line 3**, we've changed the export to a function. This gives us access to the `phase` argument, which we will use to determine if the build server is running ([more details in the docs](https://nextjs.org/docs/api-reference/next.config.js/introduction)).
- On **line 6**, we put the page extensions that our app can generate pages from. If your app has `md` or `mdx` pages, you would also add them to this array.
- On **lines 7-11**, we map through these page extensions. 
  - If the **development server is running**, the `map()` will generate the following output.
        ```js
        [
          ["dev.ts", "prod.ts"],
          ["dev.tsx", "prod.tsx"],
          ["dev.js", "prod.js"],
          ["dev.jsx", "prod.jsx"],
        ];
        ```
    - If the **development server is _not_ running**, the `map()` will generate the following output.
        ```js
        [["prod.ts"], ["prod.tsx"], ["prod.js"], ["prod.jsx"]];
        ```
  - On **line 12**, we flatten the nested arrays into a single array.
        ```js
        // Development
        ["dev.ts", "prod.ts", "dev.tsx", "prod.tsx", /** etc. */];

        // Production
        ["prod.ts", "prod.tsx", "prod.js", "prod.jsx"];
        ```

This flat array is then used to determine what pages are visible in development and production.

## Testing the dev-only pages

To test that our new development-only pages work, create a new page at `pages/unpublished.dev.js` and add the following:

```js
// pages/published.dev.js

export default function UnpublishedPage(props) {
  return <div>I am an unpublished page.</div>;
}
```

Now run `npm run dev` and navigate to http://localhost:3000/unpublished. You should see an unstyled page that returns the text, "I am an unpublished page."

Now let's try visiting the same page on a production build. Run `npm run build && npm start`, then refresh http://localhost:3000/unpublished. You should see an error page, but the site's homepage should continue to work.

This dev-only pages implementation works, but it requires you to change the filename of every page in your app, by changing page extensions to `.prod.js`. But maybe we don't have to.

## Implementation 2

It's possible to get Next.js to ignore pages with a `.dev.js` extension without having to use a `.prod.js` extension on production pages if we use a [negative lookbehind](https://queirozf.com/entries/python-regular-expressions-lookahead-and-lookbehind-examples#negative-lookbehind) regular expression. 

During the build process, Next flattens all of the page extensions down to a [singular regular expression](https://github.com/vercel/next.js/blob/e8c15e5991eea71d7e345ca9bae510ec0d6fe8ab/packages/next/build/utils.ts#L70). We can hook into this regular expression to ensure that Next only builds pages that **don't** use `.dev.[js|jsx]`.

**The following code relies on hacks to deal with inconsistencies in Next.js's Regex parsing between the dev server and build process. At some point this code may break. You've been warned.**

```js
pageExtensions: ["ts", "tsx", "js", "jsx"]
  .map((extension) => {
    const isDevServer = phase === PHASE_DEVELOPMENT_SERVER;
    const prodExtension = `(?<!dev\.)${extension}`;
    const devExtension = `dev\.${extension}`;
    return isDevServer ? [devExtension, extension] : prodExtension;
  })
  .flat(),
```

The interesting part of this code is the `return` line. At the time of writing, Next's development server isn't able to process negative lookbehinds ([GitHub issue](https://github.com/vercel/next.js/issues/33669)). To get around this, the code above tries the `.dev.[js|jsx]` first, then falls back to `.[js|jsx]`.

This code will allow you to use `.dev.js` extensions for dev-only pages, and `.js` for pages that are publicly accessible.

## Downsides of this implementation

Our development-only pages work pretty well, but this isn't a perfect solution. You may have to make changes to get this to work with an automated test suite. This solution may also break if you use a tool that asks you to wrap your Next.js config in a function call. For example, our `next.config.js` function won't work with Vercel's own `@next/mdx` package: it will only work with a config object ([GitHub issue](https://github.com/vercel/next.js/issues/33627)).

---

A lot of complexity could be avoided if there was a mechanism other than file-based routing to define routes in Next.js. For better or worse, file-based routing has been embraced by most full-stack JS frameworks, forcing developers to hack around the router to accomplish tasks that would be trivial in frameworks like Ruby on Rails.

Let me know if you found this article helpful, and let me know if you think there are improvements that can be made to these implementations.