Welcome! It's been a while since my last article, but I'm excited to return with the ninth part of my series on the Vuejs Amsterdam conference 2022. In this part, we will delve into the exciting world of full-stack development using Nuxt 3 and Nitro.
You can watch the talk on JSWorld's YouTube channel as well.
I remember last time [two years ago] I was on the stage here, we had a really big dream of Nuxt to extending from an SSR application to something that we can use everywhere and beyond it — Pooya Parsa, Head of Framework at Nuxt.js
To make this happen, Nuxt.js had to rewrite everything, which took more than 2 years. But before talking about Nuxt 3, Pooya talks about Nuxt 2, its strengths and limitations, and the reason behind starting with a project called Nitro which is a part of Nuxt 3.
Nuxt 2 is a framework mainly focused on Server Side Rendering due to its benefits. Now that we already have a server, why not take this opportunity to extend it and add our APIs? Here's how we do it in Nuxt 2 using Server Middleware:
// server-middleware/test.ts
export default function (req, res) {
res.end(JSON.stringify({ hello: 'world' }))
}
To register this middleware:
// nuxt.config
export default {
serverMiddleware: {
'/api/test': './server-middleware/test'
}
}
Now if we call http://localhost:3000/api/test
we get the response:
{
"hello": "world"
}
As you can see it's plain simple node.js middleware, supports typescript and it had a basic router system using prefixes. But it has some limitations. For example, lack of dynamic path support in the route, no alias and transform support, and files are not bundled and are loaded in the runtime and we have to deploy the entire source code to the production to make it work.
Nuxt 2 is using an older version of express.js called connect which is lighter. On top of that, it has an isolation for SSR, so every code that you write like plugins and components is bundled through webpack or vite and is loaded in this isolated environment with HMR support.
It supports TypeScript with unjs/jiti
and is fast at cache invalidation, but the server and node_modules were not isolated and the entire node_modules folder had to be deployed. Runtimes varied between production and development too.
We Could use nuxt 2 not only as a standalone framework but also as a middleware for an existing application. That gives us lots of flexibility but it comes with its own downsides.
node_modules
entirely to deploy to production, which limits us for some environments like serverless or makes it impossible in some other environments like edge rendering which are workers - Because we can not deploy node application with node_modules to edge.When you try to fetch something from the server side in your nuxt 2 application some strange things happen.
In this example, we are loading a product page in a webshop. In order to Server side render this endpoint we have to fetch the product's data, from the same API that is living on the same server. Maybe the only upside is that we have a shared server and there is no need to set up another server for the APIs.
But there are more downsides:
Nuxt 2 allows you to generate a static application that is not dependent on server files so you can deploy it everywhere.
When you run the nuxt generate
command to generate the static app, nuxt will:
dist/
is ready to use!
It is simple and fast in implementation, has full static support, and there is also a shared generation cache feature between pages. But there are also some issues with that.
Deploying a nuxt 2 SSR App to production seems to be easy, but it's not efficient.
During an internal team meeting about cloud flare workers at the time that the team was actively working on nuxt 2, Pooya realized that nuxt was limited in some important aspects.
Nitro project started as an experiment on top of nuxt 2 thanks to the modularity and the hooks that it provides. They managed to create a proof of concept to deploy nuxt 2 apps to the edge and remove most of those limitations.
Nitro's goal according to Pooya is to deploy any JavaScript server anywhere you want, including edge, and he even claims that we can even run a package like express in the browser service worker using nitro.
To create a server route in Nuxt 3:
// server/api/hello.ts
export default () => ({ nuxt: "is easy!" })
// http://localhost:3000/api/hello
{
"nuxt": "is easy!"
}
Also, it has a first-class integration with the runtime application and works out of the box:
// app.vue
<template>
<div> {{ data }} </div>
<template>
<script setup>
const { data } = await useFetch('/api/hello')
</script>
Some of the benefits are:
unjs/h3
created by themselves, which is one of the fastest libraries.unjs/radix3
In opposite to Nuxt 2 where we had to make a round-trip, here we have direct API calls with 0 latency, baseURL is automatically handled, and also fetch is improved with unjs/ohmyfetch
.
In Nuxt 3, server Entry is also integrated into the same bundle compared to nuxt 2. Also, the entire server is isolated, and it's much faster in HMR. The new gotcha is that runtime isolation is more complex.
Nitro comes with something called Pluggable Key-Value storage, which is powered by unjs/unstorage
built by the nuxt team.
The unstorage solution is a unified and powerful Key-Value (KV) interface that allows combining drivers that are either built-in or can be implemented via a super simple interface and adding conventional features like mounting, watching, and working with metadata.
It works in all environments (Browser, NodeJS, and Workers) and offers multiple built-in drivers (Memory, FS, LocalStorage, HTTP, Redis).
Here you can see a snippet of an example of a simple note-taking app:
// server/api/notes/index.post.ts
export default defineEventHandler (async (event) => {
const id = (await useStorage().getKeys( 'data: notes')) . Iength + 1
const body = await useBody(event)
const note = { ...body, id }
await storage.setItem(`data:notes:${id}`, note)
return { id }
])
// server/api/notes/[id].ts
export default defineEventHandler (async (event) =› {
const { id ] = event. context. params
const note = await useStorage().getItem(`data:notes:$(id)`)
if (!note) {
throw createError ({
statusCode: 404,
statusMessage: "Note not found!"
})
return note
})
Deployment in Nuxt 3 is easier. You will fire nuxt build
and the result will be a portable .output
directory with required public files and a tree-shaken version of the node_modules
.
Here are the steps from running the command to receiving the output:
In opposite to nuxt 2, here we have a hybrid solution to have the best of both worlds (server and static), and also generation on demand is now also possible.
I hope you enjoyed this part and that it was as valuable to you as it was to me.
Over the next few days, I’ll share the rest of the talks with you. Stay tuned…
Also published here.