Hono vs ElysiaJS vs Nitro: Lightweight Backend Frameworks 2026
TL;DR
Hono is the most versatile: runs everywhere (Node.js, Bun, Cloudflare Workers, Deno, AWS Lambda) and has crossed 1M weekly downloads. ElysiaJS is the fastest Bun framework with genuinely impressive end-to-end type inference (types flow from server to client automatically). Nitro is the backend runtime used by Nuxt, Vinxi, and Analog — it's the infrastructure layer, not an app framework. For most API projects: Hono. For Bun-only maximum performance: ElysiaJS. For building a meta-framework or SSR adapter: Nitro.
Key Takeaways
- Hono: 1.1M downloads/week, runs everywhere (multi-runtime), tRPC-style RPC optional
- ElysiaJS: 200K downloads/week, Bun-native, Eden treaty for E2E types, fastest throughput
- Nitro: 600K downloads/week, meta-framework runtime (used by Nuxt), H3 under the hood
- Performance: ElysiaJS on Bun is fastest (500K req/s), Hono on Bun second, Hono on Node.js solid
- Edge-first: Hono runs on Cloudflare Workers; ElysiaJS is Bun-only; Nitro supports all adapters
Downloads
| Package | Weekly Downloads | Trend |
|---|---|---|
hono | ~1.1M | ↑ Fast growing |
nitro (via nitropack) | ~600K | ↑ Growing |
elysia | ~200K | ↑ Fast growing |
Hono: Universal Edge Framework
npm install hono
# Hono works with multiple runtimes — same code, different entry point:
# Node.js: @hono/node-server
# Bun: bun run serve (native)
# Cloudflare Workers: wrangler
# AWS Lambda: @hono/aws-lambda
// src/app.ts — framework-agnostic Hono app:
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { jwt } from 'hono/jwt';
import { cors } from 'hono/cors';
import { logger } from 'hono/logger';
import { z } from 'zod';
const app = new Hono();
// Middleware:
app.use('*', cors());
app.use('*', logger());
app.use('/api/*', jwt({ secret: process.env.JWT_SECRET! }));
// Route with Zod validation:
const createPostSchema = z.object({
title: z.string().min(1).max(100),
content: z.string().min(1),
tags: z.array(z.string()).optional(),
});
app.post('/api/posts', zValidator('json', createPostSchema), async (c) => {
const { title, content, tags } = c.req.valid('json');
const user = c.get('jwtPayload'); // From JWT middleware
const post = await db.post.create({
data: { title, content, tags, authorId: user.sub },
});
return c.json(post, 201);
});
app.get('/api/posts/:id', async (c) => {
const id = c.req.param('id');
const post = await db.post.findFirst({ where: { id } });
if (!post) return c.json({ error: 'Not found' }, 404);
return c.json(post);
});
export default app;
// Node.js entry point:
import { serve } from '@hono/node-server';
import app from './app';
serve({ fetch: app.fetch, port: 3000 }, (info) => {
console.log(`Server running at http://localhost:${info.port}`);
});
// Cloudflare Workers entry point (same app!):
// wrangler.toml: name = "my-api", main = "src/worker.ts"
import app from './app';
export default app;
Hono RPC (tRPC-style, native)
// Type-safe client that matches your Hono routes:
import { hc } from 'hono/client';
import type { AppType } from './app';
const client = hc<AppType>('http://localhost:3000');
// Fully typed:
const post = await client.api.posts[':id'].$get({ param: { id: '123' } });
const data = await post.json(); // Typed as your response
ElysiaJS: Bun-Native E2E Types
bun add elysia @elysiajs/eden
// src/app.ts:
import { Elysia, t } from 'elysia';
import { jwt } from '@elysiajs/jwt';
import { cors } from '@elysiajs/cors';
const app = new Elysia()
.use(cors())
.use(jwt({ name: 'jwt', secret: process.env.JWT_SECRET! }))
// Schema-first — types inferred automatically:
.post('/api/posts', async ({ body, jwt: jwtInstance, set }) => {
const user = await jwtInstance.verify(body.token);
if (!user) { set.status = 401; return { error: 'Unauthorized' }; }
const post = await db.post.create({
data: { title: body.title, content: body.content, authorId: user.sub as string },
});
return post;
}, {
// Elysia's type system — validation + type inference:
body: t.Object({
title: t.String({ minLength: 1, maxLength: 100 }),
content: t.String({ minLength: 1 }),
token: t.String(),
}),
response: t.Object({
id: t.String(),
title: t.String(),
content: t.String(),
}),
})
.get('/api/posts/:id', async ({ params }) => {
const post = await db.post.findFirst({ where: { id: params.id } });
return post ?? { error: 'Not found' };
}, {
params: t.Object({ id: t.String() }),
});
export type App = typeof app;
export default app;
// Eden treaty — auto-typed client (no codegen needed):
import { treaty } from '@elysiajs/eden';
import type { App } from './app';
const client = treaty<App>('http://localhost:3000');
// Fully typed — TypeScript knows the shape:
const { data, error } = await client.api.posts.post({
title: 'Hello World',
content: 'My first post',
token: authToken,
});
if (data) console.log(data.id); // TypeScript knows this exists
ElysiaJS Performance (Bun)
HTTP throughput (simple JSON endpoint, Bun runtime):
ElysiaJS + Bun: ~520,000 req/s
Hono + Bun: ~460,000 req/s
Hono + Node.js: ~180,000 req/s
Express + Node: ~45,000 req/s
Fastify + Node: ~120,000 req/s
Nitro: The Meta-Framework Runtime
Nitro is the backend runtime under Nuxt, Vinxi, Analog, and SolidStart. You use it through those frameworks, or directly for custom server builds.
npx giget nitro-app my-app
# Or use as part of Nuxt:
# Nitro handles Nuxt's server routes automatically
// Nuxt server route (Nitro under the hood):
// server/api/posts/[id].ts:
import { defineEventHandler, getRouterParam } from 'h3';
// (h3 = Nitro's HTTP toolkit)
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id');
const post = await db.post.findFirst({ where: { id } });
if (!post) throw createError({ statusCode: 404, message: 'Not found' });
return post;
});
// Nitro standalone (not via Nuxt):
// server/api/posts.ts:
export default defineEventHandler(async (event) => {
const posts = await db.post.findMany({ orderBy: { createdAt: 'desc' } });
return posts;
});
// nitro.config.ts — deploy anywhere:
import { defineNitroConfig } from 'nitropack/config';
export default defineNitroConfig({
preset: 'cloudflare-workers', // or: vercel, aws-lambda, node-server, bun
// Nitro handles: routing, bundling, deployment
// No app framework needed — server routes are files
});
Comparison Table
| Hono | ElysiaJS | Nitro | |
|---|---|---|---|
| Runtimes | Node, Bun, CF, Deno, Lambda | Bun (Node partial) | All (via presets) |
| E2E types | Via Hono RPC | ✅ Eden treaty | H3 types |
| Performance | High | Highest (Bun) | Varies by preset |
| Edge-ready | ✅ | ❌ | ✅ |
| Middleware | ✅ Rich | ✅ Plugins | ✅ H3 hooks |
| File routing | ❌ | ❌ | ✅ (auto from files) |
| Validation | Zod/Valibot | Built-in (TypeBox) | Manual |
| Framework vs runtime | Framework | Framework | Runtime/infrastructure |
| Use as standalone | ✅ | ✅ | ✅ |
Decision Guide
Choose Hono if:
→ Multi-runtime needed (Node.js + Cloudflare Workers)
→ Building a REST API or edge API
→ Coming from Express (easy migration)
→ Want built-in middleware (JWT, CORS, cache, logger)
→ Production-proven, large community
Choose ElysiaJS if:
→ Bun is your runtime (committed)
→ End-to-end type safety without separate schema (Eden treaty)
→ Maximum throughput is priority
→ Building a full-stack Bun app
Choose Nitro if:
→ Building a meta-framework or SSR app
→ Using Nuxt (it's built-in)
→ Need universal deployment presets
→ File-based server routes (like Next.js but backend-only)
Compare Hono, ElysiaJS, Fastify, and Express on PkgPulse.
See the live comparison
View hono vs. express on PkgPulse →