AWS Builder Center

Using Kysely and kysely-codegen with AWS Blocks' RDB Block

Description (for the Builder Center form): How to combine AWS Blocks' DistributedDatabase block with Kysely for type-safe queries, and generate Kysely's types from your migrations with kysely-codegen, demonstrated with a minimal working example.

AWS Blocks  is an AWS toolkit that lets you define your backend and infrastructure together in TypeScript. I previously wrote about hosting TanStack Start SSR on AWS Blocks . This time I was using the RDB building block, DistributedDatabase , and noticed that with raw SQL, a typo in a table or column name doesn't surface until you actually run the query. Kysely , a TypeScript SQL query builder, catches that kind of mistake at compile time instead. And with kysely-codegen , you can generate Kysely's type definitions straight from your migration files, so the types keep up automatically whenever the schema changes.
This article walks through all three pieces together, using a minimal working example.
The full source code is here:
https://github.com/fossamagna/aws-blocks-kysely-example

AWS Blocks' RDB block: DistributedDatabase

AWS Blocks has two Building Blocks for SQL: Database (Aurora Serverless v2) and DistributedDatabase (Aurora DSQL). The docs recommend Database if you need foreign keys, triggers, or Row Level Security, and DistributedDatabase if you don't need those and want to avoid cold starts and idle cost. This example is meant to be minimal, so I used DistributedDatabase. Database supports the same Kysely integration, too.
I added a DistributedDatabase with a single todos table to the backend template from create-blocks-app.
1
npx @aws-blocks/create-blocks-app my-app --template backend
Migrations are plain SQL files.
1
2
3
4
5
6
7
-- aws-blocks/migrations/001_create_todos.sql
CREATE TABLE todos (
id TEXT PRIMARY KEY DEFAULT gen_random_uuid(),
title TEXT NOT NULL,
done BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
npm run dev applies this migration to a local PGlite (WASM Postgres) instance. Deploying applies the same migration to a real Aurora DSQL cluster. To keep behavior consistent between the two, the local mock rejects anything DSQL doesn't support (foreign keys, SERIAL, and so on) too.

Adding Kysely

DistributedDatabase has query() / execute() for raw SQL and transaction() for transactions, but it also ships a createKyselyAdapter() helper that hands you a Kysely instance:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// aws-blocks/index.ts
import { ApiNamespace, Scope, DistributedDatabase } from '@aws-blocks/blocks';
import { createKyselyAdapter } from '@aws-blocks/bb-distributed-data';
import type { DB } from './db-schema';

const scope = new Scope('aws-blocks-kysely-example');

const db = new DistributedDatabase(scope, 'db', {
migrationsPath: './aws-blocks/migrations',
});

const kysely = createKyselyAdapter<DB>(db);

export const api = new ApiNamespace(scope, 'api', () => ({
async listTodos() {
return kysely.selectFrom('todos').selectAll().orderBy('created_at', 'desc').execute();
},

async addTodo(title: string, priority?: number) {
return kysely.insertInto('todos').values({ title, priority }).returningAll().executeTakeFirstOrThrow();
},

async completeTodo(id: string) {
return kysely
.updateTable('todos')
.set({ done: true })
.where('id', '=', id)
.returningAll()
.executeTakeFirstOrThrow();
},
}));
The raw SQL equivalent looks like this:
1
db.query(sql`SELECT * FROM todos ORDER BY created_at DESC`)
Table and column names here are just strings, so a typo compiles fine. With Kysely, table and column names are checked against the DB type (defined in db-schema.ts — more on that below).
Let's actually break it on purpose to see this in practice.
1
2
// titl doesn't exist — it should be title
kysely.selectFrom('todos').select('titl').execute();
1
2
error TS2769: No overload matches this call.
Argument of type '"titl"' is not assignable to parameter of type 'SelectExpression<DB, "todos">'.
tsc catches it immediately. The same typo inside a sql tagged template compiles fine and only fails once you run it.

Generating db-schema.ts with kysely-codegen

The DB type used above isn't hand-written — it's generated by kysely-codegen. The script applies the migrations to a mock DistributedDatabase (PGlite), connects to it through the Kysely adapter, and introspects the schema to produce the types.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// aws-blocks/scripts/gen-db-schema.ts (excerpt)
import { Scope, DistributedDatabase, sql } from '@aws-blocks/blocks';
import { createKyselyAdapter } from '@aws-blocks/bb-distributed-data';
import { generate, PostgresDialect } from 'kysely-codegen';

const scope = new Scope('aws-blocks-kysely-example-schemagen');
const db = new DistributedDatabase(scope, 'db', { migrationsPath: './aws-blocks/migrations' });
const kysely = createKyselyAdapter<any>(db);

await db.query(sql`SELECT 1`); // wait for migrations to finish

const generated = await generate({
db: kysely as any,
dialect: new PostgresDialect(),
camelCase: false,
singularize: false,
excludePattern: '_migrations',
});
Running npm run db:codegen produces a db-schema.ts like this:
1
2
3
4
5
6
7
8
9
10
export interface Todos {
created_at: Generated<Timestamp | null>;
done: Generated<boolean>;
id: Generated<string>;
title: string;
}

export interface DB {
todos: Todos;
}
Let's try adding a column. All it takes is one more migration file:
1
2
-- aws-blocks/migrations/002_add_priority_to_todos.sql
ALTER TABLE todos ADD COLUMN priority INTEGER;
Running npm run db:codegen again adds priority: number | null; to Todos. Once I wired priority into addTodo, tsc caught the rest of the mismatches for me as compile errors. There's no more scenario where a hand-written schema type drifts out of sync with the actual migrations.
I also added db:codegen:check for CI, which fails on any diff — so a migration change without a regenerated db-schema.ts gets caught before it's merged.

Trying it out

1
2
3
npm install
npm run dev # mock DB + API server at http://localhost:3001
npm run test:e2e # exercises addTodo, listTodos, completeTodo through the typed client
All of this works without an AWS account. Deploying switches the same code over to a real Aurora DSQL connect
Architecture
Architecture

Wrapping up

AWS Blocks' RDB block, DistributedDatabase, ships a Kysely adapter out of the box. Swapping raw SQL strings for Kysely's query builder is enough to catch table and column typos at compile time. Add kysely-codegen on top, and you never have to hand-edit a schema type again after changing a migration.
All the code shown in this article is in the example repository. Feel free to use it as a reference.
https://github.com/fossamagna/aws-blocks-kysely-example
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article