TypeScript has enough ORMs now that picking one is its own project. Prisma, Drizzle, TypeORM, Kysely, and MikroORM all take different positions on how much SQL you should see, how schemas should be defined, and how much the ORM should do for you. This guide covers what each one is good at and where it falls short.
| Feature | Prisma | Drizzle | TypeORM | Kysely | MikroORM |
|---|---|---|---|---|---|
| Approach | Schema-first (.prisma DSL) | TypeScript-native schema | Decorator-based entities | Type-safe query builder | Data Mapper + Unit of Work |
| Type Safety | Generated client | SQL-like with full inference | Partial (some any returns) | Compile-time query checking | Good with entity schemas |
| Learning Curve | Low | Low (if you know SQL) | Medium | Medium | High |
| Migration Tooling | Built-in | Drizzle Kit | Built-in | Community (kysely-ctl) | Built-in |
| Bundle Size | Large (engine binary) | Minimal | Medium | Minimal | Medium |
| Raw SQL Escape | $queryRaw | Built-in | QueryBuilder | Native (it is SQL) | em.execute() |
| Database Support | Postgres, MySQL, SQLite, MongoDB, SQL Server | Postgres, MySQL, SQLite | Postgres, MySQL, SQLite, SQL Server, Oracle | Postgres, MySQL, SQLite | Postgres, MySQL, SQLite, MongoDB |
Prisma is the most popular TypeScript ORM by a wide margin. You define your data model in a .prisma file, run prisma generate, and get a fully typed client with autocompletion for every query - including nested relations, filters, and aggregations.
// schema.prisma defines the model, then:
const user = await prisma.user.findUnique({
where: { email: "alice@example.com" },
include: { orders: true },
});
// user is fully typed, including user.orders
Good at: Developer experience. The generated client is the best in class for autocompletion and error messages. Docs are thorough. The ecosystem includes Prisma Studio for visual browsing and Prisma Accelerate for connection pooling. Migrations are built in and work well.
Watch out for: The .prisma schema is its own DSL, not TypeScript. Advanced queries (CTEs, custom joins) require $queryRaw, which drops you out of type safety. The query engine binary adds to deployment size. Under heavy load, the abstraction layer can be a bottleneck.
Try Prisma: Quickstart guide. Works with Encore.ts - Encore handles database provisioning and connections, Prisma provides the query layer. Encore + Prisma docs.
Drizzle is the ORM for people who actually like SQL. Schemas are defined in TypeScript. Queries look like SQL. There's no engine binary, no separate schema language, and almost no runtime overhead.
const users = pgTable("users", {
id: serial("id").primaryKey(),
name: text("name").notNull(),
email: text("email").notNull().unique(),
});
const result = await db
.select()
.from(users)
.where(eq(users.email, "alice@example.com"));
Good at: Staying close to the database. If you know SQL, you know Drizzle. The TypeScript-native schema means no code generation step. Drizzle Kit handles migrations. Works in serverless environments, edge runtimes, and traditional Node.js. Minimal footprint.
Watch out for: The relational query API is still catching up to Prisma's nested reads. Ecosystem is younger - fewer plugins, fewer Stack Overflow answers. Docs are concise, which is either a feature or a drawback depending on your experience level.
Try Drizzle: Quickstart guide. Natural fit with Encore.ts - Drizzle handles schema and queries, Encore handles provisioning, migrations, and connections. Encore + Drizzle docs.
TypeORM has been around since 2016 and uses decorators to define entities - familiar if you come from Java/C# land. It supports both Active Record and Data Mapper patterns.
@Entity()
class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column({ unique: true })
email: string;
}
const user = await userRepository.findOneBy({ email: "alice@example.com" });
Good at: Maturity. Wide database support (Postgres, MySQL, SQLite, SQL Server, Oracle, CockroachDB). Built-in migrations, subscribers, listeners, caching. If your project already uses TypeORM, there's no urgent reason to migrate.
Watch out for: Type inference is weaker than Prisma or Drizzle - some queries return loosely typed results. Development pace has slowed. Relies on experimental TypeScript decorators, which can cause compatibility issues. Eager loading defaults can lead to N+1 problems if you're not careful.
Try TypeORM: Getting started guide. Works with Encore.ts through the SQLDatabase primitive.
Kysely isn't an ORM. It's a type-safe SQL query builder. No entity mapping, no schema definition, no migration system. What it does is let you write SQL in TypeScript with compile-time type checking on every column, table, and join.
const user = await db
.selectFrom("users")
.select(["id", "name", "email"])
.where("email", "=", "alice@example.com")
.executeTakeFirst();
// TypeScript knows exactly which fields are on `user`
Good at: Giving you full control. If you reference a column that doesn't exist, your code won't compile. Zero abstraction over SQL - you see exactly what's hitting the database. Tiny runtime footprint. Queries are composable builder objects.
Watch out for: You define database types manually (or generate them), which gets tedious for large schemas. No built-in migrations. No entity management or relation handling. If you're used to Prisma's nested queries, Kysely's approach is a different mental model.
Try Kysely: Documentation. Pairs well with Encore.ts - Encore handles provisioning and connections, Kysely provides the query layer.
MikroORM borrows patterns from enterprise Java - Data Mapper, Unit of Work, Identity Map. If those terms mean something to you, MikroORM is probably what you want. If they don't, the learning curve is steep.
const user = await em.findOne(User, { email: "alice@example.com" });
user.name = "Alice Smith";
await em.flush(); // Unit of Work batches the update
Good at: Clean architecture. The Unit of Work batches operations into single transactions, reducing round trips. The Identity Map ensures each entity is loaded once per request. Active development, good TypeScript support.
Watch out for: Conceptual overhead. The Unit of Work can lead to surprising flush behavior if you're not careful about entity lifecycle. Smaller community means fewer tutorials and third-party integrations. More boilerplate to set up than Prisma or Drizzle.
Try MikroORM: Quickstart. Works with Encore.ts and its SQLDatabase primitive.
Every ORM in this list handles queries. None of them handle provisioning a database, managing connection strings across environments, running migrations on deploy, or keeping local development in sync with production. That's a separate problem, and it's usually the one that takes the most time.
Encore.ts handles that layer. You declare a database in code:
import { SQLDatabase } from "encore.dev/storage/sqldb";
const db = new SQLDatabase("users", { migrations: "./migrations" });
Encore provisions it automatically - locally and in the cloud. Migrations run on deploy. Connections are managed per environment. Then you plug in whatever ORM you want on top. Drizzle, Prisma, Kysely, raw SQL - Encore doesn't care. It handles the infrastructure; your ORM handles the queries.
This also works well with AI coding agents. Because Encore structures your codebase around services with migration-defined schemas, agents can generate correct queries using your ORM of choice. Encore's MCP server gives agents direct access to the schema, so the generated code matches your actual database.
Drizzle if you want minimal abstraction and SQL-like ergonomics. Best starting point for most new projects.
Prisma if you want the richest tooling and docs. Best for teams that want a guided experience.
Kysely if you want full SQL control with compile-time type safety.
TypeORM if you're already using it. No urgent reason to migrate.
MikroORM if you value Data Mapper and Unit of Work patterns from enterprise ORM design.