Migrating from AdonisJS to tekir

Both frameworks share similar conventions: controllers, providers, middleware, and a structured project layout. tekir runs on Bun instead of Node.js.

Overview

AdonisJS and tekir share a lot of DNA. Both are TypeScript frameworks with controllers, providers, middleware, and a structured project layout. The main differences: tekir runs on Bun, uses tekir() as a single entry point, uses functional controllers by default (decorators optional), and works with any validation library instead of VineJS.

1. Entry point

AdonisJS uses Ignitor to bootstrap from bin/server.ts. tekir uses tekir() which returns the full app instance.

// AdonisJS v6
// bin/server.ts
import { Ignitor } from '@adonisjs/core'

const app = new Ignitor(import.meta.url)
await app.tap((app) => { import('#start/env') })
  .httpServer()
  .start()

2. Routes

AdonisJS uses lazy controller imports with tuple syntax. tekir imports controllers directly as modules and passes functions to the router.

// AdonisJS v6, start/routes.ts
import router from '@adonisjs/core/services/router'

const UsersController = () => import('#controllers/users_controller')

router.get('/api/users', [UsersController, 'index'])
router.post('/api/users', [UsersController, 'store'])
router.get('/api/users/:id', [UsersController, 'show'])

router.group(() => {
  router.get('/profile', [ProfileController, 'show'])
  router.put('/profile', [ProfileController, 'update'])
}).prefix('/api').use(middleware.auth())

3. Controllers

AdonisJS controllers are classes with request.validateUsing(). tekir controllers are plain exported functions that call any validation library directly.

// AdonisJS v6, app/controllers/users_controller.ts
import type { HttpContext } from '@adonisjs/core/http'
import User from '#models/user'
import { createUserValidator } from '#validators/user'

export default class UsersController {
  async index({ response }: HttpContext) {
    const users = await User.all()
    return response.ok(users)
  }

  async store({ request, response }: HttpContext) {
    const data = await request.validateUsing(createUserValidator)
    const user = await User.create(data)
    return response.created(user)
  }

  async show({ params, response }: HttpContext) {
    const user = await User.findOrFail(params.id)
    return response.ok(user)
  }
}

4. Config & Env

AdonisJS uses defineConfig() wrappers and Env.schema. tekir uses plain objects for config and defineEnv() from @tekir/env for typed environment validation.

// AdonisJS v6, config/app.ts
import env from '#start/env'
import { defineConfig } from '@adonisjs/core/http'

export default defineConfig({
  appKey: env.get('APP_KEY'),
  http: { ... }
})

// AdonisJS v6: start/env.ts
import { Env } from '@adonisjs/core/env'

export default await Env.create(new URL('../', import.meta.url), {
  APP_KEY: Env.schema.string(),
  PORT: Env.schema.number(),
  NODE_ENV: Env.schema.enum(['development', 'production', 'test'])
})

5. Middleware

AdonisJS middleware are classes with a handle method. tekir middleware are plain functions with the same (ctx, next)signature and less boilerplate.

// AdonisJS v6, app/middleware/auth_middleware.ts
import type { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'

export default class AuthMiddleware {
  async handle({ auth, response }: HttpContext, next: NextFn) {
    try {
      await auth.authenticate()
    } catch {
      return response.unauthorized({ error: 'Not authenticated' })
    }
    return next()
  }
}

6. Providers

Both use providers to register services. tekir providers are plain classes with register(app) and no base class.

// AdonisJS v6, providers/app_provider.ts
import type { ApplicationService } from '@adonisjs/core/types'

export default class AppProvider {
  constructor(protected app: ApplicationService) {}

  register() {
    this.app.container.singleton('userService', () => {
      return new UserService()
    })
  }

  async boot() {}
  async start() {}
  async shutdown() {}
}

7. Testing

AdonisJS uses Japa with a clientplugin. tekir uses Bun's built-in test runner with createTestApp(). The assertion style is similar.

// AdonisJS v6, tests/functional/users.spec.ts
import { test } from '@japa/runner'

test.group('Users', () => {
  test('list users', async ({ client }) => {
    const response = await client.get('/api/users')
    response.assertStatus(200)
  })

  test('create user', async ({ client }) => {
    const response = await client.post('/api/users').json({
      name: 'Alice',
      email: '[email protected]'
    })
    response.assertStatus(201)
    response.assertBodyContains({ name: 'Alice' })
  })
})

For more details: