Glass Box Terrarium, explained from the inside

Last updated · Latest source on GitHub ↗

Nuxt & VueLesson

Nitro server engine

Vue draws a page, and Nuxt decides which page an address means. Something still has to take each request, decide whether a stored copy of the page will do, and package everything into one server that runs on its own. In a Nuxt app, that is Nitro.

Know Nitro already? Skip to In Terrarium.

Outside the box · General

The concept

Nitro is a toolkit for web servers: it builds request handlers and settings into one server that runs on Node, on a serverless platform or elsewhere. Nuxt's server is Nitro. Nuxt registers its page renderer as Nitro's catch-all handler, /**, so every request that no other handler takes becomes a page render.

A request is split three ways. Nitro takes it, applies its route's rules, answers from its cache when it can, and otherwise calls a handler. Nuxt's renderer, that handler, finds the page file for the address, runs its data loading and builds the payload. Vue renders the components to HTML on the server, and hydrates them in the browser.

routeRules in nuxt.config.ts maps path patterns to options, and Nuxt hands them to Nitro. A route with a cache rule has its handler wrapped in a cache:

  • maxAge is how many seconds it keeps each response.
  • swr (on by default) keeps serving the expired copy while a new one renders in the background; with swr: false the next request waits for a fresh copy.
  • varies lists the request headers a cached render still sees; Nitro drops every other one.
  • cache: false turns the cache off for a route.

Cached responses go to the cache: mount of Nitro's storage: in memory in a production build by default, in files under the build folder in development. A config can mount another storage driver there.

nuxt build runs Nitro's build, which writes the whole server to .output/. With the default Node server preset, .output/server/index.mjs starts it, and the folder needs nothing else installed. Nuxt 4 runs Nitro v2, documented at v2.nitro.build; nitro.build documents v3.

Full referenceNitro v2 docs

Looking inside

In Terrarium

Terrarium has no server/ folder and no handlers of its own. Nitro runs Nuxt's renderer and the routes modules add, such as Nuxt Content's. What Terrarium writes is configuration. The Platform's config caches the home page and every Tenant page, and changes the cache's store:

nuxt.config.ts ↗20–25as of
20  routeRules: {
21    '/': { cache: { maxAge: 60, swr: false } },
22    '/t/**': { cache: { maxAge: 60, swr: false } },
23  },
24  // Bounds that page cache's memory (ADR-0028, #1446).
25  nitro: { storage: { cache: { driver: 'lru-cache', max: 200 } } },

A page stays cached for maxAge seconds and is never served after that. The cache: mount uses an LRU driver: past max entries it drops the one used least recently. tests/unit/page-cache.spec.ts loads that driver with the real config's options and fills it past the cap. Each cached route also gets a payload (Data fetching).

A Tenant adds rules for its own pages in its layer's config, and Nuxt merges them with the Platform's. Tinkerfund lists a header in varies and turns the cache off for its search:

layers/tinkerfund/nuxt.config.ts ↗10–14as of
10  // Page cache (ADR-0028): money and dates follow the visitor's locale (#1365); search stays in the browser.
11  routeRules: {
12    '/t/tinkerfund/**': { cache: { varies: ['accept-language'] } },
13    '/t/tinkerfund/*/search': { cache: false },
14  },

pnpm build runs nuxt build, and Nitro writes .output/. .output/server/ holds the server with only the packages its code imports; .output/public/ holds the files the server sends as they are, such as the browser's JavaScript in _nuxt/ and, in __nuxt_content/, the Collection dumps a browser query downloads. How the live container builds and starts that folder is in Continuous deployment.

Nuxt Content runs inside that server. Its queries during a render use the server's SQLite database, and the routes that send dumps to the browser are Nitro handlers the module adds (Content database).

Why it's built this way

  • Cache to get payloads. Nuxt serves payloads only for prerendered or cached routes, and Terrarium wants them (Data fetching says why). The cost: one copy serves every visitor, so a page reading a request header must list it in varies; Tinkerfund's pages once showed everyone English until it did. ADR-0028 (docs/adr/0028-navigation-reads-server-payloads.md).
  • No stale copies. The cache is short and swr is off. swr (stale-while-revalidate) would serve the first visitor after a quiet spell a copy of any age, and some pages work out "today" while rendering. The cost: the first request after a copy expires waits for a fresh render. ADR-0028 (docs/adr/0028-navigation-reads-server-payloads.md).
  • Capped cache store. Nitro keeps one copy per address, query string included, and its default in-memory store never drops expired copies, so made-up query strings could grow memory until restart. A store capped at a fixed number of copies, dropping the least recently used, fixes that in one config line. The cost: a dropped copy needs one extra render. ADR-0028 (docs/adr/0028-navigation-reads-server-payloads.md).

What to watch for

  • Check a header-reading page from two browsers. A page that renders from a request header or cookie looks right when you test it alone. Open it from a second browser with another language or cookie before you trust its varies; ADR-0028 (docs/adr/0028-navigation-reads-server-payloads.md) has the rule.
  • pnpm dev keeps its cache on disk. In development the cache: mount is files under .nuxt/cache/nitro/routes, not the LRU store, and they outlast a restart of pnpm dev. If an edited page comes back unchanged, delete that folder.

Try it in your project

Add a page that prints when it was rendered, and cache it for half a minute:

// nuxt.config.ts
export default defineNuxtConfig({
  routeRules: { '/now': { cache: { maxAge: 30, swr: false } } },
})
<!-- app/pages/now.vue -->
<script setup lang="ts">
const renderedAt = useState('renderedAt', () => new Date().toISOString())
</script>

<template><p>Rendered at {{ renderedAt }}</p></template>

Run nuxt build, list .output/, and start node .output/server/index.mjs. Reload /now a few times: the time stays the same until 30 seconds have passed.

Where next

Walkthroughs through this Lesson

Last updated · Latest source on GitHub ↗

Something went wrong

This content couldn’t be loaded. Reloading the page usually fixes it.

Technical details
route: /t/glassbox/guide/nuxt-vue/nitro