Lab 14: Drizzle ORM Type-Safe

Time: 40 minutes | Level: Advanced | Docker: docker run -it --rm node:20-alpine sh

Build a fully type-safe SQLite database layer using Drizzle ORM: schema definition, CRUD operations, relations, inferred TypeScript types, and drizzle-kit migrations.


Step 1: Environment Setup

docker run -it --rm node:20-alpine sh
apk add --no-cache python3 make g++
npm install -g typescript ts-node
mkdir lab14 && cd lab14
npm init -y
npm install drizzle-orm better-sqlite3 @types/better-sqlite3
echo '{"compilerOptions":{"module":"commonjs","target":"es2020","strict":false,"esModuleInterop":true,"moduleResolution":"node"}}' > tsconfig.json

💡 strict: false here avoids some decorator-related issues. In production, use strict: true with Drizzle's latest version which supports it fully.


Step 2: Schema Definition

Drizzle schemas are TypeScript — types are inferred automatically:

// schema.ts
import { sqliteTable, text, integer, real, blob } from 'drizzle-orm/sqlite-core';
import { relations } from 'drizzle-orm';
import { sql } from 'drizzle-orm';

// Users table
export const users = sqliteTable('users', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  name: text('name').notNull(),
  email: text('email').notNull().unique(),
  role: text('role', { enum: ['admin', 'editor', 'viewer'] }).default('viewer').notNull(),
  createdAt: text('created_at').default(sql`CURRENT_TIMESTAMP`).notNull(),
  active: integer('active', { mode: 'boolean' }).default(true).notNull(),
});

// Posts table
export const posts = sqliteTable('posts', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  title: text('title').notNull(),
  content: text('content').notNull(),
  published: integer('published', { mode: 'boolean' }).default(false).notNull(),
  authorId: integer('author_id').notNull().references(() => users.id),
  views: integer('views').default(0).notNull(),
  score: real('score').default(0.0),
  createdAt: text('created_at').default(sql`CURRENT_TIMESTAMP`).notNull(),
});

// Tags table
export const tags = sqliteTable('tags', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  name: text('name').notNull().unique(),
  color: text('color').default('#gray'),
});

// Post-Tags junction
export const postTags = sqliteTable('post_tags', {
  postId: integer('post_id').notNull().references(() => posts.id),
  tagId: integer('tag_id').notNull().references(() => tags.id),
});

// Relations (for query builder joins)
export const usersRelations = relations(users, ({ many }) => ({
  posts: many(posts),
}));

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

// TypeScript types inferred from schema
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
export type Post = typeof posts.$inferSelect;
export type NewPost = typeof posts.$inferInsert;
export type Tag = typeof tags.$inferSelect;

💡 $inferSelect gives the TypeScript type of a SELECT result. $inferInsert gives the type for INSERT (all defaults become optional). No manual type definitions needed!


Step 3: Database Setup and Connection


Step 4: INSERT Operations

Type-safe inserts with $inferInsert:


Step 5: SELECT with WHERE and Operators


Step 6: UPDATE and DELETE


Step 7: drizzle-kit Migrations Concept

💡 drizzle-kit push (no migrations) is great for prototyping. drizzle-kit generate + migrate is for production — you get versioned, reviewable SQL migration files.


Step 8: Capstone — Complete Data Layer

Run:

📸 Verified Output:


Summary

Operation
Drizzle API
Type Safety

Define schema

sqliteTable('t', {...})

Columns define TS types

Infer types

typeof table.$inferSelect

Zero manual typing

Insert

db.insert(t).values({...}).run()

Validates insert shape

Select all

db.select().from(t).all()

Returns typed rows

Select one

.get()

Returns Type | undefined

Filter

.where(eq(t.field, val))

Type-checked values

Sort

.orderBy(desc(t.field))

Type-checked column

Aggregate

count(), avg(), sum()

SQL aggregates

Update

db.update(t).set({...}).where(...)

Validates field types

Delete

db.delete(t).where(...)

Type-checked condition

Migrations

drizzle-kit generate/migrate

Schema versioning

Last updated