DEV Community

ilinieja
ilinieja

Posted on

Serve Next.js with Fastify

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;
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
    });
});
Enter fullscreen mode Exit fullscreen mode

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();
  });
});
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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",
});
Enter fullscreen mode Exit fullscreen mode

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}`);
});
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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",
    ...
Enter fullscreen mode Exit fullscreen mode

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)