Directory Structure
A tour of every folder and file in a freshly scaffolded tekir project.
Overview
When you run bunx create-tekir-app my-app, tekir generates a project with a clear, opinionated layout. Every directory has a single responsibility, so you always know where a file belongs.
project/
├── index.ts # Entry point: tekir()
├── services.ts # Typed service accessors
├── types.ts # Module augmentation
├── env.ts # Environment validation
├── config/
│ ├── app.ts # Application config (port, env, etc.)
│ ├── database.ts # Database connection & schema
│ ├── auth.ts # Authentication strategies
│ ├── cache.ts # Cache driver config
│ ├── cors.ts # CORS settings
│ └── ... # Other package configs
├── start/ # Lifecycle files (loaded when tekir({ startDir: 'start' }))
│ ├── kernel.ts # Providers, middleware
│ ├── boot.ts # DB setup, cron registration
│ ├── events.ts # Event listeners
│ └── routes.ts # Route/controller registration
├── core/ # Application code
│ ├── controllers/ # HTTP controllers (class-based)
│ ├── models/ # Database models (BaseModel subclasses)
│ ├── middleware/ # Custom middleware functions
│ ├── listeners/ # Event listener classes
│ ├── schedules/ # Cron job schedules
│ └── jobs/ # Background job classes
├── resources/ # Frontend (Vite/Bun)
├── pages/ # Frontend (Next.js)
├── database/
│ ├── migrations/ # Schema migration files
│ └── seeders/ # Database seed scripts
└── tests/ # Test filesDo not be overwhelmed. You only touch the directories that apply to your project. A simple API might only need core/controllers/ and start/routes.ts.
tekir does not scan any of these folders automatically. Your index.ts tells tekir() exactly where to look by passing envFile, configDir, and startDir. You can rename, relocate, or skip any of them; whatever you do not pass simply does not get loaded.
Inline API (Single File)
For small apps, microservices, or prototyping, put everything in one file. No directories needed:
project/
├── index.ts # Everything: tekir + routes + start
├── .env # Environment variables
├── package.json
├── tsconfig.json
└── resources/ # Frontend (optional)
├── index.html
├── main.tsx
└── App.tsxAll config, providers, routes, and startup happen inline:
import { tekir } from '@tekir/core'
import { DatabaseProvider } from '@tekir/db'
import type { Database } from '@tekir/db'
const { router, service, start } = await tekir({
config: { app: { port: 3000 }, database: { ... } },
providers: [DatabaseProvider],
frontend: { type: 'bun' }
})
const db = service<Database>('db')
router.get('/api/users', async () => await db.query('SELECT * FROM users'))
start()As your app grows, extract into the structured layout below.
core/
The core/ directory is where your application logic lives. All sub-directories follow the same rule: one class or function per file, named after what it does.
core/controllers/
Controllers are classes decorated with @Controller from @tekir/http-decorators. Each method handles one HTTP route and receives an HttpContext argument. File names use snake_case and end with _controller.ts.
import { Controller, Get, Post, Delete } from '@tekir/http-decorators'
import type { HttpContext } from '@tekir/core'
@Controller('/users')
export class UserController {
@Get('/')
index({ response }: HttpContext) {
return response.ok([])
}
@Get('/:id')
show({ params, response }: HttpContext) {
return response.ok({ id: params.id })
}
@Post('/')
store({ body, response }: HttpContext) {
return response.created(body)
}
@Delete('/:id')
destroy({ response }: HttpContext) {
return response.noContent()
}
}core/middleware/
Middleware are plain async functions with the signature (ctx: HttpContext, next: () => Promise<void>) => Promise<void>, and run before (or after, if code follows await next()) your route handler. File names use snake_case.
import type { HttpContext } from '@tekir/core'
export default async function authGuard(ctx: HttpContext, next: () => Promise<void>) {
const token = ctx.request.header('authorization')
if (!token) return ctx.response.unauthorized({ message: 'No token provided' })
// verify token...
await next()
}core/models/
Models extend BaseModel from @tekir/db. They map to a database table and expose static query methods like find, all, create, and destroy.
import { BaseModel, column } from '@tekir/db'
export class User extends BaseModel {
static table = 'users'
static schema = {
id: column.id(),
name: column.string(),
email: column.string(),
createdAt: column.dateTime({ autoCreate: true })
}
}core/listeners/
Event listener classes that respond to application events. Register them in start/events.ts.
core/jobs/ & core/schedules/
Background job classes and cron schedule definitions. Jobs are dispatched asynchronously, while schedules are registered in start/boot.ts via the Cron service.
config/
Every tekir package that needs configuration has a corresponding file here. Each file is a plain TypeScript module that exports a typed object. Access config values via the config function provided by TekirApp.
import env from '~/env'
export default {
name: 'My App',
port: env.PORT ?? 3000,
host: env.HOST ?? '0.0.0.0',
debug: env.NODE_ENV === 'development'
}You can add your own config files and read them with config('my-key') anywhere in the application.
config/app.ts: port, hostname, debug modeconfig/database.ts: DB driver, connection string, Drizzle schemaconfig/auth.ts: JWT secret, token expiry, OAuth providersconfig/cache.ts: cache driver (memory, Redis)config/cors.ts: allowed origins, headers, methods
start/
The start/ directory is the bootstrap layer. Its files are auto-loaded at startup before the server starts listening. Each file exports a default function that receives the TekirApp instance. The load order is: kernel first, boot second, then remaining files alphabetically, and routes last.
start/kernel.ts
kernel.ts is where you register service providers (which boot packages like the database and cache), global middleware (CORS, session),router middleware (logging, auth), and named middleware that controllers can reference by key.
import type { TekirApp } from '@tekir/core'
import { serverTiming } from '@tekir/core'
import { cors } from '@tekir/cors'
import { DatabaseProvider } from '@tekir/db'
import { CacheProvider } from '@tekir/cache'
import { AuthProvider } from '@tekir/auth'
import { HashProvider } from '@tekir/hash'
import { bodyParser } from '@tekir/bodyparser'
import { authenticate, silentAuth, guest } from '@tekir/auth'
import requestLogger from '~/middleware/request_logger'
export default function ({ app, router, config }: TekirApp) {
app.registerAll([
DatabaseProvider,
CacheProvider,
AuthProvider,
HashProvider
])
router.useGlobal([cors(config('cors'))])
router.useRouter([bodyParser(), serverTiming(), requestLogger])
}start/boot.ts
boot.ts runs after kernel and handles database setup, cron job registration, and other initialization that depends on services being available. Access services via service<T>('name').
import type { TekirApp } from '@tekir/core'
export default function({ service }: TekirApp) {
// Database setup
const db = service<Database>('db')
await db.exec('CREATE TABLE IF NOT EXISTS ...')
// Register cron jobs
const cron = service<Cron>('cron')
cron.add('cleanup', '0 0 * * *', async () => { /* ... */ })
}start/events.ts
events.ts registers event listeners for application events:
import type { TekirApp } from '@tekir/core'
import { emitter } from '#services'
import { UserEvents } from '~/listeners/user_events'
import { TaskEvents } from '~/listeners/task_events'
export default function (_tekir: TekirApp) {
// Register @Listener() decorated classes
emitter.register(UserEvents, TaskEvents)
// Or inline listeners
emitter.on('order.completed', (data) => console.log(data))
}start/routes.ts
routes.ts is loaded last and is where you register all your controllers with router.register(), or define standalone routes with router.get(), router.post(), etc.
import type { TekirApp } from '@tekir/core'
import { UserController } from '#controllers/user_controller'
import { PostController } from '#controllers/post_controller'
export default function({ router }: TekirApp) {
// Register decorator-based controllers
router.register(UserController, PostController)
// Or add standalone routes
router.get('/health', () => ({ status: 'ok' }))
}database/
The database/ directory holds migration files and seed scripts. Migrations are TypeScript classes that extend BaseMigration and define up() and down() methods. Seeders are TypeScript files that export a run() function.
import { BaseMigration, type Schema } from '@tekir/db'
export default class CreateUsers extends BaseMigration {
async up(schema: Schema) {
schema.createTable('users', (table) => {
table.id()
table.string('name').notNullable()
table.string('email').notNullable().unique()
table.string('password').notNullable()
table.timestamps()
})
}
async down(schema: Schema) {
schema.dropTable('users')
}
}import { User } from '#models/user'
export async function run() {
if ((await User.count()) > 0) return
await User.createMany([
{ name: 'Alice', email: '[email protected]', password: 'hashed' },
{ name: 'Bob', email: '[email protected]', password: 'hashed' }
])
}Run migrations with tekir migrate and seeders with tekir seed.
Frontend (resources/ or pages/)
tekir supports integrated frontend via the frontend config option. Set frontend: { type: 'vite' }, frontend: { type: 'next' }, or frontend: { type: 'bun' } in your app config.
- Vite / Bun: frontend assets live in
resources/. - Next.js: pages live in
pages/following the Next.js conventions.
Root Files
- index.ts: the application entry point. It calls
tekir()which returns aTekirAppwithapp,server,router,logger,config,service,start,onStart, andonShutdown. - services.ts: typed service accessor functions using
service<T>('name'). Provides convenient access to framework services likeDatabase,Auth,Hash,Drive,Mail,Notification,Cron,Health, andEncryption. - types.ts: module augmentation for extending framework types.
- .env: local environment variables. Never commit this file.
- env.ts: validates and types the values from
.envusing@tekir/env. Import from here instead of readingprocess.envdirectly.
import { tekir } from '@tekir/core'
const app = await tekir({
envFile: 'env.ts',
configDir: 'config',
startDir: 'start',
})
app.start()import { service } from '@tekir/core'
import type { Database } from '@tekir/db'
import type { Auth } from '@tekir/auth'
export const db = () => service<Database>('database')
export const auth = () => service<Auth>('auth')