Skip to main content

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

StarterBaseDatabaseAuthDX
bun create elysiaOfficialNoneNone⭐⭐⭐⭐
Custom Elysia + DrizzleCommunityDrizzle + PostgresJWT⭐⭐⭐⭐⭐
Elysia + LuciaCommunityDrizzle/PrismaLucia v3⭐⭐⭐
ElysiaJS fullstackCommunityDrizzleJWT⭐⭐⭐
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.

Comments