Next.js is an exceptional framework for React applications that comes with a lot of bells and whistles for Server-Side Rendering and Static Site Generation. One of the quickest ways to start writing production-ready React without spending the time on setup.
Next.js comes with its own server that can be used out-of-the-box when starting a brand-new projects.
But what if you need to serve Next.js app from an existing Node server? Or maybe you want to have more flexibility additional flexibility for integrating middleware, handling custom routes, etc?
If that's the case - this post is for you, it covers the setup of custom Next.js server with Fastify, solution for Express.js or plain Node.js server will be similar.
Example project used here is also available as a template on Github.
Initial setup
So imagine you have an existing Fastify project. For the sake of example I have a simple Fastify API here. It's initialized from this great Fastify template and has a couple of endpoints returning mock data:
-
/_health
- server status -
/api/pokemons
- Pokemons list -
/api/stats
- list of Pokemon stats
// src/app.ts
import { fastify as Fastify, FastifyServerOptions } from "fastify";
import { POKEMONS, STATS } from "./mocks";
export default (opts?: FastifyServerOptions) => {
const fastify = Fastify(opts);
fastify.get("/_health", async (request, reply) => {
return { status: "OK" };
});
fastify.get("/api/pokemons", async (request, reply) => {
return POKEMONS;
});
fastify.get("/api/stats", async (request, reply) => {
return STATS;
});
return fastify;
};
Adding Next.js app
It's as easy as just generating a new Next.js project using create-next-app
, I'll do it in ./src
directory:
cd ./src && npx create-next-app nextjs-app
Handling requests using Next.js
To allow Next.js render pages Fastify needs to pass requests to it.
For this example, I want Next.js to handle all routes under /nextjs-app
// Path Next.js app is served at.
const NEXTJS_APP_ROOT = "/nextjs-app";
fastify.all(`${NEXTJS_APP_ROOT}*`, (request, reply) => {
// Remove prefix to let Next.js handle request
// like it was made directly to it.
const nextjsAppUrl = parse(
request.url.replace(NEXTJS_APP_ROOT, "") || "/",
true
);
nextjsHandler(request.raw, reply.raw, nextjsAppUrl).then(() => {
reply.hijack();
reply.raw.end();
});
});
Next.js also makes requests to get static, client code chunks etc. on /_next/*
routes, need to pass requests from Fastify to it:
// Let Next.js handle its static etc.
fastify.all("/_next*", (request, reply) => {
nextjsHandler(request.raw, reply.raw).then(() => {
reply.hijack();
reply.raw.end();
});
});
As a result, complete Fastify routing would look like this:
// src/fastify-app.ts
import { fastify as Fastify, FastifyServerOptions } from "fastify";
import { POKEMONS, STATS } from "./mocks";
import nextjsApp from "./nextjs-app";
import { parse } from "url";
const nextjsHandler = nextjsApp.getRequestHandler();
export default (opts?: FastifyServerOptions) => {
const fastify = Fastify(opts);
fastify.get("/_health", async (request, reply) => {
return { status: "OK" };
});
fastify.get("/api/pokemons", async (request, reply) => {
return POKEMONS;
});
fastify.get("/api/stats", async (request, reply) => {
return STATS;
});
// Path Next.js app is served at.
const NEXTJS_APP_ROOT = "/nextjs-app";
fastify.all(`${NEXTJS_APP_ROOT}*`, (request, reply) => {
// Remove prefix to make URL relative to let Next.js handle request
// like it was made directly to it.
const nextjsAppUrl = parse(
request.url.replace(NEXTJS_APP_ROOT, "") || "/",
true
);
nextjsHandler(request.raw, reply.raw, nextjsAppUrl).then(() => {
reply.hijack();
reply.raw.end();
});
});
// Let Next.js handle its static etc.
fastify.all("/_next*", (request, reply) => {
nextjsHandler(request.raw, reply.raw).then(() => {
reply.hijack();
reply.raw.end();
});
});
return fastify;
};
Where the nextjsApp
comes from Next.js initialization here:
// src/nextjs-app.ts
import next from "next";
import env from "./env";
export default next({
dev: import.meta.env.DEV,
hostname: env.HOST,
port: env.PORT,
// Next.js project directory relative to project root
dir: "./src/nextjs-app",
});
And last but not the least - Next.js app needs to be initialized before starting the server:
nextjsApp.prepare().then(() => {
fastifyApp.listen({ port: env.PORT as number, host: env.HOST });
fastifyApp.log.info(`Server started on ${env.HOST}:${env.PORT}`);
});
Full server init will look like this:
// src/server.ts
import fastify from "./fastify-app";
import logger from "./logger";
import env from "./env";
import nextjsApp from "./nextjs-app";
const fastifyApp = fastify({
logger,
pluginTimeout: 50000,
bodyLimit: 15485760,
});
try {
nextjsApp.prepare().then(() => {
fastifyApp.listen({ port: env.PORT as number, host: env.HOST });
fastifyApp.log.info(`Server started on ${env.HOST}:${env.PORT}`);
});
} catch (err) {
fastifyApp.log.error(err);
process.exit(1);
}
Build updates
Now Next.js app needs to be built before starting the server, so a couple updates in package.json
:
"scripts": {
"build": "concurrently \"npm:build:fastify\" \"npm:build:nextjs\"",
"build:fastify": "vite build --outDir build --ssr src/server.ts",
"build:nextjs": "cd ./src/nextjs-app && npm run build",
"start": "pnpm run build && node build/server.mjs",
...
Result
With these changes applied, Fastify keeps handling all the routes it initially had:
-
/_health
- server status -
/api/pokemons
- Pokemons list -
/api/stats
- list of Pokemon stats
And everything under /nextjs-app
is handled by Next.js:
-
/nextjs-app
- main page of the new Next.js app, renders a list of Pokemons using the same data API does
Note on limitations
Vite HMR for the Fastify server became problematic after adding Next.js app - Next.js has separate build setup and it doesn't play well with Vite Node plugin out of the box.
However, HMR for Next.js app works fine and can be used with next dev
inside Next.js project.
As Next.js docs mention, using custom server disables automatic static optimizations and doesn't allow Vercel deploys.
Top comments (0)