Migrating from Express to tekir

A step-by-step guide to migrating your Express application to tekir. Each step shows the Express pattern and its tekir equivalent.

Overview

Express and tekir both let you build HTTP APIs with minimal ceremony. The main differences are that tekir runs on Bun, uses tekir() as a single entry point, and bundles controllers, validation, database access, and a test runner as first-party packages.

You can migrate incrementally. Start by swapping the entry point and routes, then optionally adopt controllers, models, and providers as needed.

1. Replace express() with tekir()

Express creates an app instance with express(). In tekir, tekir() returns the router, config, logger, and server lifecycle functions in one object.

import express from 'express'

const app = express()

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

Express registers routes on the app instance. In tekir, routes are registered on the router returned by tekir(). The biggest difference: tekir handlers return data directly instead of calling res.json().

app.get('/api/users', (req, res) => {
  res.json([{ id: 1, name: 'Alice' }])
})

app.post('/api/users', (req, res) => {
  const user = req.body
  res.status(201).json(user)
})

app.get('/api/users/:id', (req, res) => {
  res.json({ id: req.params.id })
})

3. Replace express.json() with bodyParser()

Express requires you to add express.json() middleware to parse JSON request bodies. tekir parses JSON bodies by default, but you can use the bodyParser() middleware for additional configuration.

import express from 'express'

const app = express()

// Must add middleware manually
app.use(express.json())
app.use(express.urlencoded({ extended: true }))

4. Replace cors() with @tekir/cors

Express uses the cors npm package. In tekir, use @tekir/cors: same concept, pass it as server-level middleware via middleware in tekir options or router.useGlobal() in kernel.

import express from 'express'
import cors from 'cors'

const app = express()

app.use(cors({
  origin: 'http://localhost:5173',
  credentials: true
}))

5. Replace app.listen() with start()

Express starts the server with app.listen(port). In tekir, the port is part of the config and you call start() with no arguments.

const PORT = process.env.PORT || 3000

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`)
})

6. Replace req, res with HttpContext

Express handlers receive separate req and res objects. tekir handlers receive a single HttpContext object that provides params, query, body, headers, and response in one place.

app.get('/api/users/:id', (req, res) => {
  const { id } = req.params
  const { page } = req.query
  const token = req.headers.authorization

  if (!token) {
    return res.status(401).json({ error: 'Unauthorized' })
  }

  res.json({ id, page })
})

app.post('/api/users', (req, res) => {
  const body = req.body
  res.status(201).json(body)
})

7. Move to controllers (optional)

Express typically uses Router instances in separate files to organize routes. tekir offers decorator-based controllers that group related routes into a single class with automatic route registration.

// routes/users.js
import { Router } from 'express'
const router = Router()

router.get('/', async (req, res) => {
  const users = await db.query('SELECT * FROM users')
  res.json(users)
})

router.post('/', async (req, res) => {
  const user = await db.insert('users', req.body)
  res.status(201).json(user)
})

export default router

// app.js
import userRoutes from './routes/users'
app.use('/api/users', userRoutes)

8. Add database (optional)

Express has no built-in database support; you bring your own ORM or query builder. tekir ships with @tekir/db, a database layer with models, migrations, and a query builder.

// No built-in ORM, manually wire up
import knex from 'knex'

const db = knex({
  client: 'sqlite3',
  connection: { filename: './db.sqlite' }
})

app.get('/api/posts', async (req, res) => {
  const posts = await db('posts').select('*')
  res.json(posts)
})

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