Elysia vs tekir
Both run on Bun, both are TypeScript-first. Here is how they differ in architecture, patterns, and built-in features.
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' })
})
})