Drizzle
02 / 06

Schema Definition & Table Design

Drizzle ORM: Schema Definition & Table Design

Drizzle is a TypeScript-first ORM that keeps you close to SQL while providing type safety. Your schema is the single source of truth for both the database and TypeScript types.

Setup

# Install Drizzle with your database driver
npm install drizzle-orm @neondatabase/serverless   # Neon / serverless PostgreSQL
npm install drizzle-orm postgres                    # Standard PostgreSQL
npm install drizzle-orm better-sqlite3              # SQLite
npm install drizzle-orm mysql2                      # MySQL

# Install drizzle-kit for migrations
npm install -D drizzle-kit

Database Client (Neon)

// src/db/client.ts
import { neon } from '@neondatabase/serverless';
import { drizzle } from 'drizzle-orm/neon-http';
import * as schema from './schema';

const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql, { schema });

// For connection pooling (serverful environments)
import { Pool } from '@neondatabase/serverless';
import { drizzle } from 'drizzle-orm/neon-serverless';

const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export const db = drizzle(pool, { schema });

Defining Tables (PostgreSQL)

// src/db/schema.ts
import {
  pgTable, text, varchar, integer, bigint, boolean,
  timestamp, jsonb, uuid, serial, unique, index
} from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';

export const users = pgTable('users', {
  id: uuid('id').primaryKey().defaultRandom(),
  clerkId: text('clerk_id').unique().notNull(),
  email: text('email').unique().notNull(),
  name: text('name').notNull(),
  nickname: varchar('nickname', { length: 50 }).unique(),
  plan: text('plan', { enum: ['free', 'pro', 'enterprise'] }).default('free').notNull(),
  createdAt: timestamp('created_at').defaultNow().notNull(),
  updatedAt: timestamp('updated_at').defaultNow().notNull(),
}, (table) => [
  index('users_email_idx').on(table.email),
  index('users_clerk_id_idx').on(table.clerkId),
]);

export const folders = pgTable('folders', {
  id: uuid('id').primaryKey().defaultRandom(),
  userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
  parentId: uuid('parent_id').references((): AnyPgColumn => folders.id, { onDelete: 'cascade' }),
  name: text('name').notNull(),
  slug: text('slug').notNull(),
  createdAt: timestamp('created_at').defaultNow().notNull(),
}, (table) => [
  unique('folders_user_slug_unique').on(table.userId, table.slug),
]);

export const pages = pgTable('pages', {
  id: uuid('id').primaryKey().defaultRandom(),
  folderId: uuid('folder_id').notNull().references(() => folders.id, { onDelete: 'cascade' }),
  title: text('title').notNull(),
  slug: text('slug').notNull(),
  content: text('content'),
  status: text('status', { enum: ['to_learn', 'learning', 'proficient', 'expert'] }).default('to_learn'),
  tags: uuid('tags').array().default([]).notNull(),
  metadata: jsonb('metadata').$type<{ wordCount?: number; lastEdited?: string }>(),
  isImportant: boolean('is_important').default(false).notNull(),
  createdAt: timestamp('created_at').defaultNow().notNull(),
  updatedAt: timestamp('updated_at').defaultNow().notNull(),
});

Type Inference

// Infer TypeScript types from schema — no duplication needed
export type User = typeof users.$inferSelect;       // SELECT result type
export type NewUser = typeof users.$inferInsert;    // INSERT input type

export type Folder = typeof folders.$inferSelect;
export type NewFolder = typeof folders.$inferInsert;

export type Page = typeof pages.$inferSelect;
export type NewPage = typeof pages.$inferInsert;

// Partial types for updates
export type UpdatePage = Partial<Omit<NewPage, 'id' | 'folderId' | 'createdAt'>>;

Column Reference Types (self-referencing)

// Self-referencing table needs explicit type annotation
import type { AnyPgColumn } from 'drizzle-orm/pg-core';

export const categories = pgTable('categories', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
  // Must annotate with (): AnyPgColumn for circular reference
  parentId: integer('parent_id').references((): AnyPgColumn => categories.id),
});

Column Types Reference

// Common PostgreSQL column types in Drizzle
text('col')                           // TEXT — variable length string
varchar('col', { length: 255 })      // VARCHAR(n)
char('col', { length: 10 })          // CHAR(n) — fixed length
integer('col')                        // INT4 — 32-bit integer
bigint('col', { mode: 'number' })    // INT8 — 64-bit (use mode:'bigint' for >2^53)
smallint('col')                       // INT2 — 16-bit integer
serial('col')                         // SERIAL — auto-increment int
boolean('col')                        // BOOLEAN
timestamp('col')                      // TIMESTAMP WITHOUT TIME ZONE
timestamp('col', { withTimezone: true }) // TIMESTAMPTZ
date('col')                          // DATE
numeric('col', { precision: 10, scale: 2 }) // NUMERIC — for money
real('col')                           // FLOAT4
doublePrecision('col')               // FLOAT8
uuid('col')                          // UUID
jsonb('col')                          // JSONB — binary JSON, indexable
json('col')                           // JSON — text JSON
vector('col', { dimensions: 1536 }) // pgvector extension

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free