Logo

dev-resources.site

for different kinds of informations.

Serve Next.js with Fastify

Published at
7/11/2024
Categories
nextjs
fastify
node
typescript
Author
ilinieja
Categories
4 categories in total
nextjs
open
fastify
open
node
open
typescript
open
Author
8 person written this
ilinieja
open
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.

fastify Article's
30 articles in total
Favicon
Understanding CORS and Setting it up with NestJS + Fastify 🚀
Favicon
Building a Real-Time Auction Platform: Behind the Scenes
Favicon
Async Local Storage is Here to Help You
Favicon
Master Node.js with the 5th Edition Cookbook
Favicon
Real-time data replication in Postgres and Node.js
Favicon
NestJS vs. Ditsmod: pipe features
Favicon
NodeJS Framework which one is Fast
Favicon
Gerando Documentação de API Automática com Fastify, @fastify/swagger e Zod
Favicon
Fastify v5 vs v4 — vs Encore.ts
Favicon
nestjs vs fastify Battle
Favicon
Speeding Up Your Website Using Fastify and Redis Cache
Favicon
Streaming PostgreSQL data with Fastify
Favicon
Fastify adoption guide: Overview, examples, and alternatives
Favicon
The Essential Do's and Don'ts of Fastify: Unlocking Your API's Potential
Favicon
How to Customize the Fastify Logger
Favicon
Express.js needs a funeral
Favicon
Serve Next.js with Fastify
Favicon
Nextjs custom server with fastify
Favicon
Testing Your API with Fastify and Vitest: A Step-by-Step Guide
Favicon
Introduction to Fastify: A Superior Node.js Framework
Favicon
Fastify Developers: Upgrade Your Logging with This Simple Guide
Favicon
How to create a lan server using Node.js and Fastify.js
Favicon
Criando sua API com Fastify e Prisma
Favicon
DynamoDB Single Table Design
Favicon
Stop exposing your Node.js metrics 🛑
Favicon
Fastify Meets WireMock: External Service Mocking
Favicon
The Fastify book is out!
Favicon
How to Automatically Consume RESTful APIs in Your Frontend
Favicon
Validate the Fastify input with Joi
Favicon
Starting With Fastify Today

Featured ones: