# Exploring Vue SSR with Fastify

In this article, we'll look at the state of native [Vue](https://vuejs.org/) integration in the Fastify framework.

[Vite](https://vitejs.dev/) has quickly become the industry standard for bundling modern applications, with frameworks like [Nuxt](https://nuxt.com/), [SvelteKit](https://kit.svelte.dev/) and [SolidStart](https://start.solidjs.com/) all based on it. Vite uses native ESM to quickly deliver files in development mode, but still produces a bundle for production. Check out the [motivation page](https://vitejs.dev/guide/why.html) in Vite's documentation to learn more.

[Fastify](https://fastify.dev/) has a mature and well-maintained core plugin for [Vite](https://vitejs.dev/) integration. It allows you to load any Vite application as a module in your Fastify server scope, both in production and development modes, [among many other things](https://fastify-vite.dev/).

## **Tutorial: Vue SSR Hello World**

Even though `@fastify/vite` allows you to simply serve a [static Vite application as a SPA](https://github.com/fastify/fastify-vite/tree/dev/examples/vue-vanilla-spa), its real value lies in making it possible to build hybrid SPA and SSR applications, i.e., where you can perform Server-Side Rendering (SSR), but once pages are loaded (*first render*), they start working as a SPA and no further full page requests are necessary.

This basic hybrid routing pattern is fully available out of the box via the [@fastify/vue](https://fastify-vite.dev/vue/) package, which wraps a [renderer](https://fastify-vite.dev/config/#renderer) for `@fastify/vite`. It's built on the latest versions of [Vue](https://vuejs.org/) and [Vue Router](https://router.vuejs.org/) and supports the hybrid SSR and CSR (Client-Side Rendered) navigation architecture employed by [Nuxt](https://nuxt.com/) and [Next.js](https://nextjs.org/).

Let's get started with it by building a simple Hello World application, and examine closely what's going on behind the scenes every step of the way.

The documentation recommends using one of the [starter templates](https://fastify-vite.dev/vue/#starter-templates) to get started, as they're already packed with a package.json file containing not only the dependencies, but also the build scripts and base ESLint configuration.

First, install [giget](https://github.com/unjs/giget) if you haven't already:

```plaintext
npm i giget -g
```

Next, pull a copy of the vue-base [starter template](https://fastify-vite.dev/vue/#vue-base) and install all dependencies:

```javascript
giget gh:fastify/fastify-vite/starters/vue-base#dev hello-world
cd hello-world
npm i
```

For reference, this is what `package.json` looks like (ESLint omitted):

```javascript
{
  "name": "vue-base",
  "description": "The vue-base starter template for @fastify/vue",
  "type": "module",
  "scripts": {
    "dev": "node server.js --dev",
    "build": "npm run build:client && npm run build:server",
    "start": "node server.js",
    "build:client": "vite build --outDir dist/client --ssrManifest",
    "build:server": "vite build --outDir dist/server --ssr /index.js",
    "lint": "eslint . --ext .js,.jsx --fix"
  },
  "dependencies": {
    "@fastify/one-line-logger": "latest",
    "@fastify/vite": "^6.0.3",
    "@fastify/vue": "^0.5.0",
    "fastify": "^4.24.3",
    "unihead": "^0.0.6",
    "vue": "^3.4.19",
    "vue-router": "^4.2.5"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^4.5.0",
    "vite": "^5.0.2",
    "postcss": "^8.4.31",
    "postcss-nesting": "^12.0.2",
    "postcss-preset-env": "^7.7.1",
    "tailwindcss": "^3.4.1"
  }
}
```

There's very little beyond Fastify, Vite, `@fastify/vite`, PostCSS and Tailwind in this boilerplate. It also includes [PostCSS Preset Env](https://www.npmjs.com/package/postcss-preset-env) and [CSS Nesting](https://www.npmjs.com/package/postcss-nesting) out of the box.

Before we dive into it, start it with `npm run dev` and navigate to `http://localhost:3000` to make sure you're all set:

## **Configuring Fastify**

The starter template comes with the following `server.js`:

```javascript
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.url, 
  renderer: '@fastify/vue',
})

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

A few things of note here:

* The server is  using `@fastify/one-line-logger` for human readable logs during development, but you'll want to condition that to `process.stdout.isTTY` so it continues to produce regular JSON logs for production.
    
* The `@fastify/vite` plugin registration needs to be *awaited on* for proper function. Before starting the server, it also requires you to *await on* the `vite.ready()` method. In development mode, this ensures Vite's development server middleware has been started. In production, it simply loads your Vite production bundle.
    
* `@fastify/vite`'s development mode is enabled automatically by detecting the presence of the --dev flag in the arguments passed to the Node.js process running your Fastify server. This default behavior can be changed as follows:
    

```javascript
await server.register(FastifyVite, { 
  dev: process.argv.includes('--custom-dev-flag'),
  root: import.meta.url, 
  renderer: '@fastify/vue',
})
```

It could also use an environment variable for the same purpose.

* @fastify/vue is essentially [a set of configuration options and hooks](https://github.com/fastify/fastify-vite/blob/dev/packages/fastify-vue/index.js) for @fastify/vite controlling how Vue route modules become Fastify routes. 
    

You can replace parts of it with your own should the need ever present itself:

```javascript
import FastifyVue from '@fastify/vue'

await server.register(FastifyVite, { 
  dev: process.argv.includes('--custom-dev-flag'),
  root: import.meta.url, 
  renderer: {
    ...FastifyVue,
    createRoute () {
      // Your custom createRoute implementation
    }
  }
})
```

## **Configuring Vite**

The starter template comes with the following `vite.config.js`:

```javascript
import { join, dirname } from 'path'
import { fileURLToPath } from 'url'

import viteVue from '@vitejs/plugin-vue'
import viteFastifyVue from '@fastify/vue/plugin'

const path = fileURLToPath(import.meta.url)

export default {
  root: join(dirname(path), 'client'),
  plugins: [
    viteVue(), 
    viteFastifyVue(),
  ],
}
```

Notice that the Vite application code is kept in a `client` subfolder, as indicated by the `root` setting in the config. This convention is recommended by´ `@fastify/vite` to maintain a clear separation of client and server code

Other than that, it packs standard Vite + Vue 3 configuration.

There's only the addition of `@fastify/vue/plugin`, the Vite plugin included in `@fastify/vue` to make [smart imports](https://fastify-vite.dev/vue/project-structure#smart-imports) available.

*Note:  One example of a smart import is* `/:root.vue`*. The* `/:` *prefix tells* `@fastify/vue/plugin` *to provide the internal* `root.vue` *file, part of the package itself, but only in case it doesn't find a* `root.vue` *file in your project root.*

*This is useful because most applications don't require any changes, for instance, to the* `mount.js` *script that mounts the Vue application.* `@fastify/vue` *implements a simple Nuxt-like Vue application shell using these files, but also like in Nuxt, these files don't exist in your project for the application to load.*

If you do want to customize how it works, you can shadow any of these internal modules by making your Vite project root contain a file with the same name. See the full list of files provided via smart imports by `@fastify/vue` [here](https://fastify-vite.dev/vue/project-structure#smart-imports).

## **Configuring the Vue application shell**

The starter template comes with the minimum amount of files needed to get you started, including `client/index.html`, used by Vite to resolve your dependencies, and `client/index.js`, used by `@fastify/vite` to gain access to your Vite application code on the server-side.

Let's begin by looking at `client/index.html`:

```javascript
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="/base.css">
<!-- head -->
</head>
<body>
<div id="root"><!-- element --></div>
</body>
<!-- hydration -->
<script type="module" src="/:mount.js"></script>
</html>
```

Notice the HTML comment placeholders for head, element and hydration. When `@fastify/vite` loads your Vite application, it'll load `index.html` and compile it into a templating function and register it as the `reply.html()` method.

You can use your own implementation of a templating function by providing your custom [createHtmlTemplateFunction()](https://fastify-vite.dev/guide/templating-function). The default in implementation accepts the dot notation in placeholders, and also allows for any fragment to be a Node.js Readable instance. You can copy and tweak it to your needs, e.g., you might want to remove the ability to support fragments as Readable instances if you have no use for this feature.

See `@fastify/vue`'s implementation of the `createHtmlFunction()` hook [here](https://github.com/fastify/fastify-vite/blob/dev/packages/fastify-vue/index.js#L46). You'll very rarely need to override this, but it's conveniently simple to do so if you ever need to.

As for SSR itself, it's implemented via `createRenderFunction()`, another configuration hook for `@fastify/vite`. See `@fastify/vue`'s internal implementation below:

```javascript
export async function createRenderFunction ({ routes, create }) {
  // Create convenience-access routeMap
  const routeMap = Object.fromEntries(routes.toJSON().map((route) => {
    return [route.path, route]
  }))
  // create is exported by client/index.js
  return async function (req) {
    let stream = null
    let body = null
    // Creates main Vue component with all the SSR context it needs
    if (!req.route.clientOnly) {
      const app = await create({
        routes,
        routeMap,
        ctxHydration: req.route,
        url: req.url,
      })
      if (req.route.streaming) {
        stream = renderToNodeStream(app.instance, app.ctx)
      } else {
        body = await renderToString(app.instance, app.ctx)
      }
    }
    // Perform SSR, i.e., turn app.instance into an HTML fragment
    // The SSR context data is passed along so it can be inlined for hydration
    return { routes, context: req.route, body, stream }
  }
}
```

Like `createHtmlFunction()`, you'll rarely need to provide your own version of it. It's displayed above to demonstrate its simplicity.

Both hooks covered above rely on `@fastify/vite`'s ability to load the Vite application as a module. For this to work, it relies on having `client/index.js` defined as follows:

```javascript
import routes from '/:routes.js'
import create from '/:create.js'

export default { 
  context: import('/:context.js'), 
  routes,
  create,
}
```

It needs to provide access to both the Vue page `routes` and the main Vue component `create` function. Every object in the `routes` array needs to contain a path and a `component` property, among other settings. This is automatically handled by the internal virtual module `routes.js`, imported here via the `/:routes.js` smart import.

It also needs to provide the [route context initialization file](https://fastify-vite.dev/vue/route-context#init-module), used by `@fastify/vue` to populate the return value of the useRouteContext() function, also provided internally via the `/:core.js` smart import. In this case, if you don't provide a `context.js` file, an empty one is provided automatically via the smart import.

## **Injecting Vue global properties**

We'll cover the context.js file shortly, but first, let's address injecting global properties into your Vue application. Nuxt users will typically use a plugin for this purpose.

With this, you can place a `root.vue` file at the root of your Vite project, shadowing the one provided internally by `@fastify/vue`, and export a `configure()` function:

```javascript
<script>
import Helper from '/helper.js'

export { default } from '/:router.vue'

export function configure({ app }) {
  app.config.globalProperties.$helper = new Helper()
}
```

## **The route context initialization file**

One might say the `context.js` file is a peculiar feature of `@fastify/vue`, but Nuxt users will find it somewhat similar to using the Nuxt v2 `store/` folder.

The `context.js` file exists mainly for two main special exports, shown below:

```javascript
export function state () {
  return {
    user: {
      authenticated: false,
    },
  }
}

export default (ctx) => {
  if (ctx.req?.user) {
    ctx.state.authenticated = true
  }
}
```

* The `state()` function export is used as a `reactive()` store initializer. It has special processing for adequate serialization and hydration.
    
* The default export function runs exactly once on the server and once on the client during *first render*. This means that it is not executed again in subsequent client-side navigation.
    

## **Consuming the route context**

In our `vue-base` starter template, let's change `server.js` to include a req.user decoration if a certain magic query parameter is set:

```javascript
server.decorate('user', false)
server.addHook('onRequest', (req, _, done) => {
  if (req.query.magic === '1') {
    req.user = true
  }
  done()
})
```

Next, we'll add a `context.js` file defined exactly as the one listed in the previous section, and update the `pages/index.vue` template to consume `state` from the route context. The `useRouteContext()` is a hook provided by `@fastify/vue`'s internal `core.js` virtual module and can be used in every component.

```javascript
<script setup>
import { useRouteContext } from '/:core.js'
import logo from '/assets/logo.svg'

const { state } = useRouteContext()
const message = 'Welcome to @fastify/vue!'
</script>

<template>
  <h1>{{ message }}</h1>
  <p><img :src="logo" /></p>
  <p v-if="state.authenticated">
    You're authenticated!
  </p>
</template>
```

Run it with `node server.js --dev` and navigate to [`http://localhost:3000/?magic=1`](http://localhost:3000/?magic=1)`:`

Note that state is simply a `reactive()` instance.

You can still use root.vue to define another type of store, or even provide your own versions of `core.js` and `create.js` to do so.

## **Data fetching via getData()**

`@fastify/vue` implements the [getServerSideProps()](https://nextjs.org/docs/pages/building-your-application/data-fetching/get-server-side-props)\-style of data fetching.

Route modules can export a `getData()` function. This always runs only on the server, regardless of whether it's executed for SSR or triggered by an API request during client-side navigation.

`@fastify/vue` takes care of automatically registering an internal API endpoint for it and calling it every time there's client-side navigation, very much like Next.js.

Let's expand `pages/index.vue` to include this:

```javascript
<script>
export async function getData () {
  const request = await fetch('https://jsonplaceholder.typicode.com/posts/1')
  return await request.json()
}
</script>

<script setup>
import { useRouteContext } from '/:core.js'
import logo from '/assets/logo.svg'

const { state, data } = useRouteContext()
const message = 'Welcome to @fastify/vue!'
</script>

<template>
  <h1>{{ message }}</h1>
  <p><img :src="logo" /></p>
  <p v-if="state.authenticated">
    <p>You're authenticated!</p>
  </p>
  <h2>{{ data.title }}</h2>
  <p>{{ data.body }}</p>
  <footer>
    <p><router-link to="/other">Go to /other page</router-link></p>
  </footer>
</template>
```

The key thing to understand in this example is that by the time `useRouteContext()` runs, the getData() function will already have been executed. In case it's triggered by client-side navigation, an API request is automatically fired and managed via an internal Vue Router `beforeEach()` handler registered by `@fastify/vue`.

## **Head management via getMeta()**

Similarly, you can export `getMeta()` from route modules to determine the shape of your `<head>` document section, namely, provide the value for the `<title>` tag and additional `<link>` and `<script>` tags.

```javascript
<script>
export function getMeta () {
  return {
    title: 'Welcome to @fastify/vue!'
  }
}
</script>
```

## **Building and serving in production mode**

To run it in production mode, just remove the `--dev` flag. This also requires you to run `vite build` beforehand so the production bundle is available when `@fastify/vite` runs.

The catch here is that you need run `vite build` twice:

* `vite build --outDir dist/client --ssrManifest`
    
    * This produces the client bundle, which is loaded by the client via `index.html`.
        
* `vite build --outDir dist/server --ssr /index.js`
    
    * This produces the server bundle, which is what gets exposed to Fastify on the server, namely `client/index.js`, which in turns exports the routes array and anything else that might be needed on the server, accessible via `@fastify/vite`'s hooks as seen before.
        

Here's how you need to set up your build scripts in `package.json`:

```javascript
{
  "scripts": {
    "build": "npm run build:client && npm run build:server",
    "build:client": "vite build --outDir dist/client --ssrManifest",
    "build:server": "vite build --outDir dist/server --ssr /index.js",
  }
}
```

Notice that for the client build, we also tell Vite to produce a SSR manifest file. This is a JSON file mapping all client modules and their imported modules to bundle asset URLs. It's loaded automatically by `@fastify/vite` and made available in runtime in production mode.

## **Movie Quotes App: Vue rewrite**

For this part, we're using a [fork](https://github.com/galvez/tutorial-movie-quotes-app) of the [Movie Quotes App](https://docs.platformatic.dev/docs/getting-started/movie-quotes-app-tutorial/) tutorial source code. This app actually contains two services, one for the API, and another for serving the frontend.

```plaintext
└── apps/
    ├── movie-quotes-api
    └── movie-quotes-frontend
```

The API is served from `movie-quotes-api`, a Fastify application running via the Platformatic DB CLI. The frontend is an Astro SPA which makes requests to the API retrieving data only. The frontend is served in this example via Astro's own development server, but its bundle could be served [from a variety of servers](https://docs.astro.build/en/guides/server-side-rendering/).

It's a simple app with four routes:

* the index page, `pages/index.astro`.
    
* the add movie quote page, `pages/add.astro`.
    
* the edit quote page, `pages/edit/[id.astro]`.
    
* and the delete quote page, `pages/delete/[id.astro]`.
    

All movie quote associated actions have their own components as well. There are also some vanilla JavaScript libraries responsible for a few bits of functionality, such as talking to the Platformatic DB GraphQL API and confirming actions before running them.

Below is the full source code structure for the original `movie-quotes-frontend service`.

```plaintext
.
└── movie-quotes-frontend-htmx/
    ├── components/
    │   ├── QuoteActionDelete.astro
    │   ├── QuoteActionEdit.astro
    │   ├── QuoteActionLike.astro
    │   └── QuoteForm.astro
    ├── layouts/
    │   └── Layout.astro
    ├── lib/
    │   ├── quotes-api.js
    │   └── request-utils.js
    ├── pages/
    │   ├── delete/
    │   │   └── [id].astro
    │   ├── edit/
    │   │   └── [id].astro
    │   ├── add.astro
    │   └── index.astro
    └── scripts/
        └── quote-actions.js
```

## **Rewriting the boilerplate**

We'll start by creating a exact copy of `movie-quotes-frontend` for the Vue version, making the directory structure now look like the following:

```plaintext
└── apps/
    ├── movie-quotes-api
    ├── movie-quotes-frontend
    └── movie-quotes-frontend-vue
```

Next, in `movie-quotes-frontend-vue` we'll add `server.js` to replace the Astro server:

```javascript
import Fastify from 'fastify'
import FastifyVite from '@fastify/vite'
import FastifyFormBody from '@fastify/formbody'

const server = Fastify({
  logger: {
    transport: {
      target: '@fastify/one-line-logger'
    }
  }
})

await server.register(FastifyFormBody)
await server.register(FastifyVite, { 
  root: import.meta.url, 
  renderer: '@fastify/vue',
})

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

And a `vite.config.js` file to import `@fastify/vue`s accompanying Vite plugin and set up CSS modules to use the camel case convention for class names:

```javascript
import { join, dirname } from 'path'
import { fileURLToPath } from 'url'

import inject from '@rollup/plugin-inject'
import viteFastifyVue from '@fastify/vue/plugin'

export default {
  root: join(dirname(fileURLToPath(import.meta.url)), 'client'),
  plugins: [
    viteFastifyVue()
  ],
  css: {
    modules: {
      localsConvention: 'camelCase'
    }
  }
}
```

## **Loading .env files**

Astro handles `.env` files automatically, and so does Vite. In fact, Astro's support for `.env` files comes from [Vite's own built-in support](https://vitejs.dev/guide/env-and-mode). But we also need to make `.env` files available to Fastify. In this example we'll use [fluent-env](https://github.com/galvez/fluent-env), a convenient wrapper around [env-schema](https://github.com/fastify/env-schema) and [fluent-json-schema](https://github.com/fastify/fluent-json-schema).

All we need to do is add the following import at the top of `server.js`:

```javascript
import `fluent-env/auto`
```

And change the `PUBLIC_GRAPHQL_API_ENDPOINT` variable to `VITE_GRAPHQL_API_ENDPOINT` to match Vite's default standard. Only environment variables prefixed with `VITE_` are allowed to make it into the bundle. 

In the end, this is not going to be required  since we're not ever running code that relies on the single environment variable of this application on the client, but it's still good to know.

## **Rewriting the layout component**

The main difference from the Astro version is that the main HTML shell needs to live in an `index.html` file, which is just the body rendered by the main layout component.

Here's how `layouts/default.vue` reads:

```javascript
<script setup>
import { useRouteContext } from '/:core.js'

const context = useRouteContext()
const { page } = context.data

const navActiveClasses = 'font-bold bg-yellow-400 no-underline'
</script>

<template>
  <header class="prose mx-auto mb-6">
    <h1>🎬 Movie Quotes</h1>
  </header>
  <nav class="prose mx-auto mb-6 border-y border-gray-200 flex">
    <a href="/?sort=createdAt" :class="['p-3', page === 'listing-createdAt' ? navActiveClasses : '']">Latest quotes</a>
    <a href="/?sort=likes" :class="['p-3', page === 'listing-likes' ? navActiveClasses : '']">Top quotes</a>
    <a href="/add" :class="['p-3', page === 'add' ? navActiveClasses : '']">Add a quote</a>
  </nav>
  <section class="prose mx-auto">
    <slot />
  </section>
</template>
```

## **Rewriting the ‘like’ action component**

In the original Astro version, the heart SVG that serves to perform a ‘like’ action on a movie quote is controlled by client-side code, triggering client-side requests to the GraphQL API. That means importing `@urql/core` on the client bundle, a 20kb+ library.

Let's avoid that by going with a server-side endpoint.

First we'll add an endpoint to return the number of likes for a movie quote, in plain-text:

```javascript
server.post('/api/like-movie-quote/:id', async (req, reply) => {
  const id = Number(req.params.id)
  const liked = await req.quotes.graphql({
    query: `
      mutation($id: ID!) {
        likeQuote(id: $id)
      }
    `,
    variables: { id },
  })
  reply.type('text/plain')
  reply.send(liked.toString())
})
```

Note that we're already using [Platformatic](http://platformatic.dev)'s auto-generated GraphQL client in this snippet. This is a very simple POST endpoint which runs a mutation and returns the updated value. Now, we can use it in our new `QuoteActionLike.vue` component to do the update:

```javascript
<script lang="ts" setup>
import { ref } from 'vue'

const props = defineProps<{
  id: string
  likes: number
}>()

const likes = ref(props.likes)

async function likeQuote() {
  const req = await fetch(`/api/like-movie-quote/${props.id}`, { method: 'POST' })
  const newLikes = Number(await req.text())
  if (newLikes !== likes) {
    likes.value = newLikes
  }
}
</script>

<template>
  <span 
    @click.once="likeQuote"
    class="like-quote mr-5 flex items-center"
    :class="{'cursor-pointer': likes === 0, liked: likes > 0}">
    <svg 
      class="like-icon w-6 h-6 mr-2 text-red-600"
      xmlns="http://www.w3.org/2000/svg" 
      fill="none" 
      viewBox="0 0 24 24" 
      stroke-width="1.5" 
      stroke="currentColor" >
        <path stroke-linecap="round" stroke-linejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z" />
      </svg>
      <span class="likes-count" w-8>{{ likes }}</span>
  </span>
</template>

<style scoped>
  .like-quote:hover .like-icon,
  .like-quote.liked .like-icon {
    fill: currentColor;
  }
</style>
```

Like Astro, Vue allows you to inline `<style>` tags in a single file component and have them automatically scoped to your component.

## **Rewriting the movie quotes listing**

The new `pages/index.vue` template starts with a `getData()` definition to retrieve the movie quote list on the server, using the Platformatic DB client:

```typescript
<script lang="ts">
import QuoteActionEdit from '../components/QuoteActionEdit.vue'
import QuoteActionDelete from '../components/QuoteActionDelete.vue'
import QuoteActionLike from '../components/QuoteActionLike.vue'

export async function getData ({ req }) {
  const allowedSortFields = ['createdAt', 'likes']
  const searchParamSort = req.query.sort
  const sort = allowedSortFields.includes(searchParamSort)
    ? searchParamSort
    : 'createdAt'

  const quotes = await req.quotes.graphql({
    query: `
      query {
        quotes(orderBy: {field: ${sort}, direction: DESC}) {
          id
          quote
          saidBy
          likes
          createdAt
          movie {
            id
            name
          }
        }
      }
    `,
  })

  return {
    quotes,
    page: `listing-${sort}`,
  }
}

export function getMeta () {
  return {
    title: 'All quotes'
  }
}
</script>
```

Note that the `getMeta()` function defines the page's `<title>`, and the `getData()` function is responsible for returning `page`, which also becomes accessible in the `layouts/default.vue` component.

Next we have the `<script setup>` section consuming the data:

```typescript
<script lang="ts" setup>
import { useRouteContext } from '/:core.js'
const { page, quotes } = useRouteContext().data
</script>
```

We also have the new Vue template:

```xml
<template>
  <template v-if="quotes.length > 0">
    <div v-for="quote in quotes" class="border-b mb-6">
      <blockquote class="text-2xl mb-0">
        <p class="mb-4">{{ quote.quote }}</p>
      </blockquote>
      <p class="text-xl mt-0 mb-8 text-gray-400">
        — {{ quote.saidBy }}, {{ quote.movie?.name }}
      </p>
      <div class="flex flex-col mb-6 text-gray-400">
        <span class="flex items-center">
          <QuoteActionLike :id="quote.id" :likes="quote.likes" />
          <QuoteActionEdit :id="quote.id" />
          <QuoteActionDelete :id="quote.id" />
        </span>
        <span class="mt-4 text-gray-400 italic">Added {{
          new Date(Number(quote.createdAt)).toUTCString()
        }}</span>
      </div>
    </div>
  </template>
  <p v-else>No movie quotes have been added.</p>
</template>
```

## **Wrapping up**

In this blog, we demonstrated how to integrate Vue.js with Fastify for SSR, covering setup, data fetching, routing, and production deployment, while also providing a practical example of migrating an Astro app.

There are several parts of the rewrite not directly covered in this article, only because the sections we explored in this blog capture the essence of the changes involved. Make sure to see [full source code here](https://github.com/platformatic/tutorial-movie-quotes-app/tree/main/apps/movie-quotes-frontend-vue).

The JavaScript web framework ecosystem has had many interesting innovations in the past few years, with frameworks like Next.js, Nuxt and Astro offering a truly astonishing feature set. There are those of us however who continuously seek understanding of the underlying technology and continuously seek to abstract it the simplest and most concise way possible. As Johannes Kepler once said:

[Nature uses as little as possible of anything.](https://en.wikiquote.org/wiki/Johannes_Kepler)

With `@fastify/vite`, Fastify provides just that: the absolute minimum amount of moving parts to perform SSR and integrate a Vite frontend into your application.
