Free & open source — no account required
Free & open source — no account required
This technical guide provides an in-depth analysis of the json to kysely schema engine, best practices for implementation, and data security standards.
Kysely occupies a specific niche: you want to write real SQL — including window functions, recursive CTEs, lateral joins, and database-specific syntax — but you also want TypeScript to catch typos in column names, incorrect column types in WHERE clauses, and broken JOIN references at compile time. It achieves this without adding a runtime query engine or generating code. The TypeScript types you define from your JSON shape become the contract your entire query layer is checked against.
// Input JSON
{
"user_id": "uuid-123",
"email": "dev@kysely.dev",
"is_active": true,
"subscription_tier": "pro",
"profile": {
"bio": "SQL Enthusiast",
"avatar_url": "https://cdn.example.com/avatars/dev.jpg"
},
"created_at": "2024-01-15T08:30:00Z"
}
// Generated Kysely Database Interface
import { Generated, Insertable, Selectable, Updateable, JSONColumnType } from 'kysely';
// Table row shape — what SELECT queries return
interface UserTable {
user_id: string;
email: string;
is_active: boolean;
subscription_tier: 'free' | 'pro' | 'enterprise';
profile: JSONColumnType<
{ bio: string; avatar_url: string }, // SELECT type
{ bio?: string; avatar_url?: string }, // INSERT type
{ bio?: string; avatar_url?: string } // UPDATE type
>;
created_at: Generated<Date>; // auto-set by DB, optional on insert
}
// Derived types for each operation
export type User = Selectable<UserTable>;
export type NewUser = Insertable<UserTable>;
export type UserUpdate = Updateable<UserTable>;
// Root database interface — all tables
export interface Database {
users: UserTable;
}
JSONColumnType<Select, Insert, Update> lets you express that a JSON column's SELECT type (non-null, fully typed) differs from its INSERT type (optional fields) — because the database might set defaults that aren't in the INSERT payload. This is more precise than any ORM that uses a single type for all three operations.
import { Kysely, PostgresDialect } from 'kysely';
import { Pool } from 'pg';
import { Database } from './schema';
const db = new Kysely<Database>({
dialect: new PostgresDialect({ pool: new Pool({ connectionString: process.env.DATABASE_URL }) }),
});
// SELECT — TypeScript knows every column name and type
const activeUsers = await db
.selectFrom('users')
.select(['user_id', 'email', 'subscription_tier'])
.where('is_active', '=', true)
.orderBy('created_at', 'desc')
.limit(20)
.execute();
// activeUsers: Array<{ user_id: string; email: string; subscription_tier: 'free'|'pro'|'enterprise' }>
// INSERT — NewUser type enforced at compile time
const newUser: NewUser = {
user_id: crypto.randomUUID(),
email: 'newuser@example.com',
is_active: true,
subscription_tier: 'free',
};
await db.insertInto('users').values(newUser).execute();
// UPDATE — UpdateUser type (all fields optional)
await db
.updateTable('users')
.set({ subscription_tier: 'pro', is_active: true })
.where('user_id', '=', userId)
.execute();
// DELETE with returning
const deleted = await db
.deleteFrom('users')
.where('user_id', '=', userId)
.returning(['user_id', 'email'])
.executeTakeFirst();
Kysely's type system tracks the tables in scope and validates column references across joined tables:
interface PostTable {
id: Generated<number>;
author_id: string;
title: string;
body: string;
status: 'draft' | 'published';
created_at: Generated<Date>;
}
// Database interface with both tables
interface Database {
users: UserTable;
posts: PostTable;
}
// JOIN — column names from both tables are type-checked
const results = await db
.selectFrom('posts')
.innerJoin('users', 'users.user_id', 'posts.author_id')
.select([
'posts.id',
'posts.title',
'posts.status',
'users.email as author_email', // aliased — typed as string
'users.subscription_tier',
])
.where('posts.status', '=', 'published')
.where('users.is_active', '=', true)
.orderBy('posts.created_at', 'desc')
.execute();
// results[0].title — string
// results[0].author_email — string
// results[0].subscription_tier — 'free' | 'pro' | 'enterprise'
// Typo in any column name: TypeScript compile error
This is where Kysely outshines every ORM — complex SQL is fully type-safe:
// Common Table Expression with typed CTE reference
const result = await db
.with('active_users', (db) =>
db.selectFrom('users')
.select(['user_id', 'email', 'created_at'])
.where('is_active', '=', true)
)
.selectFrom('active_users')
.innerJoin('posts', 'posts.author_id', 'active_users.user_id')
.select([
'active_users.email',
db.fn.count('posts.id').as('post_count'),
])
.groupBy('active_users.user_id')
.having(db.fn.count('posts.id'), '>=', 5)
.execute();
// result[0].email — string
// result[0].post_count — string (PostgreSQL COUNT returns bigint as string)
// Window function
const ranked = await db
.selectFrom('posts')
.select([
'id',
'author_id',
'created_at',
db.fn.agg<number>('ROW_NUMBER').over(b =>
b.partitionBy('author_id').orderBy('created_at', 'desc')
).as('rank_in_author'),
])
.execute();
For existing databases, generate the TypeScript interfaces automatically:
# Install
npm install --save-dev kysely-codegen
# Generate interfaces from PostgreSQL
npx kysely-codegen --dialect postgres --url $DATABASE_URL --out-file src/db/types.ts
# Output: src/db/types.ts
# export interface DB {
# users: UsersTable;
# posts: PostsTable;
# ...
# }
# export interface UsersTable {
# user_id: string;
# email: string;
# is_active: Generated<boolean>;
# ...
# }
This eliminates schema drift entirely — regenerate after every migration and your TypeScript types always match the actual database schema. Works with PostgreSQL, MySQL, SQLite, and MSSQL.
// Transaction — all queries share a connection and roll back together on error
await db.transaction().execute(async (trx) => {
const user = await trx
.insertInto('users')
.values(newUser)
.returning('user_id')
.executeTakeFirstOrThrow();
await trx
.insertInto('posts')
.values({ author_id: user.user_id, title: 'First Post', body: '...', status: 'draft' })
.execute();
// If this throws, the user insert is automatically rolled back
});
executeTakeFirstOrThrow() when you expect exactly one row: executeTakeFirst() returns T | undefined. If undefined would be a bug (fetching by primary key), use executeTakeFirstOrThrow() to throw a NoResultError instead of silently passing undefined into business logic.db instance: Create one db instance, export it from a dedicated module, and import it everywhere. Multiple instances each maintain their own connection pool..selectAll() only in codegen-driven codebases: selectAll() returns all columns but requires your interface to be exactly correct. For hand-written interfaces, enumerate selected columns explicitly — it makes query intent clear and avoids over-fetching.Q: Does Kysely handle migrations?
A: Kysely has a Migrator class that runs migration files (JavaScript/TypeScript functions), but it doesn't auto-generate migrations from a schema definition. Use Kysely's Migrator with hand-written SQL or TypeScript migration files, or use kysely-codegen to generate types from a schema managed by another migration tool (Flyway, Liquibase, etc.).
Q: Can I use Kysely with connection pooling in serverless environments?
A: Yes. Use @neondatabase/serverless for Neon, @planetscale/database for PlanetScale, or the standard pg pool for regular PostgreSQL. Each has a Kysely dialect package. In Lambda/Edge environments, configure the pool with max: 1 to avoid connection exhaustion from concurrent function instances.
Q: How do I handle database-generated fields like auto-increment IDs?
A: Wrap the column type in Generated<T> (e.g., Generated<number> for serial, Generated<Date> for default now()). Kysely's Insertable<T> utility automatically makes Generated fields optional on insert — you don't pass them in, and the SELECT result type has the actual value.
Q: Can Kysely validate query results at runtime?
A: No — Kysely is TypeScript-only with no runtime component. Pair it with Zod or Valibot for runtime validation of query results if you need to guard against schema drift between your TypeScript types and the actual database schema.
Is the processing local-only?
Absolutely. TypeMorph operates entirely within your browser's sandbox. We use Web Workers for high-performance computation without ever transmitting your JSON, SQL, or API data to a remote server.
Can I use this for enterprise projects?
Yes. The tool is designed for professional software engineers who require GDPR compliance and data privacy. It is trusted by developers at top-tier startups and financial institutions.
Why pasting proprietary company data into third-party web tools is a major liability, and how to stay safe.
Code generation is just the beginning. Discover how a schema-first approach can eliminate 90% of your integration bugs.