Quick Start
Build your first route, controller, and model, then make a real HTTP request. About 10 minutes.
Scaffold with Templates
The fastest way to start is with the CLI. Pick one of the 5 built-in templates:
# Interactive, prompts you to choose a template
bunx create-tekir-app my-app
# Or pass --template directly
bunx create-tekir-app my-app --template=minimal # Single-file TODO API
bunx create-tekir-app my-app --template=api # Full API with auth, ORM, mail
bunx create-tekir-app my-app --template=fullstack # API + React frontend
bunx create-tekir-app my-app --template=with-vite # tekir + Vite (React/Vue/Svelte)
bunx create-tekir-app my-app --template=with-next # tekir + Next.js SSREach template comes fully configured with TypeScript, hot reload, and testing. See the Installation guide for details on each template.
Minimal Single-File App
The fastest way to get started is a single-file app. tekir() is the entry point for every tekir application. It returns a TekirApp object with everything you need: app, server, router, logger, config, service, start, onStart, and onShutdown.
import { tekir } from '@tekir/core'
const { router, start } = await tekir()
router.get('/', () => ({ message: 'Hello from tekir!' }))
start()Run it with tekir serve and visit http://localhost:3000.
Inline API (Single File)
For real apps, tekir()accepts inline config, providers, middleware, and frontend integration in one file. Here's a complete TODO API with database, Swagger docs, and CRUD:
import { tekir } from '@tekir/core'
import { DatabaseProvider } from '@tekir/db'
import { swagger } from '@tekir/swagger'
import type { Database } from '@tekir/db'
const { router, service, start } = await tekir({
config: {
app: { name: 'Todo API', port: 3000, env: 'development' },
database: {
default: 'sqlite',
connections: { sqlite: { driver: 'sqlite', connection: { path: ':memory:' } } }
}
},
providers: [DatabaseProvider]
})
const db = service<Database>('db')
await db.exec(`CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
done INTEGER DEFAULT 0
)`)
router.get('/api/todos', async () => await db.query('SELECT * FROM todos'))
router.post('/api/todos', async ({ body }) => {
await db.run('INSERT INTO todos (title) VALUES (?)', [body.title])
return await db.queryOne('SELECT * FROM todos ORDER BY id DESC LIMIT 1')
})
router.put('/api/todos/:id', async ({ body, params }) => {
await db.run('UPDATE todos SET done = ? WHERE id = ?', [body.done ? 1 : 0, params.id])
return await db.queryOne('SELECT * FROM todos WHERE id = ?', [params.id])
})
router.delete('/api/todos/:id', async ({ params }) => {
await db.run('DELETE FROM todos WHERE id = ?', [params.id])
return { deleted: true }
})
swagger(router, { title: 'Todo API', version: '1.0.0', path: '/docs' })
start()This gives you a full API server with SQLite, auto-generated Swagger docs at /docs, and typed database access, all in one file. No start/ directory, no config files needed.
Frontend Integration
tekir supports three frontend types via the frontend option. All serve on the same port: /api/* goes to tekir, everything else goes to the frontend.
Bun Native (recommended for new projects)
Zero-config React/TS/CSS bundling with HMR. Can compile to a single executable. Put your HTML in resources/:
import { tekir } from '@tekir/core'
import { DatabaseProvider } from '@tekir/db'
import type { Database } from '@tekir/db'
const { router, service, start } = await tekir({
config: {
app: { name: 'Fullstack App', port: 3000 },
database: { default: 'sqlite', connections: { sqlite: { driver: 'sqlite', connection: { path: ':memory:' } } } }
},
providers: [DatabaseProvider],
frontend: { type: 'bun' } // Bun native, HMR, single executable
})
// API routes
const db = service<Database>('db')
router.get('/api/todos', async () => await db.query('SELECT * FROM todos'))
start()Vite (React, Vue, Svelte, etc.)
Use Vite's plugin ecosystem. Requires @tekir/vite package. Frontend lives in resources/:
import { tekir } from '@tekir/core'
import { DatabaseProvider } from '@tekir/db'
const { router, service, start } = await tekir({
config: { app: { name: 'Vite App', port: 3000 } },
providers: [DatabaseProvider],
frontend: { type: 'vite' } // Vite + React/Vue/Svelte
})
// API routes: /api/* goes to tekir, rest goes to Vite
router.get('/api/health', () => ({ status: 'ok' }))
start()Next.js (SSR)
Server-side rendering with Next.js pages router. Requires @tekir/next package. Pages live in pages/:
import { tekir } from '@tekir/core'
import { DatabaseProvider } from '@tekir/db'
const { router, service, start } = await tekir({
config: { app: { name: 'Next App', port: 3000 } },
providers: [DatabaseProvider],
frontend: { type: 'next' } // Next.js SSR (pages router)
})
// API routes: /api/* goes to tekir, rest goes to Next.js
router.get('/api/health', () => ({ status: 'ok' }))
start()Your First Route
In a structured project, routes live in the start/ directory. Each file in start/ exports a default function that receives the TekirApp instance. The directory auto-loads all files: kernel first, boot second, then alphabetical order, and routes last.
import { tekir } from '@tekir/core'
const app = await tekir({
envFile: 'env.ts',
configDir: 'config',
startDir: 'start',
})
app.start()You can use the full HttpContext to access request data and build structured responses:
import type { TekirApp } from '@tekir/core'
import type { HttpContext } from '@tekir/core'
export default function({ router }: TekirApp) {
router.get('/', ({ response }: HttpContext) => {
return response.ok({ message: 'Hello from tekir!' })
})
router.get('/users/:id', ({ params, response }: HttpContext) => {
return response.ok({ id: params.id })
})
router.post('/echo', ({ body, response }: HttpContext) => {
return response.created(body)
})
}For anything beyond simple utility routes, controllers are the right tool.
Creating a Controller
Controllers are TypeScript classes decorated with @Controller from @tekir/http-decorators. Each method is decorated with an HTTP method decorator: @Get, @Post, @Put, @Patch, or @Delete.
Create core/controllers/post_controller.ts:
import { Controller, Get, Post, Put, Delete } from '@tekir/http-decorators'
import type { HttpContext } from '@tekir/core'
import { Post } from '#models/post'
@Controller('/api/posts')
export class PostController {
// GET /api/posts
@Get('/')
async index({ response }: HttpContext) {
const posts = await Post.all()
return response.ok(posts.map(p => p.toJSON()))
}
// GET /api/posts/:id
@Get('/:id', { where: { id: { match: /^\d+$/, cast: Number } } })
async show({ params, response }: HttpContext) {
const post = await Post.find(Number(params.id))
if (!post) return response.notFound({ message: 'Post not found' })
return response.ok(post.toJSON())
}
// POST /api/posts
@Post('/')
async store({ body, response }: HttpContext) {
const post = await Post.create(body)
return response.created(post.toJSON())
}
// PUT /api/posts/:id
@Put('/:id')
async update({ params, body, response }: HttpContext) {
const post = await Post.find(Number(params.id))
if (!post) return response.notFound({ message: 'Post not found' })
await post.merge(body).save()
return response.ok(post.toJSON())
}
// DELETE /api/posts/:id
@Delete('/:id')
async destroy({ params, response }: HttpContext) {
const post = await Post.find(Number(params.id))
if (!post) return response.notFound({ message: 'Post not found' })
await post.delete()
return response.noContent()
}
}Register the controller in start/routes.ts:
import type { TekirApp } from '@tekir/core'
import { PostController } from '#controllers/post_controller'
export default function({ router }: TekirApp) {
router.register(PostController)
}tekir will automatically map the controller methods to the correct HTTP routes based on the decorators. The @Controller('/api/posts') decorator sets the route prefix for all methods in the class.
Creating a Model
tekir's ORM (@tekir/db) uses the ActiveRecord pattern. Models extend BaseModel and define their schema, fillable fields, and relationships declaratively.
The Model
import { BaseModel, column, hasMany, belongsTo, type Relation } from '@tekir/db'
import { User } from './user'
import { Comment } from './comment'
export class Post extends BaseModel {
static table = 'posts'
static schema = {
id: column.id(),
title: column.string(),
body: column.text(),
published: column.boolean({ default: false }),
userId: column.integer(),
createdAt: column.dateTime({ autoCreate: true }),
updatedAt: column.dateTime({ autoCreate: true, autoUpdate: true, nullable: true })
}
static fillable = ['title', 'body', 'published', 'userId']
static relations: Record<string, Relation> = {
author: belongsTo(() => User, { foreignKey: 'userId' }),
comments: hasMany(() => Comment)
}
}
// Usage
const post = await Post.create({ title: 'Hello', body: 'World', userId: 1 })
const posts = await Post.query().where('published', true).orderBy('createdAt', 'desc').limit(10)
const withAuthor = await Post.query().preload('author').first()The Migration
Migrations define the database schema. Create one for the posts table:
import { BaseMigration, type Schema } from '@tekir/db'
export default class CreatePosts extends BaseMigration {
async up(schema: Schema) {
schema.createTable('posts', (table) => {
table.id()
table.string('title').notNullable()
table.text('body').notNullable()
table.boolean('published').defaultTo(false)
table.integer('user_id').references('users', 'id').onDelete('CASCADE')
table.timestamps()
})
}
async down(schema: Schema) {
schema.dropTable('posts')
}
}Run the migration:
tekir migrateAdding Validation
Use @tekir/validator with @Middleware to validate request bodies. tekir integrates with Zod for schema definition:
import { Controller, Post, Middleware } from '@tekir/http-decorators'
import type { HttpContext } from '@tekir/core'
import { validate } from '@tekir/validator'
import { z } from 'zod'
import { Post as PostModel } from '#models/post'
const createPostSchema = z.object({
title: z.string().min(3).max(255),
body: z.string().min(10),
published: z.boolean().optional().default(false)
})
@Controller('/api/posts')
export class PostController {
@Post('/')
@Middleware([validate({ body: createPostSchema })])
async store({ body, response }: HttpContext) {
// body is fully typed as z.infer<typeof createPostSchema>
const post = await PostModel.create(body)
return response.created(post.toJSON())
}
}The validate middleware validates the request body against the Zod schema and automatically returns a 422 Unprocessable Entity response with structured error messages if validation fails. If validation passes, body in the context is fully typed.
Making a Request
With the dev server running, let's test the API with curl:
Create a post
curl -X POST http://localhost:3000/api/posts \
-H "Content-Type: application/json" \
-d '{"title":"My first post","body":"Hello from tekir!","published":true}'{
"id": 1,
"title": "My first post",
"body": "Hello from tekir!",
"published": true,
"userId": null,
"createdAt": "2026-03-24T10:00:00.000Z",
"updatedAt": null
}List all posts
curl http://localhost:3000/api/postsValidation errors
If you send an invalid body, tekir returns a structured error response:
curl -X POST http://localhost:3000/api/posts \
-H "Content-Type: application/json" \
-d '{"title":"Hi"}'Build & Deploy
Every tekir app uses the same three commands. The entry point handles everything based on your config:
# Development
tekir serve
# Build for production
tekir build
# Start production server
tekir serve
# Single executable (CLI flag)
tekir build --compile # → ./server binary
./server # runs without bun installedWith frontend: { type: 'bun' } or frontend: { type: 'vite' }, running tekir build --compile produces a single executable binary that bundles your API, frontend, and all dependencies. No runtime needed. See the fluent API reference for cross-compile, --define, splitting, and the rest.
What's Next
You now have a working REST API with a controller, model, database migration, and validation. Here are some natural next steps:
- Routing: grouped routes, named routes, route parameters with type casting, and more.
- Middleware: global and per-route middleware, execution order, and built-in middleware.
- Models: query scopes, hooks, computed properties, and advanced querying.
- Authentication: JWT and session-based auth with built-in guard middleware.
- Error Handling: custom exception handlers and structured error responses.