Serve Next.js with Fastify

In case you ever wondered how to integrate Fastify with Next.js and let latter be a part of Fastify's lifecycle this short guide is for you.

As you may know, Next.js has limited public API that does not provide anything that would return response as string or object. Moreover, Next.js writes response directly to stream that is being sent to the client.

What if we want to maintain session and attach or detach cookies that are being handled with Fastify when serving Next.js content?

Node.Js Proxy to the resque!

Let's write a simple plugin that will wrap http.IncomingMessage and http.ServerResponse and forward necessary calls to Fastify.

First, let's augment Fastify instance as well as http.IncomingMessage and http.OutgoingMessage interfaces with methods and properties that we want to be available.

import { FastifyReply, FastifyRequest } from 'fastify';

declare module 'fastify' {
    interface FastifyInstance {
        nextJsProxyRequestHandler: (request: FastifyRequest, reply: FastifyReply) => void;
        nextJsRawRequestHandler: (request: FastifyRequest, reply: FastifyReply) => void;
        nextServer: NextServer;
        passNextJsRequests: () => void;
        passNextJsDataRequests: () => void;
        passNextJsDevRequests: () => void;
        passNextJsPageRequests: () => void;
        passNextJsStaticRequests: () => void;
    }
}

declare module 'http' {

    // eslint-disable-next-line no-unused-vars
    interface IncomingMessage {
        fastify: FastifyRequest;
    }

    // eslint-disable-next-line no-unused-vars
    interface OutgoingMessage {
        fastify: FastifyReply;
    }
}

Define plugin options

export interface FastifyNextJsOptions {
    dev?: boolean;
    basePath?: string;
}

Implement plugin logic

import { FastifyPluginAsync, FastifyReply, FastifyRequest } from 'fastify';
import fastifyPlugin from 'fastify-plugin';
import { IncomingMessage, ServerResponse } from 'http';
import Next from 'next';
import { NextServer } from 'next/dist/server/next';
import fastifyStatic from 'fastify-static';

const fastifyNextJs: FastifyPluginAsync<FastifyNextJsOptions> = async (fastify, { dev, basePath = '' }) => {
  if (dev === undefined) {
    dev = process.env.NODE_ENV !== 'production';
  }

  const nextServer = Next({
    dev,
  });

  const nextRequestHandler = nextServer.getRequestHandler();

  const passNextJsRequestsDecorator = () => {
    fastify.passNextJsDataRequests();

    if (dev) {
      fastify.passNextJsDevRequests();
    } else {
      fastify.passNextJsStaticRequests();
    }

    fastify.passNextJsPageRequests();

  };

  const passNextJsDataRequestsDecorator = () => {
    fastify.get(`${basePath}/_next/data/*`, nextJsProxyRequestHandler);
  };

  const passNextJsDevRequestsDecorator = () => {
    fastify.all(`${basePath}/_next/*`, nextJsRawRequestHandler);
  };

  const passNextJsStaticRequestsDecorator = () => {
    fastify.register(fastifyStatic, {
      prefix: '${basePath}/_next/static/',
      root: `${process.cwd()}/.next/static`,
      decorateReply: false,
    });
  };

  const passNextJsPageRequestsDecorator = () => {
    if (basePath) {
      fastify.all(`${basePath}`, nextJsProxyRequestHandler);
    }
    fastify.all(`${basePath}/*`, nextJsProxyRequestHandler);
  };
  fastify.decorate('passNextJsRequests', passNextJsRequestsDecorator);
  fastify.decorate('passNextJsDataRequests', passNextJsDataRequestsDecorator);
  fastify.decorate('passNextJsDevRequests', passNextJsDevRequestsDecorator);
  fastify.decorate('passNextJsStaticRequests', passNextJsStaticRequestsDecorator);
  fastify.decorate('passNextJsPageRequests', passNextJsPageRequestsDecorator);
  fastify.decorate('nextServer', nextServer);

  const nextJsProxyRequestHandler = function (request: FastifyRequest, reply: FastifyReply) {
    nextRequestHandler(proxyFastifyRawRequest(request), proxyFastifyRawReply(reply));
  };

  const nextJsRawRequestHandler = function (request: FastifyRequest, reply: FastifyReply) {
    nextRequestHandler(request.raw, reply.raw);
  };

  fastify.decorate('nextJsProxyRequestHandler', nextJsProxyRequestHandler);
  fastify.decorate('nextJsRawRequestHandler', nextJsRawRequestHandler);

  fastify.addHook('onClose', function () {
    return nextServer.close();
  });

  await nextServer.prepare();
};

Additionally, let's proxy necessary calls to http.OutgoingMessage.

const proxyFastifyRawReply = (reply: FastifyReply) => {
  return new Proxy(reply.raw, {
    get: function (target: ServerResponse, property: string | symbol, receiver: unknown): unknown {
      const value = Reflect.get(target, property, receiver);

      if (typeof value === 'function') {
        if (value.name === 'end') {
          return function () {
            return reply.send(arguments[0]);
          };
        }
        if (value.name === 'getHeader') {
          return function () {
            return reply.getHeader(arguments[0]);
          };
        }
        if (value.name === 'hasHeader') {
          return function () {
            return reply.hasHeader(arguments[0]);
          };
        }
        if (value.name === 'setHeader') {
          return function () {
            return reply.header(arguments[0], arguments[1]);
          };
        }
        if (value.name === 'writeHead') {
          return function () {
            return reply.status(arguments[0]);
          };
        }
        return value.bind(target);
      }

      if (property === 'fastify') {
        return reply;
      }

      return value;
    },
  });
};

Finally, export the plugin

export default fastifyPlugin(fastifyNextJs, {
  fastify: '3.x',
});

From now on, after plugin registration you can serve Next.js content with Fastify and enjoy all the benefits of both frameworks.

Do not forget to disable compression in next.config.js

module.exports = {
  compress: false,
};

Simple usage of create plugin is as follows

const dev = process.env.NODE_ENV !== 'production';

fastify.register(fastifyNextJs, {
    dev,
});

await fastify.after();

fastify.passNextJsDataRequests();

if (dev) {
    fastify.passNextJsDevRequests();
} else {
    fastify.passNextJsStaticRequests();
}

fastify.passNextJsPageRequests();

In case you have ideas how to improve the plugin and contribute to its development, visit its GitHub repository and try it with npm.

30