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?
2. How does typeof users.$inferSelect help?
3. Why does db.select().from(users).where(eq(users.email, input)) resist SQL injection?
Flashcards
Question
$inferSelect / $inferInsert
Click to reveal answer
Answer
Type-level row and insert types derived directly from the table definition.
Question
drizzle-kit push
Click to reveal answer
Answer
Applies schema changes straight to a database — prototyping only, no review trail.
Question
drizzle-kit generate
Click to reveal answer
Answer
Emits SQL migration files from schema diffs for review and CI.
Question
Relational queries
Click to reveal answer
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.