Run 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 runningremix setup node
, will make imports from remix work - A
dev:remix
script runningremix watch
, will build the app in local - A
build:remix
script runningremix build
, will build the app in production - A
postinstall
script runningnpm run setup
, probably also run it beforedev:remix
andbuild: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
issrc
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.