Migrating from Elysia to tekir

A step-by-step guide to migrating your Elysia application to tekir. Both frameworks run on Bun, so the transition is straightforward.

Overview

Elysia and tekir are both Bun-native frameworks that share a similar handler shape: destructured context parameters, return values sent as responses, and TypeScript-first inference on params, query, and body. The key differences are that tekir uses tekir() instead of method chaining, and adds built-in support for database, DI container, caching, mail, file storage, and structured testing.

1. Replace new Elysia() with tekir()

Elysia creates an app via new Elysia(). In tekir, tekir() returns a destructured object with the router, config, logger, services, and lifecycle hooks.

import { Elysia } from 'elysia'

const app = new Elysia()

2. Replace .get/.post with router.get/post

Elysia uses method chaining on the app instance. tekir uses the router object returned from tekir(). The handler signature is nearly identical, both destructure context and return values directly.

const app = new Elysia()
  .get('/api/users', () => {
    return [{ id: 1, name: 'Alice' }]
  })
  .post('/api/users', ({ body }) => {
    return body
  })
  .get('/api/users/:id', ({ params: { id } }) => {
    return { id }
  })

3. Replace plugins with providers + middleware

Elysia uses .use() for plugins like Swagger and CORS. In tekir, CORS is a middleware (@tekir/cors), services like database use the provider pattern, and utilities like Swagger are standalone functions.

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

const app = new Elysia()
  .use(swagger())
  .use(cors())

4. Replace t.Object() with Zod

Elysia has its own validation system based on TypeBox (t.Object, t.String, etc.). tekir works with any validation library; Zod is the most common choice. Call .parse(body) directly in your handler. If validation fails, Zod throws and tekir returns a 422 automatically.

import { Elysia, t } from 'elysia'

const app = new Elysia()
  .post('/api/users', ({ body }) => {
    return body
  }, {
    body: t.Object({
      name: t.String({ minLength: 3 }),
      email: t.String({ format: 'email' }),
      age: t.Optional(t.Number({ minimum: 0 }))
    })
  })

5. Replace .listen() with start()

Elysia chains .listen(port) at the end of the builder. In tekir, the port is part of the config and you call start() separately.

const app = new Elysia()
  .get('/', () => 'Hello')
  .listen(3000)

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

6. Replace app.handle() with createTestApp()

Elysia tests use app.handle(new Request(...)) to send requests directly. tekir provides createTestApp() from @tekir/testing which boots the app in test mode and returns a request client with built-in assertion helpers.

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

describe('Users API', () => {
  const app = new Elysia()
    .get('/api/users', () => [{ id: 1, name: 'Alice' }])

  it('should list users', async () => {
    const response = await app
      .handle(new Request('http://localhost/api/users'))
      .then(res => res.json())

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

That covers the core migration path. For more details on each feature, see the relevant documentation: