Litro vs Nuxt.js
The difference between Litro and Nuxt is the component model. Nuxt uses Vue Single File Components; Litro uses Lit web components. Everything under the server layer — Nitro, H3, deployment presets — is shared.
Feature Comparison
| Feature | Nuxt.js | Litro |
|---|---|---|
| Component model | Vue 3 (SFC) | Lit (web components) |
| File-based routing | ✓ | ✓ |
| SSR | Vue SSR | Declarative Shadow DOM streaming |
| SSG | ✓ | ✓ |
| Data fetching | useFetch / useAsyncData | definePageData() |
| API routes | server/api/*.ts (H3) | Identical ↔ |
| Server engine | Nitro | Identical ↔ |
| Deployment adapters | All Nitro presets | Identical ↔ |
| Client routing | vue-router | LitroRouter (URLPattern) |
| Hello World JS (gzipped) | ~60 kB | ~8 kB |
| Virtual DOM | ✓ (Vue) | — |
| W3C standard components | — | ✓ |
| TypeScript | ✓ | ✓ |
Side by Side: A Page Component
Nuxt — pages/about.vue
<script setup lang="ts">
const title = 'About Us';
const items = ['SSR', 'SSG', 'Nitro'];
</script>
<template>
<main>
<h1>{{ title }}</h1>
<ul>
<li v-for="item in items">
{{ item }}
</li>
</ul>
</main>
</template>
Litro — pages/about.ts
import { html } from 'lit';
import { customElement } from 'lit/decorators.js';
import { LitroPage } from '@beatzball/litro/runtime';
const items = ['SSR', 'SSG', 'Nitro'];
@customElement('page-about')
export class AboutPage extends LitroPage {
override render() {
return html`
<main>
<h1>About Us</h1>
<ul>
${items.map(i => html`<li>${i}</li>`)}
</ul>
</main>
`;
}
}
export default AboutPage;
Side by Side: Data Fetching
Nuxt — pages/blog/[slug].vue
<script setup lang="ts">
const route = useRoute();
const { data: post } = await useFetch(
`/api/posts/${route.params.slug}`
);
</script>
<template>
<article>
<h1>{{ post?.title }}</h1>
<div v-html="post?.body"></div>
</article>
</template>
Litro — pages/blog/[slug].ts
export const pageData = definePageData(
async (event) => {
const slug = event.context.params?.slug;
const post = await $fetch(
`/api/posts/${slug}`
);
return { post };
}
);
@customElement('page-blog-slug')
export class BlogPost extends LitroPage {
override render() {
const { post } = this.serverData ?? {};
if (!post) return html`<p>Loading…</p>`;
return html`
<article>
<h1>${post.title}</h1>
${unsafeHTML(post.body)}
</article>
`;
}
}
API Routes Are Identical
H3 API route handlers are copy-paste compatible. The only difference:
Nuxt auto-imports defineEventHandler; Litro requires an explicit import.
Nuxt — server/api/posts.ts
// defineEventHandler is auto-imported
export default defineEventHandler(async () => {
return await db.getPosts();
});
Litro — server/api/posts.ts
import { defineEventHandler } from 'h3';
export default defineEventHandler(async () => {
return await db.getPosts();
});
What Carries Over
- H3 API routes — copy as-is, add the explicit
h3import - Server middleware —
server/middleware/*.tsis identical - Deployment targets — all Nitro presets work identically: Vercel, Cloudflare Workers, AWS Lambda, Deno Deploy, static, and more
- Route rules —
routeRulesinnitro.config.tsis the same API - File-based routing convention —
[slug],[...all]work the same
What Changes
- Vue SFCs → Lit class components —
<script setup>becomesdefinePageData();<template>becomesrender() - No Vue runtime — bundle drops from ~60 kB to ~8 kB
- vue-router → LitroRouter — different API, built on URLPattern
<NuxtLink>→<litro-link>— SPA navigation; attribute ishrefnotto
Why Build on Web Standards?
Litro's component model is built on Custom Elements, Shadow DOM, and <slot> — W3C
specifications that browsers implement natively and interoperate with any framework or
plain HTML. Because the server layer is already shared (Nitro, H3, deployment presets),
moving from Nuxt to Litro means you keep your server investment entirely and adopt
a component model built directly on the web platform.
For new projects, building on web standards means your component layer stays stable as the broader ecosystem evolves — no framework version to upgrade, no breaking changes to absorb between major releases.
Litro