Skip to content
intermediate Phase 12 · TypeScript & Databases

Drizzle ORM

Build type-safe database queries with Drizzle ORM.

1h
0 problems
Topic Progress 0%

Schema Written in TypeScript

Schema Written in TypeScript

Drizzle keeps the schema in ordinary TypeScript files, so the compiler checks it before anything touches a database:

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

export const users = pgTable("users", {
  id: serial("id").primaryKey(),
  email: text("email").notNull().unique(),
});

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

$inferSelect and $inferInsert derive row types straight from the table definition — no code generation step between editing a column and getting autocomplete. drizzle-kit push applies the schema to a live database (prototyping), while drizzle-kit generate emits SQL migration files your team reviews and applies in CI.

Query Builder and Relational Reads

Query Builder and Relational Reads

The builder mirrors SQL structure while staying fully typed:

const rows = await db
  .select()
  .from(users)
  .where(eq(users.email, "a@b.c"))
  .innerJoin(posts, eq(posts.authorId, users.id));

await db.insert(users).values({ email: "x@y.z" }).returning();

Pass the schema into drizzle(db, { schema }) to unlock relational queries: db.query.users.findMany({ with: { posts: true } }) compiles to an efficient joined fetch and types the nested result. Joins, returning(), and onConflictDoUpdate all produce inferred row shapes, so a bad column name is a compile error rather than a runtime 500.

Pitfalls and Interview Angles

Pitfalls and Interview Angles

push versus generate. push mutates the database immediately and can drop data on destructive diffs — fine locally, wrong for shared environments. Teams commit generate output so migrations are reviewable.

Dialect APIs differ. drizzle-orm/pg-core, mysql2, and sqlite-core expose different column builders; porting means rewriting the schema, not just the connection string.

Values stay parameters. eq(users.email, input) binds the value as a query parameter, so user input cannot rewrite SQL — the same property raw template strings would break.

Interview: contrast Drizzle's inferred types with Prisma's generated client, and explain why a typed builder still needs runtime validation at the HTTP boundary.

Quiz

1. What is the difference between drizzle-kit push and drizzle-kit generate?

Question 1 options

2. How does typeof users.$inferSelect help?

Question 2 options

3. Why does db.select().from(users).where(eq(users.email, input)) resist SQL injection?

Question 3 options

Flashcards

Question

$inferSelect / $inferInsert

Answer

Type-level row and insert types derived directly from the table definition.

Question

drizzle-kit push

Answer

Applies schema changes straight to a database — prototyping only, no review trail.

Question

drizzle-kit generate

Answer

Emits SQL migration files from schema diffs for review and CI.

Question

Relational queries

Answer

Pass { schema } to drizzle() and use db.query.x.findMany({ with }) for typed joined reads.

Revision Notes

Key Takeaways

  • 1. Drizzle's schema is TypeScript, so types update without a generate step
  • 2. push is for local iteration; generate keeps migrations reviewable
  • 3. The builder parameterizes values, which is the actual injection defense

Interview Tips

  • • Compare inference-based typing (Drizzle) with codegen-based typing (Prisma) and name one tradeoff each
  • • Show a join and explain how the result type is derived

Cheat Sheet

Schema in TS → $inferSelect/$inferInsert; push = live sync (local), generate = SQL files (CI); eq() binds params; db.query.*.findMany({ with }) for relations.

Continue Learning