| |

Node.js 46 🟢 Lightweight Type-Safe Database Querying with Drizzle ORM

Drizzle ORM is a TypeScript ORM built on a different philosophy than the traditional data frameworks. It does not try to abstract SQL away. It embraces SQL and provides a thin, type-safe layer on top of it. The schema is defined in TypeScript, the queries look like SQL, and the generated SQL is exactly what you would write by hand. The result is a library that is lightweight, serverless-ready, and predictable. Drizzle is approximately 7.4 kB minified and gzipped, has zero runtime dependencies, and is tree-shakeable. It supports every PostgreSQL, MySQL, and SQLite database, including serverless platforms like Turso, Neon, PlanetScale, and Cloudflare D1 .

The core insight is that the double learning curve of the traditional ORMs is unnecessary. With Drizzle, if you know SQL, you know Drizzle. The query builder uses the SQL-like syntax, and the generated query is what you would expect. The select, the insert, the update, and the delete map directly to the SQL statements. The schema is declared with the pgTable, the mysqlTable, or the sqliteTable functions, and the columns are the TypeScript objects with the type inference. The type safety is the compile-time, and the runtime is the thin .

The Drizzle ecosystem has three parts. The drizzle-orm is the runtime library. The drizzle-kit is the CLI for the migrations and the introspection. The Drizzle Studio is the visual browser for the database. The drizzle-kit generate creates the SQL migration files from the schema changes, and the drizzle-kit migrate applies them. The migrations are the plain SQL, and they can be reviewed before the application .

This chapter covers three areas. First, why Drizzle exists — the abstraction problem and the SQL-like approach. Second, how to define the schema and the queries — the table declaration, the column types, the relations, and the CRUD operations. Third, the migrations and the patterns — the drizzle-kit, the generate and the migrate, the relational queries, and the type inference. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing the Drizzle workflow.

Key point: Drizzle ORM is a lightweight, type-safe ORM that embraces SQL. The schema is defined in TypeScript with the pgTable, the mysqlTable, or the sqliteTable. The queries use the SQL-like builder or the relational query API. The drizzle-kit generates and applies the migrations. Drizzle has zero runtime dependencies, supports every PostgreSQL, MySQL, and SQLite database, and is serverless-ready by design.


Why Drizzle exists

The abstraction problem. The traditional ORMs hide the SQL behind the object-oriented API. The developer writes the user.save() instead of the INSERT INTO users, and the ORM generates the SQL. The abstraction is convenient, but it introduces the double learning curve: the developer must know both SQL and the ORM’s API. The debugging requires the understanding of the generated SQL, and the performance tuning requires the inspection of the query plan. Drizzle removes the abstraction. The query builder is the SQL-like, and the generated SQL is the predictable .

The type-safety problem. The TypeScript ORMs provide the type safety, but the level of the inference varies. The Drizzle’s inference is the full: the $inferSelect and the $inferInsert types are derived from the schema, and the query results are typed. The db.select().from(users) returns the User[], and the db.insert(users).values(...) accepts the NewUser. The compiler catches the typos, the wrong types, and the missing fields .

The serverless problem. The serverless databases — the Neon, the PlanetScale, the Turso, the Cloudflare D1 — require the lightweight client. The traditional ORMs often ship the heavy runtime, the Rust binaries, or the serverless adapters. Drizzle is the serverless-ready by design: it has the zero dependencies, the small bundle, and it works in every JavaScript runtime including the Node.js, the Bun, the Deno, the Cloudflare Workers, and the edge runtimes .

The migration problem. The Drizzle’s drizzle-kit generates the SQL migration files from the schema changes. The migrations are the plain SQL, and they can be reviewed, versioned, and applied in the order. The drizzle-kit generate creates the migration, and the drizzle-kit migrate applies it. The __drizzle_migrations table tracks the applied migrations, and the drizzle-kit push is the development shortcut that skips the migration files .

The relational-query problem. The Drizzle provides the two query APIs. The SQL-like API is the db.select().from(users).leftJoin(posts, eq(users.id, posts.userId)), and the relational API is the db.query.users.findMany({ with: { posts: true } }). The relational API returns the nested data in the single SQL query, and the developer does not worry about the joins or the data mapping .

The trade-off. Drizzle is the thin layer, and the thin layer means the developer writes the SQL-like queries. The relational API is the optional, and the SQL-like API is the default. The trade-off is between the control of the SQL and the convenience of the relational API, and Drizzle provides both.


a. The schema definition

The schema is defined in TypeScript with the dialect-specific functions. The pgTable is for the PostgreSQL, the mysqlTable is for the MySQL, and the sqliteTable is for the SQLite.

import { pgTable, serial, text, varchar, timestamp, boolean } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  email: varchar('email', { length: 256 }).notNull().unique(),
  name: text('name').notNull(),
  active: boolean('active').default(true).notNull(),
  createdAt: timestamp('created_at').defaultNow().notNull(),
});

The pgTable defines the table, and the columns are the TypeScript objects. The serial is the auto-incrementing integer, the varchar is the variable-length string, the text is the unlimited string, the timestamp is the date and time, and the boolean is the true/false .

The snake_case mapping. The database uses the snake_case, and the TypeScript uses the camelCase. The snakeCase builder maps the TypeScript keys to the database columns automatically:

import { pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  fullName: text('full_name').notNull(),
  createdAt: timestamp('created_at').defaultNow().notNull(),
}, (table) => ({
  // the indexes
}));

The fullName in the TypeScript maps to the full_name in the database .

The relations. The relations function defines the application-level relations. The one and the many describe the cardinality:

import { relations } from 'drizzle-orm';

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
});

export const posts = pgTable('posts', {
  id: serial('id').primaryKey(),
  title: text('title').notNull(),
  authorId: integer('author_id').notNull().references(() => users.id),
});

export const usersRelations = relations(users, ({ many }) => ({
  posts: many(posts),
}));

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

The usersRelations defines the one-to-many from the users to the posts, and the postsRelations defines the many-to-one from the posts to the users. The fields and the references specify the foreign key .

The indexes. The indexes are defined in the third argument of the table function:

import { index, uniqueIndex } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  email: varchar('email', { length: 256 }).notNull(),
}, (table) => ({
  emailIdx: uniqueIndex('idx_users_email').on(table.email),
  nameIdx: index('idx_users_name').on(table.name),
}));

The uniqueIndex creates the unique index, and the index creates the regular. The indexes are the important for the query performance .


b. The queries

The insert. The db.insert creates the rows:

import { db } from './db';
import { users } from './schema';

const newUser = await db
  .insert(users)
  .values({ email: 'alice@example.com', name: 'Alice' })
  .returning();

console.log(newUser[0].id);

The .returning() returns the inserted row, which is the PostgreSQL and the SQLite feature. The MySQL uses the $returningId() or the separate select .

The select. The db.select reads the rows:

import { eq, and, like, gt, desc } from 'drizzle-orm';

const allUsers = await db.select().from(users);

const user = await db
  .select()
  .from(users)
  .where(eq(users.email, 'alice@example.com'))
  .limit(1);

const filtered = await db
  .select()
  .from(users)
  .where(and(like(users.email, '%@example.com'), gt(users.createdAt, new Date('2024-01-01'))))
  .orderBy(desc(users.createdAt))
  .limit(10);

The eq is the equality, the and combines the conditions, the like is the pattern, the gt is the greater than, and the desc is the descending order .

The update. The db.update modifies the rows:

await db
  .update(users)
  .set({ name: 'Alice Smith' })
  .where(eq(users.id, userId));

The delete. The db.delete removes the rows:

await db.delete(users).where(eq(users.id, userId));

The relational queries. The db.query API returns the nested data:

const usersWithPosts = await db.query.users.findMany({
  with: {
    posts: true,
  },
});

const userWithPosts = await db.query.users.findFirst({
  where: eq(users.id, 1),
  with: {
    posts: {
      where: eq(posts.published, true),
      orderBy: desc(posts.createdAt),
      limit: 5,
    },
  },
});

The findMany returns the array, and the findFirst returns the single. The with includes the relations, and the nested where, the orderBy, and the limit are the supported .

The partial select. The columns parameter limits the returned fields:

const names = await db
  .select({ id: users.id, name: users.name })
  .from(users);

The partial select reduces the data transferred from the database .

The transactions. The db.transaction runs the multiple queries in the transaction:

await db.transaction(async (tx) => {
  const [user] = await tx
    .insert(users)
    .values({ email: 'alice@example.com', name: 'Alice' })
    .returning();

  await tx.insert(posts).values({ title: 'First Post', authorId: user.id });
});

The tx is the transaction object, and the queries inside the callback are the transactional. The commit happens when the callback resolves, and the rollback happens when it throws .


c. The migrations

The drizzle.config.ts. The configuration specifies the schema path, the output directory, and the dialect:

import { defineConfig } from 'drizzle-kit';

export default defineConfig({
  schema: './src/db/schema.ts',
  out: './drizzle',
  dialect: 'postgresql',
});

The schema is the path to the schema file, the out is the migration output directory, and the dialect is the database type .

The generate. The drizzle-kit generate creates the migration from the schema changes:

npx drizzle-kit generate

The command compares the current schema with the last known state and generates the SQL migration file. The output is the .sql file in the drizzle directory .

The migrate. The drizzle-kit migrate applies the pending migrations:

npx drizzle-kit migrate

The command runs the pending migration files against the database and tracks the applied migrations in the __drizzle_migrations table .

The push. The drizzle-kit push applies the schema changes directly to the database, skipping the migration files:

npx drizzle-kit push

The push is the development shortcut, and it should not be used in the production, because it bypasses the migration history .

The Studio. The drizzle-kit studio opens the visual browser:

npx drizzle-kit studio

The Studio shows the tables, the columns, and the data, and it allows the editing .


Complete Example Session

// ============================================
// PART 1: THE SCHEMA
// ============================================

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

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  email: varchar('email', { length: 256 }).notNull().unique(),
  name: text('name').notNull(),
  active: boolean('active').default(true).notNull(),
  createdAt: timestamp('created_at').defaultNow().notNull(),
});

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

export const usersRelations = relations(users, ({ many }) => ({
  posts: many(posts),
}));

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


// ============================================
// PART 2: THE TYPE INFERENCE
// ============================================

export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
export type Post = typeof posts.$inferSelect;
export type NewPost = typeof posts.$inferInsert;

// The $inferSelect is the read type, and the $inferInsert is the write type.


// ============================================
// PART 3: THE DATABASE CONNECTION
// ============================================

// src/db/index.ts
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';

const client = postgres(process.env.DATABASE_URL);
export const db = drizzle(client, { schema });


// ============================================
// PART 4: THE INSERT
// ============================================

const newUser = await db
  .insert(users)
  .values({ email: 'alice@example.com', name: 'Alice' })
  .returning();

console.log(newUser[0].id);


// ============================================
// PART 5: THE SELECT
// ============================================

import { eq, and, like, gt, desc } from 'drizzle-orm';

const allUsers = await db.select().from(users);

const user = await db
  .select()
  .from(users)
  .where(eq(users.email, 'alice@example.com'))
  .limit(1);

const filtered = await db
  .select()
  .from(users)
  .where(and(like(users.email, '%@example.com'), gt(users.createdAt, new Date('2024-01-01'))))
  .orderBy(desc(users.createdAt))
  .limit(10);


// ============================================
// PART 6: THE UPDATE AND THE DELETE
// ============================================

await db
  .update(users)
  .set({ name: 'Alice Smith' })
  .where(eq(users.id, userId));

await db.delete(users).where(eq(users.id, userId));


// ============================================
// PART 7: THE RELATIONAL QUERY
// ============================================

const usersWithPosts = await db.query.users.findMany({
  with: {
    posts: true,
  },
});

const userWithPosts = await db.query.users.findFirst({
  where: eq(users.id, 1),
  with: {
    posts: {
      where: eq(posts.published, true),
      orderBy: desc(posts.createdAt),
      limit: 5,
    },
  },
});


// ============================================
// PART 8: THE TRANSACTION
// ============================================

await db.transaction(async (tx) => {
  const [user] = await tx
    .insert(users)
    .values({ email: 'bob@example.com', name: 'Bob' })
    .returning();

  await tx.insert(posts).values({
    title: 'First Post',
    content: 'Hello world',
    authorId: user.id,
  });
});


// ============================================
// PART 9: THE MIGRATIONS
// ============================================

// The configuration
// drizzle.config.ts
import { defineConfig } from 'drizzle-kit';

export default defineConfig({
  schema: './src/db/schema.ts',
  out: './drizzle',
  dialect: 'postgresql',
});

// The generate
// npx drizzle-kit generate

// The migrate
// npx drizzle-kit migrate

// The studio
// npx drizzle-kit studio


// ============================================
// PART 10: THE COMPLETE EXAMPLE
// ============================================

// The schema
export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  email: varchar('email', { length: 256 }).notNull().unique(),
  name: text('name').notNull(),
  createdAt: timestamp('created_at').defaultNow().notNull(),
});

export const posts = pgTable('posts', {
  id: serial('id').primaryKey(),
  title: text('title').notNull(),
  authorId: integer('author_id').notNull().references(() => users.id),
});

export const usersRelations = relations(users, ({ many }) => ({
  posts: many(posts),
}));

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

// The query with the relations
const usersWithPosts = await db.query.users.findMany({
  with: {
    posts: true,
  },
});

// The type
type UserWithPosts = Awaited<ReturnType<typeof usersWithPosts>>;
// The type is inferred: the User with the posts array.

The ten parts show the schema, the type inference, the connection, the insert, the select, the update and the delete, the relational query, the transaction, the migrations, and the complete example.


Quick Reference

The Packages

PackageThe purpose
drizzle-ormThe runtime library
drizzle-kitThe CLI for the migrations and the introspection
drizzle-zodThe Zod integration
The driversThe postgres, the mysql2, the better-sqlite3, the @neondatabase/serverless

The Table Functions

The dialectThe function
PostgreSQLpgTable
MySQLmysqlTable
SQLitesqliteTable

The Column Types

The PostgreSQLThe MySQLThe SQLite
serialintinteger
varcharvarchartext
texttexttext
timestamptimestampinteger
booleanbooleaninteger
integerintinteger

The Query Methods

The methodThe purpose
db.select()The select
db.insert()The insert
db.update()The update
db.delete()The delete
db.queryThe relational query
db.transaction()The transaction

The Operators

The operatorThe purpose
eqThe equality
andThe AND
orThe OR
likeThe pattern
gtThe greater than
descThe descending
ascThe ascending

The drizzle-kit Commands

The commandThe purpose
drizzle-kit generateThe generate the migration
drizzle-kit migrateThe apply the migration
drizzle-kit pushThe development shortcut
drizzle-kit studioThe visual browser
drizzle-kit checkThe schema drift

Best Practices

✅ Do This:

// Use the schema as the source of truth
export const users = pgTable('users', { ... });                           // ✅
// Use the $inferSelect and $inferInsert
export type User = typeof users.$inferSelect;                             // ✅
// Use the .returning() for the insert
await db.insert(users).values(...).returning();                           // ✅
// Use the relations for the relational queries
const users = await db.query.users.findMany({ with: { posts: true } });   // ✅
// Use the transactions for the related inserts
await db.transaction(async (tx) => { ... });                              // ✅
// Generate the migrations for the production
// npx drizzle-kit generate && npx drizzle-kit migrate                     // ✅
// Use the partial select for the large tables
const names = await db.select({ id: users.id, name: users.name });        // ✅

❌ Don’t Do This:

// Don't forget the .returning()
await db.insert(users).values(...);  // the no data returned                // ❌
// Don't skip the transactions for the related inserts
await db.insert(users).values(...);
await db.insert(posts).values(...);  // the partial failure                 // ❌
// Don't use the db:push in the production
// npx drizzle-kit push  // the bypasses the migrations                     // ❌
// Don't manually edit the generated migration
// the create a new one instead                                             // ❌
// Don't delete the applied migrations
// the __drizzle_migrations tracks them                                     // ❌
// Don't forget the indexes
// the query performance degrades                                           // ❌
// Don't use the raw SQL when the builder works
await db.execute(sql`SELECT * FROM users`);  // the builder is the safer   // ❌

Common Pitfalls

The pitfallThe whyThe fix
The insert returns no dataThe missing .returning()Add the .returning()
The partial failureThe no transactionUse the db.transaction
The schema driftThe manual changeUse the drizzle-kit check
The missing indexThe no index in the schemaAdd the index()
The relation not inferredThe missing relationNameAdd the relationName
The migration failsThe SQL errorReview the generated SQL
The production pushThe db:pushUse the generate and the migrate

Real-World Examples

1. The Basic Select

const users = await db.select().from(users);

2. The Filter

const user = await db.select().from(users).where(eq(users.email, 'a@b.com'));

3. The Insert

await db.insert(users).values({ email: 'a@b.com', name: 'A' }).returning();

4. The Update

await db.update(users).set({ name: 'B' }).where(eq(users.id, 1));

5. The Delete

await db.delete(users).where(eq(users.id, 1));

6. The Relational

const users = await db.query.users.findMany({ with: { posts: true } });

7. The Transaction

await db.transaction(async (tx) => { ... });

8. The Partial Select

const names = await db.select({ id: users.id, name: users.name }).from(users);

9. The Index

(table) => ({ emailIdx: uniqueIndex('idx_users_email').on(table.email) })

10. The Type

type User = typeof users.$inferSelect;

Visual

The Drizzle Workflow

┌──────────────────────────────────────────────────────────────┐
│  THE DRIZZLE WORKFLOW                                        │
│                                                              │
│  The schema:                                                 │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  src/db/schema.ts                                    │    │
│  │    export const users = pgTable('users', { ... });   │    │
│  └──────────────────────────────────────────────────────┘    │
│    │                                                         │
│    │  drizzle-kit generate                                  │
│    ▼                                                         │
│  The migration:                                              │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  drizzle/0000_initial.sql                            │    │
│  │    CREATE TABLE users (...);                         │    │
│  └──────────────────────────────────────────────────────┘    │
│    │                                                         │
│    │  drizzle-kit migrate                                   │
│    ▼                                                         │
│  The database:                                               │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  PostgreSQL, MySQL, SQLite, ...                      │    │
│  └──────────────────────────────────────────────────────┘    │
│    │                                                         │
│    │  drizzle-orm                                           │
│    ▼                                                         │
│  The application:                                            │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  const users = await db.select().from(users);        │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
└──────────────────────────────────────────────────────────────┘

The SQL-Like vs the Relational

┌──────────────────────────────────────────────────────────────┐
│  THE SQL-LIKE vs THE RELATIONAL                              │
│                                                              │
│  THE SQL-LIKE:                                               │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  db.select()                                         │    │
│  │    .from(users)                                      │    │
│  │    .leftJoin(posts, eq(users.id, posts.authorId))    │    │
│  │                                                       │    │
│  │  The explicit, the SQL-like, the full control.       │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  THE RELATIONAL:                                             │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  db.query.users.findMany({                           │    │
│  │    with: { posts: true },                            │    │
│  │  })                                                  │    │
│  │                                                       │    │
│  │  The nested data, the no joins, the convenience.     │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  The both output the single SQL query.                       │
│                                                              │
└──────────────────────────────────────────────────────────────┘

The Relations

┌──────────────────────────────────────────────────────────────┐
│  THE RELATIONS                                               │
│                                                              │
│  export const users = pgTable('users', { ... });             │
│  export const posts = pgTable('posts', { ... });             │
│                                                              │
│  export const usersRelations = relations(users, ({ many }) => ({
│    posts: many(posts),                                      │
│  }));                                                       │
│                                                              │
│  export const postsRelations = relations(posts, ({ one }) => ({
│    author: one(users, {                                     │
│      fields: [posts.authorId],                              │
│      references: [users.id],                                │
│    }),                                                      │
│  }));                                                       │
│                                                              │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  The query:                                          │    │
│  │    const users = await db.query.users.findMany({     │    │
│  │      with: { posts: true },                          │    │
│  │    });                                               │    │
│  │                                                       │    │
│  │  The result: the User[] with the posts array.        │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
└──────────────────────────────────────────────────────────────┘

The Type Inference

┌──────────────────────────────────────────────────────────────┐
│  THE TYPE INFERENCE                                          │
│                                                              │
│  export const users = pgTable('users', {                     │
│    id: serial('id').primaryKey(),                            │
│    email: varchar('email', { length: 256 }).notNull(),       │
│    name: text('name').notNull(),                             │
│  });                                                         │
│                                                              │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  export type User = typeof users.$inferSelect;       │    │
│  │    // { id: number, email: string, name: string }    │    │
│  │                                                       │    │
│  │  export type NewUser = typeof users.$inferInsert;    │    │
│  │    // { email: string, name: string, id?: number }   │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  The types are inferred from the schema, and the compiler    │
│  catches the typos and the wrong types.                      │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
Drizzle ORMThe lightweight, type-safe ORM
The bundle~7.4 kB minified+gzipped
The dependenciesZero
The dialectsPostgreSQL, MySQL, SQLite
The schemaThe pgTable, the mysqlTable, the sqliteTable
The queriesThe SQL-like and the relational
The migrationsThe drizzle-kit generate and the migrate
The type inferenceThe $inferSelect and the $inferInsert
The relationsThe relations function
The serverlessThe Neon, the PlanetScale, the Turso, the D1

Key takeaways:

  • Drizzle ORM is a lightweight, type-safe ORM that embraces SQL. The schema is defined in TypeScript, and the queries look like SQL. The generated SQL is the predictable, and the type inference is the full .
  • The schema is the source of truth. The pgTable, the mysqlTable, and the sqliteTable define the tables, and the $inferSelect and the $inferInsert derive the types. The relations function defines the application-level relations .
  • The queries use the SQL-like or the relational API. The db.select(), the db.insert(), the db.update(), and the db.delete() are the SQL-like. The db.query is the relational, and it returns the nested data in the single query .
  • The drizzle-kit generates and applies the migrations. The generate creates the SQL migration from the schema changes, and the migrate applies it. The push is the development shortcut .
  • The type inference is the full. The User and the NewUser types are derived from the schema, and the compiler catches the typos and the wrong types .
  • The relations are the application-level, not the database-level. The relations function defines the one-to-one, the one-to-many, and the many-to-many. The foreign keys are the separate .
  • Drizzle is the serverless-ready. The zero dependencies, the small bundle, and the support for the Neon, the PlanetScale, the Turso, and the D1 make it the choice for the serverless and the edge .
  • The migrations are the plain SQL. The drizzle-kit generate produces the .sql files, and they can be reviewed before the application. The __drizzle_migrations table tracks the applied migrations .

Remember: Drizzle ORM is the lightweight, type-safe ORM that embraces SQL. The schema is the TypeScript, and the queries are the SQL-like or the relational. The drizzle-kit handles the migrations, and the type inference is the full. The result is the predictable, the performant, and the serverless-ready database layer, which is the foundation of the modern Node.js application.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!