Best ElysiaJS + Bun Boilerplates 2026
·StarterPick Team
elysiajsbunbackendapitypescripteden-treaty2026
TL;DR
ElysiaJS doesn't have a rich boilerplate ecosystem yet — the official bun create elysia is the best starting point. The framework is fast (500K req/s on Bun), has unique end-to-end type safety via Eden treaty, and the ecosystem is growing. Most Elysia projects are simple: one entry file, plugins for separation, Drizzle for database. For teams fully committed to Bun: ElysiaJS is compelling. For teams needing Node.js compat: Hono is safer.
Key Takeaways
- Official start:
bun create elysia my-app— generates minimal Elysia app - Eden treaty: type-safe client, no codegen, types from server to client automatically
- Drizzle integration: common choice (Drizzle supports Bun natively)
- Limitation: Bun-only (Node.js support partial, not recommended for production)
- Deployment: Bun runtime required on server (Railway, Fly.io, or custom Docker)
- Performance: ~500K req/s beats Fastify and Hono on Bun
Official Starter
bun create elysia my-api
cd my-api
# Project structure:
# src/
# index.ts
# .env
# package.json
# tsconfig.json
// src/index.ts — Elysia starter:
import { Elysia } from 'elysia';
const app = new Elysia()
.get('/', () => 'Hello Elysia!')
.get('/health', () => ({
status: 'ok',
timestamp: new Date().toISOString(),
}));
app.listen(3000);
console.log(`Server running at ${app.server?.hostname}:${app.server?.port}`);
Production Elysia Stack
bun add elysia @elysiajs/cors @elysiajs/jwt @elysiajs/swagger
bun add drizzle-orm postgres
bun add --dev drizzle-kit @types/bun
Schema-First with Automatic Types
// src/db/schema.ts:
import { pgTable, text, timestamp, boolean } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
email: text('email').notNull().unique(),
name: text('name').notNull(),
plan: text('plan', { enum: ['free', 'pro', 'team'] }).notNull().default('free'),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
export const posts = pgTable('posts', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
title: text('title').notNull(),
content: text('content').notNull(),
published: boolean('published').default(false).notNull(),
authorId: text('author_id').notNull().references(() => users.id),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
// src/db/index.ts:
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
const client = postgres(process.env.DATABASE_URL!);
export const db = drizzle(client);
Building a Full API with Plugins
// src/routes/posts.ts — Elysia plugin:
import { Elysia, t } from 'elysia';
import { db } from '../db';
import { posts } from '../db/schema';
import { eq, desc } from 'drizzle-orm';
export const postsPlugin = new Elysia({ prefix: '/posts' })
.get('/', async () => {
return db.select().from(posts)
.where(eq(posts.published, true))
.orderBy(desc(posts.createdAt))
.limit(20);
})
.get('/:id', async ({ params: { id }, error }) => {
const [post] = await db.select().from(posts).where(eq(posts.id, id));
if (!post) return error(404, { message: 'Post not found' });
return post;
}, {
params: t.Object({ id: t.String() }),
})
.post('/', async ({ body, error }) => {
try {
const [post] = await db.insert(posts).values({
title: body.title,
content: body.content,
authorId: body.authorId,
}).returning();
return post;
} catch (e) {
return error(500, { message: 'Failed to create post' });
}
}, {
body: t.Object({
title: t.String({ minLength: 1, maxLength: 200 }),
content: t.String({ minLength: 1 }),
authorId: t.String(),
}),
});
// src/index.ts — compose plugins:
import { Elysia } from 'elysia';
import { cors } from '@elysiajs/cors';
import { jwt } from '@elysiajs/jwt';
import { swagger } from '@elysiajs/swagger';
import { postsPlugin } from './routes/posts';
import { authPlugin } from './routes/auth';
const app = new Elysia()
.use(cors({ origin: process.env.FRONTEND_URL }))
.use(swagger({ documentation: { info: { title: 'My API', version: '1.0.0' } } }))
.use(jwt({ name: 'jwt', secret: process.env.JWT_SECRET! }))
.use(authPlugin)
.group('/api', (app) => app
.use(postsPlugin)
)
.listen(3000);
// Export for Eden treaty type inference:
export type App = typeof app;
console.log(`Server on http://localhost:${app.server?.port}`);
Eden Treaty: The Killer Feature
// Frontend — zero-config type-safe client:
import { treaty } from '@elysiajs/eden';
import type { App } from '../server/src/index'; // Import server types
const client = treaty<App>('http://localhost:3000');
// Types are inferred from the Elysia schema above:
const { data: posts, error } = await client.api.posts.get();
// posts is: Array<{ id: string; title: string; content: string; ... }>
const { data: newPost } = await client.api.posts.post({
title: 'Hello World',
content: 'My first post',
authorId: currentUser.id,
});
// TypeScript error if body doesn't match Elysia's t.Object schema
// Params:
const { data: post } = await client.api.posts({ id: 'post-123' }).get();
No codegen, no schema files, no separate type declarations — types flow directly from Elysia's route definitions to the client.
Deployment: Bun on Railway
# Dockerfile — minimal Bun production image:
FROM oven/bun:1 as base
WORKDIR /app
FROM base as install
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile --production
FROM base as release
COPY --from=install /app/node_modules node_modules
COPY . .
ENV NODE_ENV=production
EXPOSE 3000
CMD ["bun", "run", "src/index.ts"]
# railway.toml:
[build]
builder = "DOCKERFILE"
[deploy]
startCommand = "bun run src/index.ts"
healthcheckPath = "/health"
Starter Templates Comparison
| Starter | Base | Database | Auth | DX |
|---|---|---|---|---|
bun create elysia | Official | None | None | ⭐⭐⭐⭐ |
| Custom Elysia + Drizzle | Community | Drizzle + Postgres | JWT | ⭐⭐⭐⭐⭐ |
| Elysia + Lucia | Community | Drizzle/Prisma | Lucia v3 | ⭐⭐⭐ |
| ElysiaJS fullstack | Community | Drizzle | JWT | ⭐⭐⭐ |
Choose ElysiaJS + Bun if:
→ Committed to Bun runtime in production
→ Want fastest possible API throughput
→ E2E type safety without tRPC
→ Building a new service (greenfield)
→ Railway/Fly.io deployment (Bun supported)
Choose Hono instead if:
→ Need Node.js compatibility guarantee
→ Deploying to Cloudflare Workers
→ Want larger boilerplate ecosystem
→ Team not ready for Bun edge cases
Choose T3/tRPC if:
→ Next.js full-stack app
→ Need NextAuth integration
→ Largest ecosystem
Find ElysiaJS and Bun boilerplates at StarterPick.