Serve Next.js with Fastify
Tags: nextjs, fastify, react, typescript
Published: 2021-06-23 08:44:07

In case you ever wondered how to integrate [Fastify](https://www.fastify.io) with [Next.js](https://nextjs.org) 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`](https://nodejs.org/api/http.html#http_class_http_incomingmessage) and [`http.OutgoingMessage`](https://nodejs.org/api/http.html#http_class_http_outgoingmessage) interfaces with methods and properties that we want to be available.

```typescript
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

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

Implement plugin logic

```typescript
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`.

```typescript
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

```typescript
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`**

```javascript
module.exports = {
  compress: false,
};
```

Simple usage of create plugin is as follows

```typescript
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](https://github.com/applicazza/fastify-nextjs) repository and try it with [npm](https://www.npmjs.com/package/@applicazza/fastify-nextjs).