Elysia vs tekir

Both run on Bun, both are TypeScript-first. Here is how they differ in architecture, patterns, and built-in features.

Key differences: Elysia is a lightweight, chain-based HTTP framework focused on raw throughput and type inference. tekir is a full-stack framework with a built-in ORM, DI container, config system, mail, cron, events, and more. Elysia uses TypeBox for validation; tekir uses Zod. Elysia uses plugins; tekir uses providers with lifecycle hooks.

App Creation

Elysia uses new Elysia() with method chaining. tekir uses tekir() which returns a destructurable app object. Both are concise, but tekir includes config, providers, and service injection from the start.

import { Elysia } from 'elysia'

const app = new Elysia()
  .get('/', () => 'Hello World')
  .get('/json', () => ({ message: 'Hello' }))
  .listen(3000)

console.log(`Running at ${app.server?.hostname}:${app.server?.port}`)

Routes

Both frameworks support method-based routing with similar syntax. Elysia chains methods on the app instance. tekir provides both a fluent router API and decorator-based controllers for larger applications.

import { Elysia } from 'elysia'

const app = new Elysia()
  // Method chaining
  .get('/users', () => getUsers())
  .post('/users', ({ body }) => createUser(body))
  .put('/users/:id', ({ params, body }) => updateUser(params.id, body))
  .delete('/users/:id', ({ params }) => deleteUser(params.id))

  // Route groups
  .group('/api/v2', (app) =>
    app
      .get('/users', () => getUsersV2())
      .post('/users', ({ body }) => createUserV2(body))
  )

  // Guards (scoped middleware)
  .guard({ beforeHandle: [isAuthenticated] }, (app) =>
    app
      .get('/profile', ({ store }) => store.user)
      .put('/profile', ({ store, body }) => updateProfile(store.user, body))
  )

  .listen(3000)

Plugins vs Providers

Elysia extends functionality with .use() plugins that chain onto the app instance. tekir uses providers with a full lifecycle: register, boot, and shutdown hooks, backed by a dependency injection container. Providers are more structured and support graceful teardown.

import { Elysia } from 'elysia'
import { swagger } from '@elysiajs/swagger'
import { cors } from '@elysiajs/cors'
import { jwt } from '@elysiajs/jwt'

// Plugins chain onto the Elysia instance
const app = new Elysia()
  .use(swagger())
  .use(cors())
  .use(jwt({ secret: 'my-secret' }))

  // Custom plugin
  .use((app) =>
    app.derive(({ headers }) => ({
      requestId: headers['x-request-id'] ?? crypto.randomUUID()
    }))
  )

  .get('/', ({ requestId }) => ({ id: requestId }))
  .listen(3000)

// Plugin as a separate module
const myPlugin = new Elysia({ name: 'my-plugin' })
  .decorate('logger', console)
  .derive(() => ({ timestamp: Date.now() }))

const app2 = new Elysia()
  .use(myPlugin)
  .listen(3000)

Validation

Elysia uses TypeBox (t.Object, t.String) for schema validation inline with route definitions. tekir uses Zod schemas with a validate middleware. Both provide full type inference from the schema.

import { Elysia, t } from 'elysia'

const app = new Elysia()
  .post('/users', ({ body }) => {
    // body is typed based on the schema
    return createUser(body)
  }, {
    body: t.Object({
      name: t.String({ minLength: 1 }),
      email: t.String({ format: 'email' }),
      age: t.Optional(t.Number({ minimum: 0 }))
    }),
    // Query and params validation
    query: t.Object({
      role: t.Optional(t.String())
    }),
    // Custom error response
    error({ code }) {
      if (code === 'VALIDATION') {
        return { error: 'Invalid request data' }
      }
    }
  })
  .listen(3000)

WebSocket

Both frameworks have built-in WebSocket support on Bun. Elysia uses .ws() with inline handlers. tekir supports both an inline API and structured channel classes with pub/sub built in.

import { Elysia } from 'elysia'

const app = new Elysia()
  .ws('/chat', {
    // Validate incoming messages
    body: t.Object({
      message: t.String()
    }),

    open(ws) {
      console.log('Client connected')
      ws.subscribe('chat')
    },

    message(ws, { message }) {
      // Broadcast to all subscribers
      ws.publish('chat', { from: ws.id, message })
    },

    close(ws) {
      console.log('Client disconnected')
    }
  })
  .listen(3000)

Testing

Elysia testing uses app.handle(new Request(...)) to simulate requests; you construct raw Request objects manually. tekir ships a test client with a fluent assertion API, model factories, and database helpers for test isolation.

import { Elysia } from 'elysia'
import { describe, it, expect } from 'bun:test'

const app = new Elysia()
  .get('/hello', () => 'Hello World')
  .post('/users', ({ body }) => ({ id: 1, ...body }), {
    body: t.Object({ name: t.String() })
  })

describe('API', () => {
  it('should return hello', async () => {
    const response = await app
      .handle(new Request('http://localhost/hello'))
      .then(res => res.text())

    expect(response).toBe('Hello World')
  })

  it('should create user', async () => {
    const response = await app
      .handle(
        new Request('http://localhost/users', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ name: 'Alice' })
        })
      )
      .then(res => res.json())

    expect(response).toEqual({ id: 1, name: 'Alice' })
  })
})