webroad.online
  1. 1Web
  2. 2HTML
  3. 3CSS
  4. 4JavaScript
  5. 5TypeScript
  6. 6Git
  7. 7Tooling
  8. 8React
  9. 9State management
  10. 10Next.js
  11. 11Forms
  12. 12Data and backend
  13. 13SEO
  14. 14Tailwind CSS
  15. 15Animations
  16. 16Testing
  17. 17Architecture
Data and backend · Lesson 3 of 4

ORMs: Drizzle and Prisma

Schemas in TypeScript, typed queries, migrations; raw SQL vs Drizzle vs Prisma.

Updated

What an ORM is

An ORM (Object-Relational Mapper) lets you work with the database through TypeScript code instead of SQL written as text. You define the tables once, as a schema, and the ORM gives you:

  • typed queries — autocomplete on columns, compile-time errors if you get a name wrong;
  • migrations — a versioned history of structural changes;
  • built-in protection against SQL injection.

Ways to access the database

Option Example Pros Cons
raw SQL (a driver) sql`SELECT ...` with @neondatabase/serverless full control, zero abstraction no automatic types, manual migrations
a query builder / "SQL-like" ORM Drizzle, Kysely close to SQL, typed, light, runs on the edge you have to know SQL
a classic ORM Prisma a very simple API, excellent tooling (Studio) a thicker abstraction, client generation

Drizzle — the schema and queries

// shared/api/schema.ts
import { pgTable, serial, text, integer, timestamp } from 'drizzle-orm/pg-core'

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

export const posts = pgTable('posts', {
  id: serial('id').primaryKey(),
  userId: integer('user_id').notNull().references(() => users.id),
  title: text('title').notNull(),
  createdAt: timestamp('created_at').defaultNow().notNull(),
})
import { drizzle } from 'drizzle-orm/neon-http'
import { desc, eq } from 'drizzle-orm'

const db = drizzle(process.env.DATABASE_URL!)

const latest = await db.select().from(posts).where(eq(posts.userId, 1)).orderBy(desc(posts.createdAt)).limit(10)
const [created] = await db.insert(posts).values({ userId: 1, title: 'Hello' }).returning()
type Post = typeof posts.$inferSelect                    // a row's type, from the schema

Notice: userId in the code, user_id in the database — the camelCase ↔ snake_case mapping is done by the ORM.

Prisma — the same idea, a different style

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  author    User     @relation(fields: [authorId], references: [id])
  authorId  Int
}
const latest = await prisma.post.findMany({ where: { authorId: 1 }, orderBy: { id: 'desc' }, take: 10, include: { author: true } })

Migrations

A migration is a file that describes a structural change (a new table, a new column), applied only once, in order, to every database (local, preview, production).

npx drizzle-kit generate    # from the TS schema → a SQL migration file
npx drizzle-kit migrate     # applies the migrations that haven't run yet
Rule Why
migrations go into Git the same history everywhere
you don't edit a migration that's already applied you create a new one
"destructive" changes (renaming, dropping a column) in steps existing data and old code must keep working in between

Where they live in FSD

The schema and the DB client → shared/api/. An entity's queries → entities/<x>/api/ (like entities/lesson/api/lesson-api.ts in this project). Components never import the DB client directly.

Summary

  • An ORM = a schema in TS → typed queries + migrations + injection protection.
  • Drizzle = close to SQL, light; Prisma = a simple API, rich tooling; raw SQL = full control.
  • Migrations are versioned in Git and applied in order, never edited after being applied.

Official sources

Exercises

Was this page helpful?

One tap — no account needed.