How toRun Next and Remix on the same server

If you want to migrate a Next app to Remix, you may be tempted to do a complete migration. Still, if your app is too complex, you may not be able to do it. In that case, a gradual migration is the best option.

So how can you do that? If you are deploying your Next app as a Node app (not serverless), the easier way is to use Express.

Create a custom Next server

First, we need to create a custom server for our Next.js application, something like this should do it:

let express = require("express");
let next = require("next");

let port = process.env.PORT || "4000";
let host = process.env.HOST || "localhost";
let env = process.env.NODE_ENV || "development";

let dev = env !== "production";
let app = next({ dev });
let handle = app.getRequestHandler();

function nextHandler(req, res) {
  return handle(req, res);
}

async function main() {
  await app.prepare();

  let server = express();

  // First, we need to serve all the /_next URLs, this includes the built files
  // and the images optimized by Next.js
  server.all("/_next/*", nextHandler);

  // Then, we need to server the static files on the public folder
  server.use(express.static("public", { immutable: false, maxAge: "1h" }));

  // Finally, we need to tell our server to pass any other request to Next
  // so it can keep working as an expected
  server.all("*", nextHandler);

  server.listen(port, host, (error) => {
    if (error) throw error;
    console.log(`> Ready on http://${host}:${port}`);
  });
}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});

With this, we can now run our Next app doing node server.js, and to run it in production, we can use NODE_ENV=production node server.js.

Add Remix to the mix

Now we can add Remix, first, install the required packages.

npm i @remix-run/express @remix-run/react remix
npm i -D @remix-run/dev

Then, update your scripts in the package.json to add:

  • A setup script running remix setup node, will make imports from remix work
  • A dev:remix script running remix watch, will build the app in local
  • A build:remix script running remix build, will build the app in production
  • A postinstall script running npm run setup, probably also run it before dev:remix and build:remix so it will always be ready

I have my scripts like this:

{
  "beforebuild:remix": "npm run setup",
  "build:remix": "remix build",
  "build:next": "next build",
  "build": "npm run build:remix && npm run build:next",

  "beforedev:remix": "npm run setup",
  "dev:remix": "remix watch",
  "dev": "node server/index.js",

  "start": "NODE_ENV=production node server/index.js",

  "setup": "remix setup node",
  "postinstall": "npm run setup"
}

Also add "sideEffects": false to the package.json.

Now, create the remix.config.js in the root of your project with this content:

/**
 * @type {import('@remix-run/dev/config').AppConfig}
 */
module.exports = {
  serverBuildDirectory: "server/build",
  ignoredRouteFiles: [".*"],
};

If you use TypeScript, create the remix.env.d.ts file somewhere in your app, I put it inside a types directory.

/// <reference types="@remix-run/dev" />
/// <reference types="@remix-run/node/globals" />

Then, in your tsconfig.json set the target to be es2019, the baseUrl to be ., add a path alias to { "~/*": ["./src/*"] }, and in include add the remix.env.d.ts

Finally, create your app folder with an entry.client, entry.server and root, and probably some route to test it works.

Tip: You could configure in the remix.config.js that the appDirectory is src and mix your Next and Remix code inside the same folder.

After we have all of that boilerplate setup (if you used the Remix CLI, they give you all of that), we can add Remix to our Express server. Let's go back to our server code and update it.

let express = require("express");
let next = require("next");
const { createRequestHandler } = require("@remix-run/express");
const path = require("path");

let port = process.env.PORT || "4000";
let host = process.env.HOST || "localhost";
let env = process.env.NODE_ENV || "development";
let buildDir = path.join(process.cwd(), "server/build");

let dev = env !== "production";
let app = next({ dev });
let handle = app.getRequestHandler();

function nextHandler(req, res) {
  return handle(req, res);
}

// This function will be used as request handler for the Remix app
function remixHandler(req, res, next) {
  // In production, we create the request handle and require the build once
  if (env === "production") {
    return createRequestHandler({ build: require("./build") })(req, res, next);
  }

  // In development, we purge the require cache on every request so it's always
  // up to date and then require again and handle the request
  for (let key in require.cache) {
    if (key.startsWith(buildDir)) {
      delete require.cache[key];
    }
  }

  let build = require("./build");

  return createRequestHandler({ build, mode: env })(req, res, next);
}

async function main() {
  await app.prepare();

  let server = express();

  // First, we need to serve all the /_next URLs, this includes the built files
  // and the images optimized by Next.js
  server.all("/_next/*", nextHandler);

  // Then, we need to server the static files on the public folder
  server.use(express.static("public", { immutable: false, maxAge: "1h" }));

  // Remix fingerprints its assets so we can cache forever
  server.use(express.static("public/build", { immutable: true, maxAge: "1y" }));

  // If we have a `/something` route in our Remix app, we can add it here so we
  // can tell Express to send the request to Remix instead of Next
  server.all("/something", remixHandler);

  // Finally, we need to tell our server to pass any other request to Next
  // so it can keep working as an expected
  server.all("*", nextHandler);

  // Note: Because we have both Remix and Next, the framework handling `*` should
  // be the one rendering the error pages, or if you have a catch-all route the
  // one with that route.

  server.listen(port, host, (error) => {
    if (error) throw error;
    console.log(`> Ready on http://${host}:${port}`);
  });
}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});

That's it, your npm run dev:remix and npm run dev and go to /something in your browser, and you should see the Remix app running, to go another URL of your Next app, and it will also work!

When you add a new route to Remix, go to the server code and add the handler with server.all(path, remixHandler), eventually migrate all the Next routes until you can switch them to be:

server.all("/something", nextHandler);
server.all("*", remixHandler);

Once you are there, you can keep migrating routes and remove the Next app.