Serve Next.js with Fastify
Tags: nextjs, fastify, node, typescript
Published: 2024-07-11 14:14:31

---
title: Serve Next.js with Fastify
published: true
description: How to setup custom Next.js server using Fastify
tags: nextjs, fastify, nodejs, typescript
# published_at: 2024-07-11 14:04 +0000
---


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](https://github.com/ilinieja/nextjs-custom-server-with-fastify).

# Initial setup

So imagine you have an existing Fastify project. For the sake of example I have a simple Fastify API [here](https://github.com/ilinieja/nextjs-custom-server-with-fastify/tree/initial). It's initialized from [this great Fastify template](https://github.com/ManUtopiK/vite-fastify-boilerplate) and has a couple of endpoints returning mock data:

- `/_health` - server status
- `/api/pokemons` - Pokemons list
- `/api/stats` - list of Pokemon stats

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

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

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

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

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

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

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

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

```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](https://nextjs.org/docs/pages/building-your-application/configuring/custom-server), using custom server disables automatic static optimizations and doesn't allow Vercel deploys.