Have you already watched Rich Hickey's "Simple Made Easy" talk? I need to begin this article with a few snapshots from his slides, because they encapsulate the essence of the @fastify/vite
stack.
After deconstructing the concepts of simple and complex, and making the case that developers are infatuated with programmer convenience — not to their benefit — Rich explores development speed and how simple and easy factor in.

Rich eloquently makes the argument that ignoring complexity will slow you down in the long run. Sprints become about refactoring and remaking things from scratch. Because you missed simple, you went straight to easy, for speed, for convenience, for a job. To arrive at easy, one must truly focus on simple first.
Rather than being content with just dropping files on a folder and expecting magic things to happen, know how things work, inside-out.
But how do we arrive at simple systems? Composability is fundamental:

But modularity doesn't necessarily imply simplicity, as individual modules can still be architected to be very tightly coupled. My take away is that the more individual components are restricted to their own domain and necessary connective abstractions only, the more truly modular they are.

⁂
In a sea of VC-infused tools products, easy is the primary selling point:
We'll make things nice and easy for you, don't worry about the details, our world-class engineers accounted for everything. You can trust us.
This captures the essence of what we are presented with as state of the art tooling in modern web development. It's no longer about about clear, well described constructs, or primitive components that interact cleanly with each other. It's always about easy and a sprinkle of fast to make it worthwhile.

But not simple.
My Golden Age of Nuxting
From 2018 to 2019, I was heavily involved with the Nuxt project, becoming a part of their core team. Nuxt enabled full stack Node.js/Vue.js applications, providing seamless SSR to CSR with zero configuration.
I have written many pieces about Nuxt.js, and dedicated several months of my life to building the now defunct NuxtPress.
It's very easy (no pun intended) to be infatuated with Nuxt.
% npm install nuxt
% mkdir pages
% echo '<template>Hello!</template>' > pages/index.vue
% npx nuxt dev
Wonderful, that's all I need — why bother looking under the hood?
The problem, as you might see, is the last line. The Nuxt CLI. The Next.js CLI. The <insert-your-framework-here> CLI. So much happens in there. So much that you have no knowledge or any control about.
I wasn't doing Node.js full stack development. I was doing Nuxt full stack development. Everything, the Nuxt way. Through Nuxt plugins, Nuxt modules, the Nuxt runtime API. I was trapped in the Nuxt vortex.
In 2020, I started to slowly drift away. I tried Fastify in a project and was convinced this was the clean, transparent away to build HTTP servers in Node.js, and anything else needed to grow from it. Or rather, it dawned on me that I really wanted to retain control over the logical steps to boot my application.
Everything that happens from instantiating a server, setting up routes, making a bundle available for SSR. I wanted to be able to easily lay my eyes on the code responsible for all that, but most importantly, easily replace anything I needed.
At first, I tried incorporating Nuxt into Fastify. I'd venture a guess that there are still systems I built running that exact Fastify + Nuxt setup, where Nuxt runs as a middleware. And I was initially extremely pleased with it.
But it still fell short on openness and transparency — Nuxt's black box, despite now cleanly attached as a middleware, was still responsible for too much.
Fastify's Beatiful New Vue
@fastify/vue@1.0.0
came out earlier this month. In reality, earlier alpha versions of the same code have been running in production by early adopters for a long time, but it matured with the release of @fastify/vite@8.0.0
, which implemented Vite 6's Environment API. I always expected I'd get constant bug reports from the people I knew were running it in production, but I never did.
It's not because @fastify/vue
is bullet proof and completely immune to bugs. There are still very likely some rough patches to be found. It's probably because it's extremely judicious about what it does: the absolute minimum.
There's no CLI, you are responsible for the server setup:
import Fastify from 'fastify'
import FastifyVite from '@fastify/vite'
const server = Fastify({
logger: {
transport: {
target: '@fastify/one-line-logger'
}
}
})
await server.register(FastifyVite, {
root: import.meta.dirname,
renderer: '@fastify/vue',
})
await server.vite.ready()
await server.listen({ port: 3000 })
Can you picture the difference in modularity and composability?
In this setup, you have a very clear separation of concerns:
- The Fastify server
- The Vite frontend application
- What connects them together (
@fastify/vite
) - What brings Vue into the mix (
@fastify/vue
)
Turning Vue components into Fastify routes
In a nutshell, @fastify/vue
enables you to treat Vue components as route modules, very much the same way Nuxt and Next.js allow.
@fastify/vue
's Vite plugin adds a micro application shell that turns .vue
files under pages/
into a routes
export, which is then picked up by Fastify, which then sets up all the server routes matching your frontend (Vue Router) routes.
When Vue route modules are loaded, Fastify checks for a few special exports to help determining aspects of the rendering of the page:
- Is it SSR-only?
export const serverOnly = true
. - Is it CSR-only?
export const clientOnly = true
. - Should it stream?
export const streaming = true
. - What should
<head>
look like?export function getMeta() {}
. - Any data fetching?
export functon getData() {}
.
All of that happens in a createRoute()
@fastify/vite
hook, provided by the @fastify/vue
package. Don't like how things are laid out? Want to rename or remove a thing or two? You can just provide your own hook implementation:
import FastifyVue from '@fastify/vue'
import { createRoute } from './my-overrides.js'
await server.register(FastifyVite, {
root: import.meta.dirname,
renderer: {
...FastifyVue,
createRoute,
}
})
Overriding just about anything
Overriding default behavior is not limited to replacing the @fastify/vite
hooks provided by @fastify/vue
. The micro application shell I mentioned earlier is provided via @fastify/vue/plugin
, a Vite plugin.
Consider what you see in the vue-base
starter:
├── README.md
├── client
│ ├── index.html
│ ├── pages
│ │ └── index.vue
│ └── root.vue
├── server.js
└── vite.config.js
Since there's no CLI, you have to provide server.js
— that's by design. But there's a big question: how is client/pages/index.vue
becoming the /
route?
That works because in reality, your @fastify/vue
application looks like this:
├── README.md
├── client
│ ├── index.html
│ ├── pages
│ │ └── index.vue
+ │ ├── context.js
+ │ ├── create.js
+ │ ├── hooks.js
+ │ ├── index.js
+ │ ├── layout.vue
+ │ ├── layouts
+ │ │ └── default.vue
+ │ ├── mount.js
│ ├── root.vue
+ │ ├── router.vue
+ │ └── routes.js
├── server.js
└── vite.config.js
These extra files, which don't need to be part of your application, compose the application shell. They are provided leveraging Vite's virtual modules, something many other Vite-based meta-frameworks also do. In this setup however, there's one fundamental difference: you can override any one of them.
Just place your version of any of these files on your application, and @fastify/vue/plugin
will automatically use your version instead.
That means extracting the code that sets up the main Vue application shell is as straightforward as cp -r node_modules/@fastify/vue/virtual/* .
It's batteries included, and batteries replaceable.
⁂
@fastify/vue@1.1.0
is out. It comes with official support for TypeScript and replaces unihead
with the more mature and battle-tested @unhead/vue
.

The @fastify/vite
user base is still growing today because it aims to provide that level of true modularity, with a clear effort to limit the amount of complexity and focus on primitive components and how they interact.
@fastify/vue
is nowhere near a place where you can blindly trust it to provide for your every Nuxt need. But I consider it a gemstone gifted to the Vue community, in raw form, ready to be polished up.
On one side, you have the maturity of Fastify for your backend needs. On the other side, you have an extremely light SSR setup that can leverage the lowest common denominator of all meta-frameworks: the Vite ecosystem.
My focus now is on providing as many real world examples as I possibly can. I'm looking at shadcn/vue
, ElementPlus
, Kysely
, tRPC
, everything I've used and can think of. And I'm hoping this will inspire more people to do the same.
The only way to break free from complexity in software development is to embrace it, own it, know how things work, then devise abstractions that turn complex into simple, without compromising modular independence.
⁂
Like the project? Like where it's headed? Consider becoming a GitHub Sponsor!
Happy Little Monoliths (the @fastify/vite
book) is also available!