Tre ORM’er, ét datasæt (og et valg du ikke gider rulle tilbage)
Jeg sidder med tre terminalvinduer åbne, samme Postgres database kørende i Docker, og tre mapper: prisma-case, typeorm-case, drizzle-case. Samme datamodel. Tre helt forskellige følelser i maven.
Det er den situation, du helst vil undgå at stå i halvvejs inde i et rigtigt projekt.
1. Før du vælger: de 6 spørgsmål der faktisk gør en forskel
Du behøver ikke kende alle features i Prisma, TypeORM og Drizzle for at træffe et fornuftigt valg. Du skal have styr på dit eget problem.
Start her. Svar (ærligt) på de her 6 spørgsmål, før du overhovedet installerer noget:
- Hvor ofte ændrer du dit database-schema?
- Hvor godt kender du SQL i forvejen?
- Hvor mange udviklere er I på projektet?
- Skal andre end TypeScript bruge databasen (andre services, data-team osv.)?
- Hvor vigtig er typesafety for dig i daglig brug?
- Forventer du meget kompleks SQL senere (rapporter, tunge joins, custom indeksering)?
Mini-case: Users + Orders
Jeg bruger den samme lille case til at sammenligne:
- Tabel
users:id,email,name - Tabel
orders:id,user_id,total,created_at - Relation: én bruger har mange ordrer
Og så forestiller vi os tre scenarier:
- Scenario A: Lille projekt, én udvikler, schema ændrer sig tit.
- Scenario B: Mindre team (2-5 devs), løbende features, fælles database.
- Scenario C: Større system, meget rapportering, tunge queries, flere services.
Hele artiklen her bygger på: hvordan de tre ORM’er opfører sig, når vi implementerer netop det lille users+orders setup og begynder at ændre det.
2. Prisma: stærke typer, stærke migrations, stærke meninger
Prisma føles som at få et ekstra lag oven på databasen, der gerne vil tage dig i hånden. Du beskriver dit schema i en egen fil, schema.prisma, og Prisma genererer TypeScript typer og en klient til dig.
Hvordan ser casen ud i Prisma?
Schemaet kunne fx se sådan her:
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
orders Order[]
}
model Order {
id Int @id @default(autoincrement())
userId Int
total Decimal @db.Numeric(10, 2)
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
}
Migrations styres med CLI:
npx prisma migrate dev --name init
Og din kode for at hente en bruger med ordrer kan være så kort her:
const user = await prisma.user.findUnique({
where: { id: 1 },
include: { orders: true },
});
Styrker i praksis
- Typesafety: Prisma genererer typer direkte fra schemaet. Hvis du ændrer en kolonne, får du TypeScript-fejl de steder, du glemmer at opdatere.
- Migrations: Du får migreringsfiler i SQL (eller et mix), og workflowet med
migrate dever ret stramt. Godt til teams. - DX (developer experience): Autocomplete på queries føles som snyd. Du får hjælp til feltnavne, relationer osv.
- Relations: Det er næsten for nemt at traverse relationer.
includeogselecter konsekvent.
Svagheder og friktion
- Opinionated model: Du skriver schema ét sted (Prisma), og den styrer databasen. Hvis du har andre ting, der migrerer databasen, kan det blive rodet.
- Kompleks SQL: Prisma kan en del, men når du rammer kanten, skal du bruge
$queryRawog pludselig selv tænke over SQL injection og typer. - Runtime layer: Prisma er sin egen motor. Den er ikke bare tynd mapping. Det har lidt performance overhead sammenlignet med ren query builder.
Hvornår giver Prisma mening?
Mit eget mønster:
- Jeg vælger næsten altid Prisma hvis:
- Projektet er et klassisk CRUD-agtigt web-API.
- Der er et lille eller mellemstort team.
- Schema ændrer sig tit de første måneder.
- Jeg er mere forsigtig, hvis:
- Databasen skal ejes af et data-team eller flere services på forskellige stacks.
- Jeg ved, der kommer mange tilpassede, tunge rapport-queries.
Hvis du vil have lidt mere om migrations generelt, har jeg skrevet om databaseændringer uden svedige håndflader før. Prisma lægger sig faktisk fint ind i de mønstre.
3. TypeORM: klassisk ORM med dekorationer og overraskelser
TypeORM er mere “ORM som vi kender det fra andre sprog”. Du beskriver dine tabeller som TypeScript-klasser med decorators, og den mapper til databasen.
Hvordan ser casen ud i TypeORM?
Entities kan fx se sådan her:
@Entity()
export class User {
@PrimaryGeneratedColumn()
id!: number;
@Column({ unique: true })
email!: string;
@Column({ nullable: true })
name?: string;
@OneToMany(() => Order, (order) => order.user)
orders!: Order[];
}
@Entity()
export class Order {
@PrimaryGeneratedColumn()
id!: number;
@ManyToOne(() => User, (user) => user.orders)
@JoinColumn({ name: "user_id" })
user!: User;
@Column({ type: "decimal", precision: 10, scale: 2 })
total!: string;
@CreateDateColumn({ name: "created_at" })
createdAt!: Date;
}
Query:
const userRepo = dataSource.getRepository(User);
const user = await userRepo.findOne({
where: { id: 1 },
relations: ["orders"],
});
Styrker i praksis
- Alt i TypeScript: Ingen separat schema-fil. Entities er både typerne og mappet til DB.
- Query builder: TypeORM har en relativt fleksibel query builder til mere komplekse queries.
- Flere databaser: Understøtter flere databaser og er ret udbredt i Node/TS-verdenen.
Typiske faldgruber
Her er hvad jeg oftest ser gå galt:
- Sync vs migrations: TypeORM har en
synchronize-option, som ændrer schema direkte fra dine entities. Det virker magisk i dev, men er ret farligt i prod. Bruger du ikke eksplicitte migrations, kan schema glide stille og roligt væk fra det, du tror. - Typer hvor alt er lidt for åbent: Typerne er ikke nær så skarpe som Prisma eller Drizzle. Du kan nemt ende med runtime-fejl, som TypeScript ikke fangede.
- Relationer og performance: Lazy vs eager loading, relations, cascade options osv. kræver disciplin, hvis det ikke skal ende i N+1 queries eller uventet sletning.
Hvornår giver TypeORM mening?
Jeg ville overveje TypeORM hvis:
- Du arbejder i et miljø, hvor der allerede er meget TypeORM, og teamet kender det.
- Du har brug for et mere klassisk ORM-mønster med entities og inheritance.
- Du har et projekt, der ikke nødvendigvis er ren CRUD, men heller ikke super meget data science eller tunge queries.
Personligt vælger jeg det sjældent til nye projekter. Ikke fordi det er “dårligt”, men fordi kombinationen af migrations, typesafety og moderne DX bare er bedre andre steder.
4. Drizzle: query builder med stærke typer og lidt mindre magi
Drizzle kaldes ofte en ORM, men føles mere som en type-skarp SQL query builder med migrations ovenpå. Du beskriver din schema i TypeScript, og Drizzle bruger det til både typer, migrations og queries.
Hvordan ser casen ud i Drizzle?
import { pgTable, serial, varchar, integer, numeric, timestamp } from "drizzle-orm/pg-core";
export const users = pgTable("users", {
id: serial("id").primaryKey(),
email: varchar("email", { length: 255 }).notNull().unique(),
name: varchar("name", { length: 255 }),
});
export const orders = pgTable("orders", {
id: serial("id").primaryKey(),
userId: integer("user_id").references(() => users.id).notNull(),
total: numeric("total", { precision: 10, scale: 2 }).notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
Query til at hente en bruger med ordrer kunne være:
const result = await db
.select()
.from(users)
.leftJoin(orders, eq(orders.userId, users.id))
.where(eq(users.id, 1));
Du får så et resultat med en tuple-agtig struktur, hvor du tydeligt kan se, hvad der kommer fra hvor.
Styrker i praksis
- Typesafety helt ned i kolonnerne: Definitionen af tabellerne er TypeScript. Drizzle sørger for, at dine queries bruger de rigtige felter og typer.
- Nær på SQL: Query builderen føles meget som struktureret SQL. Det gør det lettere at reasonere om performance.
- Migrations: Der er et indbygget migrationssystem, der spiller godt med schema-definitionen.
- Lav runtime-magi: Ingen stor runtime motor. Det er mest typer og en tynd builder.
Svagheder og friktion
- Mindre “klik og kør” end Prisma: Du får ikke en stor, venlig klient med GUI, relations includes osv. Du skriver mere selv.
- Relations og ergonomi: Relationer er der (med relation helpers osv.), men føles mere lav-niveau end i Prisma.
- Dokumentation og eksempler: Det er blevet bedre, men Prisma vinder stadig på mængden af eksempler og community-svar.
Hvornår giver Drizzle mening?
Jeg vælger Drizzle når:
- Jeg har det ok med at tænke i SQL.
- Jeg gerne vil have meget præcis kontrol over queries og performance.
- Projektet potentielt skal bruge mere avancerede queries senere.
Og når jeg ikke har lyst til at indføre et stort ekstra lag oven på databasen, men stadig gerne vil have stærke typer og migrations.
5. Migrations i praksis: hvor teams går i stykker
Migrations er ofte dér, hvor forskellene for alvor kan mærkes i et team. Ikke i hello world, men når du er på migration nummer 27 sent torsdag aften.
Prisma migrations
Workflowet er typisk:
- Ret i
schema.prisma. - Kør
npx prisma migrate dev --name noget_beskrivende. - Prisma genererer SQL-migrationer og opdaterer databasen lokalt.
Du ender med migreringsfiler i repoet, som kan køres i CI/CD. Konflikter håndteres ved at merge migrations og køre dem i samme rækkefølge.
Fordel: du har ét sandhedssted for schema (Prisma-filen), og migrations følger automatisk. Ulempe: hvis andre systemer også ændrer schema, skal I koordinere meget stramt.
TypeORM migrations
To typiske veje:
- Auto-generate migrations baseret på entity-ændringer.
- Skrive migrations i hånden (enten SQL eller TypeScript).
Problemet opstår ofte, når folk stoler for meget på auto-generation uden at kigge nok på output. Så får du migrations, som er svære at læse, eller som forsøger at gøre mere end nødvendigt.
Det kan fungere fint, men det kræver disciplin, og du bør slå synchronize fra i prod ret hurtigt.
Drizzle migrations
Drizzle genererer migrations ud fra dine TypeScript schemas. Workflowet minder lidt om Prisma:
- Ret i schema (TypeScript tabeldefinitioner).
- Generer migrations med CLI.
- Tjek SQL-filerne ind i repo.
Forskellen er, at du er tættere på SQL hele vejen. Det gør det nemmere at forstå, hvad der sker, hvis du i forvejen tænker meget i SQL.
Hvor går det typisk galt?
På tværs af alle tre:
- To udviklere ændrer schema på hver sin branch uden at merge migrations ordentligt.
- Migrations bliver ikke kørt i prod i samme rækkefølge som i dev.
- Der laves breaking schema-ændringer uden plan for data migration.
Her hjælper værktøjet dig lidt, men det er primært proces. I en tidligere artikel om databaseændringer handler det faktisk mest om netop det: små skridt, backwards compatibility, tidlig test.
6. Typesafety og runtime-fejl: hvad ser du reelt i editoren?
Typesafety lyder pænt, men spørgsmålet er: hvor mange fejl fanger dit værktøj, før koden kører?
Prisma og typer
Eksempel på query, der bruger selection:
const user = await prisma.user.findUnique({
where: { id: 1 },
select: { id: true, email: true },
});
// user har type: { id: number; email: string } | null
Hvis du prøver at tilgå user.name uden at have valgt det, brokker TypeScript sig (efter null-check). Det er ret stærkt.
Hvis du ændrer feltnavn i schemaet, vil alle queries, der bruger det gamle navn, give compile-fejl. Det er præcis dét, man gerne vil have.
TypeORM og typer
TypeORM giver dig typer for entities, men ikke nær så nuancerede for queries. Fx:
const user = await userRepo.findOne({ where: { id: 1 } });
// user: User | null
Det er fint, men hvis du laver en custom select med query builder, bliver typerne hurtigt brede eller any-agtige, med mindre du er meget disciplineret.
Drizzle og typer
Drizzle går ret langt med typerne. Et simpelt select:
const result = await db
.select({ id: users.id, email: users.email })
.from(users)
.where(eq(users.id, 1));
// result: { id: number; email: string }[]
Her har du fuld kontrol over, hvad du vil have ud, og typerne følger præcist med. Hvis du refererer til en kolonne, der ikke findes, får du compile-fejl.
Er stærke typer altid en fordel?
Ja, næsten. Men de kan gøre refactors lidt tungere, fordi du får fejl mange steder, når du ændrer schema. Jeg ser det som et plus, men du skal være klar på, at “bare lige at omdøbe et felt” kan være en lille opgave.
Hvis jeg arbejder på et projekt, hvor mange hurtigt-throwaway queries bliver skrevet, og schema er ret fleksibelt, kan det være, at de stærkeste typer føles som lidt ekstra friktion. Men for de fleste backend-projekter er det et plus.
7. Performance og kompleks SQL: hvor har du din escape hatch?
Før eller siden står du med en query, der ikke lige passer ind i de pæne abstractions. Her er spørgsmålet: hvor nemt er det at hoppe ned på rå SQL uden at ødelægge alt det gode?
Prisma: $queryRaw og $executeRaw
Eksempel:
const result = await prisma.$queryRaw<{ userId: number; total: number }>`
SELECT user_id AS "userId", SUM(total) AS total
FROM orders
GROUP BY user_id
HAVING SUM(total) > 1000
`;
Du får SQL direkte, men typerne skal du selv behejle via generics, og du skal være opmærksom på SQL injection. Prisma har tagged template helpers, men du er tættere på metal.
TypeORM: query builder og rå SQL
TypeORMs query builder:
const result = await dataSource
.createQueryBuilder("orders", "o")
.select("o.user_id", "userId")
.addSelect("SUM(o.total)", "total")
.groupBy("o.user_id")
.having("SUM(o.total) > :min", { min: 1000 })
.getRawMany();
Typerne er her ikke fantastiske. Ofte ender du med any eller generelle rekord-typer.
Du kan også køre rå SQL med query-metoder direkte på data source eller connection.
Drizzle: tættere på SQL i forvejen
Samme query kunne se nogenlunde sådan her:
const result = await db
.select({ userId: orders.userId, total: sql`SUM(${orders.total})`.as("total") })
.from(orders)
.groupBy(orders.userId)
.having(sql`SUM(${orders.total}) > ${1000}`);
Her er escape hatch mere integreret. Du kan bruge sql-tagget til custom udtryk, men stadig beholde en del af type-sikkerheden.
Hvad med ren performance?
Ren RPS-benchmark uden kontekst er ikke særlig informativ. De tre værktøjer laver alle SQL. Forskellene kommer mest fra:
- Hvor mange queries der laves (relation loading, N+1 osv.).
- Om du har gode indekser.
- Om abstractions gør det svært at se, at du fx sorterer på et non-index felt.
Her er Drizzle og ren SQL/knex-agtige løsninger ofte lidt lettere at optimere, fordi du er vant til at tænke i SQL. Prisma kan sagtens performe godt, men du skal holde øje med, hvad dine include-kæder faktisk laver under motorhjelmen.
8. Beslutningstabel: scenarier og anbefaling
Nu til det, de fleste egentlig leder efter: “hvilken skal jeg vælge til mit projekt?”. Her er en simpel tabel og lidt kommenteret anbefaling.
| Scenario | Kendetegn | Mit valg | Hvorfor |
|---|---|---|---|
| A: Lille CRUD-app | 1-2 devs, hyppige schema-ændringer, klassisk web-API | Prisma | Hurtigt at komme i gang, stærk DX, migrations og typer hjælper dig meget. |
| B: Mindre produkt-team | 3-6 devs, delt database, feature-udvikling over tid | Prisma eller Drizzle | Prisma hvis flest ikke er SQL-stærke, Drizzle hvis I har SQL-komfort og vil tættere på queries. |
| C: Data-tung app | Mange rapporter, tunge queries, flere services deler DB | Drizzle | Nemt at skrive avancerede queries, stærke typer, lav magi omkring runtime. |
| D: Eksisterende monolit | Arv, meget TypeORM i forvejen, blandede teams | TypeORM, men stramt | Brug det, der er i huset, men med migrations og klare regler for sync og relations. |
| E: Læringsprojekt | Du vil lære database-arbejde, SQL og Typescript | Drizzle eller ren SQL | Du lærer mest af et værktøj, der ikke skjuler SQL fuldstændig. |
Hvis jeg sidder med en standard TypeScript/Node backend i 2026 og ingen andre krav, vælger jeg oftest:
- Prisma, hvis projektet er produkt-agtigt og relations-tungt.
- Drizzle, hvis jeg forventer meget skæve rapporter og komplekse queries.
9. Startpakker: sådan kan du strukturere dit projekt
Her kommer den del, hvor vi gør det lidt mere konkret. Hvis du vil prøve de tre af i et lille projekt, kan du genbruge meget af samme struktur.
Fælles grundstruktur
/src
/db
schema.* // afhænger af ORM
client.ts
/modules
/users
users.service.ts
users.routes.ts
/orders
orders.service.ts
orders.routes.ts
server.ts
/drizzle
/prisma
/ormconfig.* // for TypeORM
Pointen er: hold database-laget samlet. Din HTTP-del (routes, controllers osv.) bør ikke kende til, hvilket ORM du bruger, kun til de services, der leverer data.
Scripts og workflows
Prisma
{
"scripts": {
"dev": "ts-node-dev src/server.ts",
"db:migrate": "prisma migrate dev",
"db:deploy": "prisma migrate deploy",
"db:studio": "prisma studio"
}
}
Typisk loop for en feature:
- Opdatér
schema.prisma. - Kør
npm run db:migrate -- --name add_status_to_orders. - Brug de nye felter i kode (TS hjælper dig).
TypeORM
{
"scripts": {
"dev": "ts-node-dev src/server.ts",
"db:migrate:generate": "typeorm migration:generate -n",
"db:migrate": "typeorm migration:run",
"db:migrate:revert": "typeorm migration:revert"
}
}
Jeg vil anbefale, at du:
- Slår
synchronizefra i alt andet end evt. små lokale spikes. - Gennemgår genererede migrations, og retter dem til, så de er læsbare.
Drizzle
{
"scripts": {
"dev": "ts-node-dev src/server.ts",
"db:generate": "drizzle-kit generate:pg",
"db:migrate": "drizzle-kit migrate:pg"
}
}
Workflow:
- Ret dine tabel-definitioner i
src/db/schema.ts. - Kør
npm run db:generatefor at lave migrationer. - Kør
npm run db:migratefor at anvende dem.
Mini-øvelse: samme feature i alle tre
Hvis du vil mærke forskellen på din egen krop, kan du lave den her lille øvelse:
- Start tre små projekter med samme Node/TS-setup.
- Implementér users+orders case i alle tre ORM’er.
- Tilføj feltet
statustilorders(fx “pending”, “paid”). - Lav en endpoint, der returnerer alle ordrer pr. status pr. bruger.
- Mål: hvor hurtigt får du migrations + typer + queries til at gå op?
Det giver en ret ærlig fornemmelse af, hvad du foretrækker. Og ja, det tager en aften, men det er stadig billigere end at skifte ORM efter 6 måneder i et rigtigt projekt.
10. Hvad jeg selv ville gøre i dit sted
Hvis vi nu forestiller os, at du sidder med et moderne Node/TypeScript-projekt, Postgres, og en rimelig klassisk web- eller mobilbackend, så er det her min standard-anbefaling:
- Du er begynder/let øvet: Tag Prisma. Du får mest hjælp af editoren, migrations er til at forstå, og du kan fokusere på at bygge features.
- Du er tryg ved SQL og vil nørde performance: Tag Drizzle. Du får stærke typer uden en tung runtime, og du lærer samtidig mere om databasen.
- Du arver et TypeORM-projekt: Lær TypeORM godt nok til at spille med, men indfør klare regler for migrations, og undgå magiske sync-funktioner.
Og hvis du er helt i tvivl, så vælg én, og forpligt dig til at blive i den i mindst ét mindre projekt. Det værste er ikke at vælge “forkert” mellem Prisma, TypeORM og Drizzle. Det værste er at skifte ORM midt i en kodebase, der allerede har 40+ migrations.
Jeg har prøvet at lave det skift én gang. Det føltes lidt som at repotte en stor plante i for lille potte med kat på bordet og jord overalt. Jeg kan ikke anbefale det som hobby.









1 kommentar