DEV Community

Cover image for Serve Next.js with Fastify
applicazza
applicazza

Posted on

5

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;
    }
}

Enter fullscreen mode Exit fullscreen mode

Define plugin options

export interface FastifyNextJsOptions {
    dev?: boolean;
    basePath?: string;
}
Enter fullscreen mode Exit fullscreen mode

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

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

Finally, export the plugin

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

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

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

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

Jetbrains image

Don’t Become a Data Breach Headline

57% of organizations have suffered from a security incident related to DevOps toolchain exposures. Is your CI/CD protected? Check out these nine practical tips to keep your CI/CD secure—without adding friction.

Learn more

Top comments (1)

Collapse
 
seemstechies profile image
Sergey Inozemcev

Is it possible to friendship your code with @fastify/nextjs?

Neon image

Next.js applications: Set up a Neon project in seconds

If you're starting a new project, Neon has got your databases covered. No credit cards. No trials. No getting in your way.

Get started →

👋 Kindness is contagious

Explore a trove of insights in this engaging article, celebrated within our welcoming DEV Community. Developers from every background are invited to join and enhance our shared wisdom.

A genuine "thank you" can truly uplift someone’s day. Feel free to express your gratitude in the comments below!

On DEV, our collective exchange of knowledge lightens the road ahead and strengthens our community bonds. Found something valuable here? A small thank you to the author can make a big difference.

Okay