Skip to main content

Getting Started

Get up and running with Kysera in 5 minutes.

Installation

Prerequisites

  • Runtime: Node.js >=20.0.0, Bun >=1.0.0, or Deno (experimental)
  • TypeScript: ^5.9.2 (recommended)
  • Module System: ESM-only (no CommonJS)

Step 1: Install Core Dependencies

# Install Kysely (peer dependency) and database driver
npm install kysely@^0.28.9 pg

# For other databases:
# npm install kysely@^0.28.9 mysql2 # MySQL
# npm install kysely@^0.28.9 better-sqlite3 # SQLite

Step 2: Install Kysera Foundation

# Install in order - executor first (foundation layer)
npm install @kysera/core # Errors, pagination, types, logger (~8KB)
npm install @kysera/executor # Unified Execution Layer - plugin foundation (~6KB)

Step 3: Choose Your Pattern (or use both)

# Repository pattern (structured CRUD with validation)
npm install @kysera/repository # Repository pattern (~12KB)

# Functional DAL (type-inferred queries with context)
npm install @kysera/dal # Functional DAL (~7KB)

# Or install both for CQRS-lite pattern

Step 4: Add Validation (Optional)

# Choose one validation library or none
npm install zod@^4.1.13 # Popular schema validation (recommended)
# OR: npm install valibot # Lightweight alternative
# OR: npm install @sinclair/typebox # JSON Schema based

Step 5: Add Plugins (Optional)

npm install @kysera/soft-delete    # Soft delete plugin (~4KB)
npm install @kysera/audit # Audit logging plugin (~11KB)
npm install @kysera/timestamps # Auto timestamps plugin (~4KB)
npm install @kysera/rls # Row-level security plugin (~44KB)

Step 6: Add Infrastructure (Optional)

npm install @kysera/infra          # Health checks, retry, circuit breaker (~12KB)
npm install @kysera/debug # Query logging and profiling (~5KB)
npm install @kysera/testing # Test utilities (~6KB) - dev dependency
npm install @kysera/migrations # Migration system (~11KB)

Quick Start

1. Define Your Database Schema

import { Generated } from 'kysely'

interface Database {
users: {
id: Generated<number>
email: string
name: string
created_at: Generated<Date>
}
posts: {
id: Generated<number>
user_id: number
title: string
content: string
created_at: Generated<Date>
}
}

2. Create Database Connection

import { Kysely, PostgresDialect } from 'kysely'
import { Pool } from 'pg'

const db = new Kysely<Database>({
dialect: new PostgresDialect({
pool: new Pool({
host: 'localhost',
database: 'myapp',
user: 'postgres',
password: 'postgres',
max: 10
})
})
})

3. Create Executor with Plugins

import { createExecutor } from '@kysera/executor'
import { softDeletePlugin } from '@kysera/soft-delete'
import { timestampsPlugin } from '@kysera/timestamps'

// Create executor with plugins (foundation layer)
const executor = await createExecutor(db, [
softDeletePlugin({ deletedAtColumn: 'deleted_at' }),
timestampsPlugin({ createdAtColumn: 'created_at', updatedAtColumn: 'updated_at' })
])

// Plugins now apply to ALL queries through this executor

4. Option A: Repository Pattern

import { createORM, createRepositoryFactory, zodAdapter } from '@kysera/repository'
import { z } from 'zod'

// Define validation schemas
const userSchema = z.object({
email: z.string().email(),
name: z.string().min(1)
})

// Create ORM (plugin container, not traditional ORM)
const orm = await createORM(executor, [])

// Create repository
const userRepo = orm.createRepository(exec => {
const factory = createRepositoryFactory(exec)
return factory.create({
tableName: 'users' as const,
mapRow: row => row,
schemas: {
create: zodAdapter(userSchema),
update: zodAdapter(userSchema.partial())
}
})
})

4. Option B: Functional DAL Pattern

import { createQuery, createContext } from '@kysera/dal'

// Define queries with type inference
const getUser = createQuery((ctx, id: number) =>
ctx.db.selectFrom('users').where('id', '=', id).selectAll().executeTakeFirst()
)

const listUsers = createQuery((ctx, limit = 10) =>
ctx.db.selectFrom('users').selectAll().limit(limit).execute()
)

const createUser = createQuery((ctx, data: { email: string; name: string }) =>
ctx.db.insertInto('users').values(data).returningAll().executeTakeFirstOrThrow()
)

// Create context with executor (plugins apply automatically)
const ctx = createContext(executor)

5. Use Your Chosen Pattern

Using Repository Pattern

// Create a user (timestamps added automatically)
const user = await userRepo.create({
email: 'john@example.com',
name: 'John Doe'
})
// Result: { id: 1, email: '...', name: '...', created_at: Date, updated_at: Date, deleted_at: null }

// Find user by ID
const foundUser = await userRepo.findById(user.id)

// Update user (updated_at set automatically)
const updated = await userRepo.update(user.id, {
name: 'John Smith'
})

// List users with pagination (soft-deleted automatically excluded)
const { data, hasNext } = await userRepo.findAll({
limit: 10,
offset: 0
})

// Soft delete user (sets deleted_at instead of removing)
await userRepo.softDelete(user.id)

// Include soft-deleted records
const allUsers = await userRepo.findAllWithDeleted()

// Restore soft-deleted user
await userRepo.restore(user.id)

Using Functional DAL Pattern

// All queries automatically filtered by plugins
const user = await createUser(ctx, {
email: 'john@example.com',
name: 'John Doe'
})
// Timestamps added automatically by timestampsPlugin

const foundUser = await getUser(ctx, user.id)
// Returns null if soft-deleted

const users = await listUsers(ctx, 20)
// Automatically excludes soft-deleted records

Using Transactions

Repository Pattern with Transactions

// Transactions with plugins preserved
await orm.transaction(async (txCtx) => {
// Create repositories with transaction context
const txUserRepo = orm.createRepository(createUserRepository)
const txPostRepo = orm.createRepository(createPostRepository)

// All operations are atomic, plugins still apply
const user = await txUserRepo.create({
email: 'jane@example.com',
name: 'Jane Doe'
})
// Timestamps added automatically in transaction

await txPostRepo.create({
user_id: user.id,
title: 'First Post',
content: 'Hello World!'
})
// Timestamps added automatically

// If error occurs, both operations roll back
})

Functional DAL with Transactions

import { withTransaction } from '@kysera/dal'

// Transactions preserve plugins
await withTransaction(executor, async (txCtx) => {
// All queries use the same transaction
const user = await createUser(txCtx, {
email: 'jane@example.com',
name: 'Jane Doe'
})

const post = await createPost(txCtx, {
user_id: user.id,
title: 'First Post',
content: 'Hello World!'
})

// Plugins (soft-delete, timestamps) still work in transaction
// If error occurs, both operations roll back
})

Combining Multiple Plugins

Plugins work with both Repository and DAL patterns through the Unified Execution Layer:

import { createExecutor } from '@kysera/executor'
import { softDeletePlugin } from '@kysera/soft-delete'
import { auditPlugin } from '@kysera/audit'
import { timestampsPlugin } from '@kysera/timestamps'

// Create executor with multiple plugins
const executor = await createExecutor(db, [
softDeletePlugin({ deletedAtColumn: 'deleted_at' }),
timestampsPlugin({
createdAtColumn: 'created_at',
updatedAtColumn: 'updated_at'
}),
auditPlugin({
getUserId: () => currentUser?.id || null,
captureOldValues: true,
captureNewValues: true
})
])

// Now use with Repository pattern
const orm = await createORM(executor, [])
const userRepo = orm.createRepository(createUserRepository)

// OR use with DAL pattern
const ctx = createContext(executor)

// ALL queries through executor get:
// - Automatic timestamps (created_at, updated_at)
// - Soft delete filtering (deleted_at)
// - Audit logging on mutations

Plugin-Specific Methods

Some plugins add methods to repositories:

// Soft delete methods (Repository pattern only)
await userRepo.softDelete(userId) // Sets deleted_at timestamp
await userRepo.restore(userId) // Clears deleted_at
await userRepo.findAllWithDeleted() // Include soft-deleted records
await userRepo.findDeletedOnly() // Only soft-deleted records

// Audit methods (Repository pattern only)
const history = await userRepo.getAuditHistory(userId)
const entry = await userRepo.getAuditEntry(auditId)
await userRepo.restoreFromAudit(auditId) // Restore old values

// Query filtering (works in both Repository and DAL)
const users = await getUsers(ctx) // Automatically excludes soft-deleted

Health Checks

import { checkDatabaseHealth, createMetricsPool } from '@kysera/infra'

const pool = new Pool({
/* config */
})
const metricsPool = createMetricsPool(pool)

const health = await checkDatabaseHealth(db, metricsPool)
console.log(health)
// {
// status: 'healthy',
// checks: {
// database: { connected: true, latency: 12 },
// pool: { size: 10, active: 2, idle: 8, waiting: 0 }
// },
// timestamp: Date
// }

Error Handling

import { DatabaseError, UniqueConstraintError, ForeignKeyError, NotFoundError } from '@kysera/core'
import { ZodError } from 'zod'

try {
await userRepo.create({ email: 'duplicate@example.com', name: 'User' })
} catch (error) {
if (error instanceof ZodError) {
// Validation error from Zod schema
console.error('Invalid input:', error.errors)
} else if (error instanceof UniqueConstraintError) {
console.error('Email already exists:', error.constraint)
} else if (error instanceof ForeignKeyError) {
console.error('Referenced record not found:', error.constraint)
} else if (error instanceof NotFoundError) {
console.error('Record not found')
} else if (error instanceof DatabaseError) {
console.error('Database error:', error.code, error.detail)
} else {
throw error
}
}

Pagination

import { paginate, paginateCursor } from '@kysera/core'

// Offset-based pagination
const page1 = await paginate(db.selectFrom('users').selectAll(), { page: 1, limit: 20 })

// Cursor-based pagination (more efficient for large datasets)
const result = await paginateCursor(db.selectFrom('users').selectAll(), {
orderBy: [{ column: 'created_at', direction: 'desc' }],
limit: 20
})

// Get next page using cursor
const nextPage = await paginateCursor(db.selectFrom('users').selectAll(), {
orderBy: [{ column: 'created_at', direction: 'desc' }],
limit: 20,
cursor: result.pagination.nextCursor
})

CQRS-lite Pattern (Repository + DAL)

Combine both patterns for commands and queries:

import { createORM } from '@kysera/repository'
import { createQuery, createContext } from '@kysera/dal'

// Create executor with plugins
const executor = await createExecutor(db, [
softDeletePlugin(),
timestampsPlugin()
])

// Create ORM for writes
const orm = await createORM(executor, [])

// Define complex read queries with DAL
const getDashboardStats = createQuery((ctx, userId: number) =>
ctx.db
.selectFrom('users')
.leftJoin('posts', 'users.id', 'posts.user_id')
.select(({ fn }) => [
'users.id',
'users.name',
fn.count('posts.id').as('post_count')
])
.where('users.id', '=', userId)
.groupBy('users.id')
.executeTakeFirst()
)

// Use in transaction - both patterns work together
await orm.transaction(async (txCtx) => {
// Repository for writes
const userRepo = orm.createRepository(createUserRepository)
const user = await userRepo.create({ email: 'test@example.com', name: 'Test' })

// DAL for complex reads (same transaction context)
const stats = await getDashboardStats(txCtx, user.id)

// Both share the same plugins and transaction
})

Next Steps