← Back to articles

How to Set Up Database Migrations (2026)

Database migrations track schema changes over time. Without them, deploying database changes is manual and error-prone. Here's how to set them up properly.

Why Migrations?

Migrations are version control for your database schema:

migration_001_create_users.sql     → CREATE TABLE users
migration_002_add_email_column.sql → ALTER TABLE users ADD email
migration_003_create_posts.sql     → CREATE TABLE posts

Every developer and environment runs the same migrations in order. Your production database matches development.

Option 1: Drizzle Migrations (Recommended)

Setup

npm install drizzle-orm drizzle-kit

Define Schema

// src/db/schema.ts
import { pgTable, text, uuid, timestamp, boolean } from 'drizzle-orm/pg-core'

export const users = pgTable('users', {
  id: uuid('id').primaryKey().defaultRandom(),
  email: text('email').notNull().unique(),
  name: text('name').notNull(),
  createdAt: timestamp('created_at').defaultNow(),
})

export const posts = pgTable('posts', {
  id: uuid('id').primaryKey().defaultRandom(),
  title: text('title').notNull(),
  content: text('content'),
  published: boolean('published').default(false),
  authorId: uuid('author_id').references(() => users.id),
  createdAt: timestamp('created_at').defaultNow(),
})

Generate Migration

npx drizzle-kit generate

This creates a SQL migration file:

-- drizzle/0000_create_users_and_posts.sql
CREATE TABLE "users" (
  "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  "email" text NOT NULL UNIQUE,
  "name" text NOT NULL,
  "created_at" timestamp DEFAULT now()
);

CREATE TABLE "posts" (
  "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  "title" text NOT NULL,
  "content" text,
  "published" boolean DEFAULT false,
  "author_id" uuid REFERENCES "users"("id"),
  "created_at" timestamp DEFAULT now()
);

Run Migration

npx drizzle-kit migrate

Workflow

1. Modify schema.ts
2. Run `drizzle-kit generate` → creates SQL migration
3. Review the generated SQL
4. Run `drizzle-kit migrate` → applies to database
5. Commit schema + migration file to git

Option 2: Prisma Migrations

npm install prisma @prisma/client

Define Schema

// prisma/schema.prisma
model User {
  id        String   @id @default(uuid())
  email     String   @unique
  name      String
  posts     Post[]
  createdAt DateTime @default(now())
}

model Post {
  id        String   @id @default(uuid())
  title     String
  content   String?
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  String
  createdAt DateTime @default(now())
}

Generate and Run

npx prisma migrate dev --name init    # Development
npx prisma migrate deploy             # Production

Option 3: Raw SQL Migrations (golang-migrate)

For maximum control:

migrate create -ext sql -dir migrations create_users
-- migrations/000001_create_users.up.sql
CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email TEXT NOT NULL UNIQUE,
  name TEXT NOT NULL,
  created_at TIMESTAMP DEFAULT NOW()
);

-- migrations/000001_create_users.down.sql
DROP TABLE users;
migrate -database $DATABASE_URL -path migrations up

Production Migration Best Practices

1. Never Edit Existing Migrations

Once a migration is committed, treat it as immutable. Create a new migration for changes.

2. Test Migrations Before Production

# Create a test database
createdb myapp_test
DATABASE_URL=postgresql://localhost/myapp_test npx drizzle-kit migrate

3. Backward-Compatible Changes

Deploy in steps to avoid downtime:

Adding a column:

Step 1: Add column (nullable)
Step 2: Deploy code that writes to new column
Step 3: Backfill existing rows
Step 4: Make column NOT NULL

Renaming a column:

Step 1: Add new column
Step 2: Deploy code that writes to both columns
Step 3: Migrate data from old to new
Step 4: Deploy code that only uses new column
Step 5: Drop old column

4. Run Migrations in CI

# GitHub Actions
- name: Run migrations
  run: npx drizzle-kit migrate
  env:
    DATABASE_URL: ${{ secrets.DATABASE_URL }}

5. Always Have Rollback Plan

# Drizzle: drop the changes manually or restore backup
# Prisma: prisma migrate resolve
# golang-migrate: migrate down 1

FAQ

Drizzle vs Prisma for migrations?

Drizzle generates SQL you can review and modify. Prisma generates migrations automatically. Drizzle gives more control; Prisma is more automated.

Should I use an ORM for migrations?

Yes for most projects. Raw SQL migrations (golang-migrate) for teams that want maximum control or use multiple languages.

How do I handle data migrations?

Separate schema migrations from data migrations. Run data migrations as scripts, not in the migration system.

Bottom Line

Use Drizzle for TypeScript projects (you review the SQL). Use Prisma for rapid development (more automated). Use golang-migrate for polyglot teams. Always: review migrations before applying, test on staging, deploy backward-compatible changes, and never edit committed migrations.

Get AI tool guides in your inbox

Weekly deep-dives on the best AI coding tools, automation platforms, and productivity software.