Lab 07: Type-Safe ORM

Time: 60 minutes | Level: Architect | Docker: node:20-alpine

Overview

Type-safe database layer: Drizzle ORM advanced patterns (relations/subqueries/window functions/CTEs), Kysely query builder for fully typed SQL, query result inference, migration typing, and branded types for SQL injection prevention.


Step 1: Drizzle ORM Schema Definition

import { sqliteTable, text, integer, real, index, primaryKey } from 'drizzle-orm/sqlite-core';
import { relations } from 'drizzle-orm';
import { createId } from '@paralleldrive/cuid2';

// Schema definition is the source of truth for TypeScript types
export const users = sqliteTable('users', {
  id:        text('id').primaryKey().$defaultFn(() => createId()),
  name:      text('name').notNull(),
  email:     text('email').notNull().unique(),
  role:      text('role', { enum: ['admin', 'user', 'guest'] }).notNull().default('user'),
  createdAt: integer('created_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
  updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
}, (t) => ({
  emailIdx: index('users_email_idx').on(t.email),
  roleIdx:  index('users_role_idx').on(t.role),
}));

export const posts = sqliteTable('posts', {
  id:        text('id').primaryKey().$defaultFn(() => createId()),
  title:     text('title').notNull(),
  body:      text('body').notNull(),
  authorId:  text('author_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
  published: integer('published', { mode: 'boolean' }).notNull().default(false),
  createdAt: integer('created_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
});

// Relations — enable join queries
export const usersRelations = relations(users, ({ many }) => ({
  posts: many(posts),
}));

export const postsRelations = relations(posts, ({ one }) => ({
  author: one(users, { fields: [posts.authorId], references: [users.id] }),
}));

// Inferred types — free from schema
export type User        = typeof users.$inferSelect;  // SELECT result
export type NewUser     = typeof users.$inferInsert;  // INSERT shape
export type UserRole    = typeof users.role.enumValues[number]; // 'admin' | 'user' | 'guest'

Step 2: Drizzle Queries — Type-Inferred Results


Step 3: Drizzle — CTEs and Window Functions


Step 4: Kysely — Fully Typed SQL Builder


Step 5: Branded Types for SQL Safety


Step 6: Type-Safe Migrations


Step 7: Repository Pattern with Type Safety


Step 8: Capstone — Drizzle + better-sqlite3 Verification

📸 Verified Output:


Summary

Feature
Drizzle
Kysely

Schema definition

Table declarations

Interface types

Type inference

$inferSelect/$inferInsert

Selectable/Insertable

Relations

relations() + with:

Manual joins

Raw SQL

sql template

sql template

Migrations

drizzle-kit generate

kysely-ctl

Bundle size

~38KB

~40KB

SQL injection

Parameterized queries

Branded types

Last updated