Skip to content

If you're coming from Next.js or Nuxt.js and are looking for a practical Fastify-flavored replacement, jump straight to the @fastify/react or @fastify/vue documentation sections. All topics in this documentation section cover @fastify/vite from the ground up, targeted mainly at framework authors, and of course, potential contributors to @fastify/react, @fastify/vue and perhaps new core renderers for other frameworks.


Getting Started

This Fastify plugin allows you to run Vite's development server as middleware, expose your Vite application to your Fastify application, with configuration hooks to ease router integration and other customizations, and also automatically serve Vite builds, inferred from a Vite configuration file.

Why not a framework?

The key principle behind @fastify/vite is minimalism, based on the belief that Fastify and Vite alone are good enough core foundations.

Instead of adopting the arbitrary semantics and runtime of a full-blown SSR framework like Next.js or Nuxt.js, the idea is to just use Fastify for your backend needs, and just use Vite to build your client application, and still be able to run them together in a happy modular monolith.

Your client application can be written of course in whatever framework you like, without losing the ability to also perform SSR if needed.

In addition to the basic integration building blocks required to run Vite's development server as a middleware and serving your Vite application's production bundle, this plugin offers granular hooks that let you customize your Fastify server according to what your client application module provides, allowing you to essentially build your own framework.

You can read about creating a mini Next.js using @fastify/vite here:

https://hire.jonasgalvez.com.br/2022/may/18/building-a-mini-next-js/

For Vue and React users, @fastify/vue and @fastify/react are available as starting points featuring essential features from Nuxt.js and Next.js such as SSR data fetching and seamless SSR-to-CSR navigation, where client-side navigation and rendering takes over after SSR for the first render.

These packages are implemented the same way, following the specification found in Core Renderers. And most importantly, these packages are implemented using only the hooks provided by @fastify/vite.

A quick walkthrough

The vanilla React SPA (Single Page Application) project available in examples/ is a good starting point to demonstrate the basics of @fastify/vite. The only difference from this to running Vite's own development server directly is that it's executed as a middleware for the Fastify server, allowing other code and custom routes to be added. Vite's development server middleware only runs if you enable it, otherwise it will serve the production bundle (result of running vite build), whose location is automatically inferred from the Vite configuration file.

This basic SPA setup requires a Vite configuration file, the Fastify server file and the appropriate commands in package.json to run the server in development and production modes, and to build your Vite application.

To run this project, fastify, @fastify/vite, react and react-dom are the only dependencies required. Vite is only required in development.

bash
npm i fastify @fastify/vite react react-dom
npm i vite -D
npm i fastify @fastify/vite react react-dom
npm i vite -D
bash
pnpm add fastify @fastify/vite react react-dom
pnpm add vite -D
pnpm add fastify @fastify/vite react react-dom
pnpm add vite -D
bash
yarn add fastify @fastify/vite react react-dom
yarn add vite -D
yarn add fastify @fastify/vite react react-dom
yarn add vite -D

In server.js, notice how starting the development mode is conditioned to the presence of a --dev CLI argument passed to the Node.js process — could also be an environment variable. The default value for the dev configuration option is actually what you see in this snippet, a CLI argument check for --dev. All server.js files in the examples/ are using this default behavior.

js
import Fastify from 'fastify'
import FastifyVite from '@fastify/vite'

const server = Fastify()

await server.register(FastifyVite, {
  root: import.meta.url,
  dev: process.argv.includes('--dev'),
  spa: true
})

server.get('/', (req, reply) => {
  return reply.html()
})

await server.vite.ready()
await server.listen({ port: 3000 })
import Fastify from 'fastify'
import FastifyVite from '@fastify/vite'

const server = Fastify()

await server.register(FastifyVite, {
  root: import.meta.url,
  dev: process.argv.includes('--dev'),
  spa: true
})

server.get('/', (req, reply) => {
  return reply.html()
})

await server.vite.ready()
await server.listen({ port: 3000 })

This Fastify server only has a root route and it replies with the result of reply.html(). This html() method is added by @fastify/vite, using the result of the createHtmlFunction() configuration hook, and will seamlessly serve either the development or production version of your index.html, according to the dev configuration setting passed to the @fastify/vite plugin options, with or without server-side rendered markup.

As for awaiting on server.vite.ready(), this is what triggers the Vite development server to be started (if in development mode) and all client-level code loaded. This step is intentionally kept separate from the plugin registration, as you might need to wait on other plugins to be registered first.

In vite.config.js, notice how the Vite project root is set to ./client, and in server.js, how just passing import.meta.url is enough to let @fastify/vite know where to look for your Vite configuration file.

js
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import viteReact from '@vitejs/plugin-react'

const path = fileURLToPath(import.meta.url)
const root = resolve(dirname(path), 'client')

const plugins = [
  viteReact({ jsxRuntime: 'classic' })
]

export default { root, plugins }
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import viteReact from '@vitejs/plugin-react'

const path = fileURLToPath(import.meta.url)
const root = resolve(dirname(path), 'client')

const plugins = [
  viteReact({ jsxRuntime: 'classic' })
]

export default { root, plugins }

In package.json, take note of how the dev, start and build commands are defined, all just using your server.js file and Vite.

json
{
  "type": "module",
  "scripts": {
    "dev": "node server.js --dev",
    "start": "node server.js",
    "build": "vite build"
  },
  "dependencies": {
    "@fastify/vite": "latest",
    "fastify": "latest",
    "react": "latest",
    "react-dom": "latest"
  },
  "devDependencies": {
    "vite": "latest"
  }
}
{
  "type": "module",
  "scripts": {
    "dev": "node server.js --dev",
    "start": "node server.js",
    "build": "vite build"
  },
  "dependencies": {
    "@fastify/vite": "latest",
    "fastify": "latest",
    "react": "latest",
    "react-dom": "latest"
  },
  "devDependencies": {
    "vite": "latest"
  }
}

Then for the client code, cleanly separated in the client/ directory, you have index.html loading mount.js, base.jsx with a React component and mount.js loading it. Notice that Vite requires you to have an index.html file as it's the front-and-central build entry point.

html
<!DOCTYPE html>
<div id="root"><!-- element --></div>
<script type="module" src="/mount.js"></script>
<!DOCTYPE html>
<div id="root"><!-- element --></div>
<script type="module" src="/mount.js"></script>
js
import { createRoot } from 'react-dom/client'
import { createApp } from './base.jsx'

const root = createRoot(document.getElementById('root'))
root.render(createApp())
import { createRoot } from 'react-dom/client'
import { createApp } from './base.jsx'

const root = createRoot(document.getElementById('root'))
root.render(createApp())
jsx
import React from 'react'

export function createApp () {
  return (
    <p>Hello world from React and @fastify/vite!</p>
  )
}
import React from 'react'

export function createApp () {
  return (
    <p>Hello world from React and @fastify/vite!</p>
  )
}

Directory structure

This is what the directory structure for the example above looks like:

text
├── server.js
├── client/
│    ├── base.jsx
│    ├── mount.js
│    └── index.html
├── vite.config.js
└── package.json
├── server.js
├── client/
│    ├── base.jsx
│    ├── mount.js
│    └── index.html
├── vite.config.js
└── package.json

In all examples in this documentation, the client application code is kept in a client/ directory, to be explicitly separated from the server code and configuration files. In the vite.config.js previously shown, the project root is set as client. This is the recommended approach.

WARNING

It's important to realize that in server.js, the root configuration option determines where your vite.config.js is located. But in vite.config.js itself, the root configuration option determines your project root in Vite's context.

Regardless of whether you want to simply deliver a SPA bundle to the browser or perform SSR, projects using @fastify/vite will always need a minimum of three files: the Fastify server, an index.html file and a Vite configuration file.

Architectural primitives

If you want to have access to your client module on the server for SSR or other purposes, @fastify/vite offers granular hooks that let you set up a rendering function (receiving access to to your Vite application module), a HTML templating function and register server-side routes for your client routes. The diagram below shows the order of execution of each available hook.

text
└─ prepareClient()
   └─ createHtmlFunction()
      └─ createRenderFunction()
         └─ createRouteHandler()
            └─ createErrorHandler()
               └─ createRoute()
└─ prepareClient()
   └─ createHtmlFunction()
      └─ createRenderFunction()
         └─ createRouteHandler()
            └─ createErrorHandler()
               └─ createRoute()

You can consider these architectural primitives for building your own framework. Nearly all of them come with sensible defaults that you probably won't need to change for basic use cases, the exception being createRenderFunction(). For setting up SSR, you need to tell Fastify how to create a rendering function for your client application, that is, a function that will produce on the server, the same HTML markup your client application would on the client, so it can deliver it prerendered for speed.

If you're new to SSR, consider reading this step-by-step introduction.

In the next section, createRenderFunction() is explored using both simple and advanced (Nuxt.js and Next.js-like) examples.

Released under the MIT License.