TypeORM
This page compares Prisma ORM and TypeORM. If you want to learn how to migrate from TypeORM to Prisma ORM, check out this guide.
TypeORM vs Prisma ORM
While Prisma ORM and TypeORM solve similar problems, they work in very different ways.
TypeORM is a traditional ORM which maps tables to model classes. These model classes can be used to generate SQL migrations. Instances of the model classes then provide an interface for CRUD queries to an application at runtime.
Prisma ORM is a new kind of ORM that mitigates many problems of traditional ORMs, such as bloated model instances, mixing business with storage logic, lack of type-safety or unpredictable queries caused e.g. by lazy loading.
It uses the Prisma schema to define application models in a declarative way. Prisma Migrate then allows to generate SQL migrations from the Prisma schema and executes them against the database. CRUD queries are provided by Prisma Client, a lightweight and entirely type-safe database client for Node.js and TypeScript.
API design & Level of abstraction
TypeORM and Prisma ORM operate on different levels of abstraction. TypeORM is closer to mirroring SQL in its API while Prisma Client provides a higher-level abstraction that was carefully designed with the common tasks of application developers in mind. Prisma ORM's API design heavily leans on the idea of making the right thing easy.
While Prisma Client operates at a higher level of abstraction, it strives to expose the full power of the underlying database, allowing you to drop down to raw SQL at any time if your use case requires it.
The following sections examine a few examples for how Prisma ORM's and TypeORM's APIs differ in certain scenarios and what the rationale of Prisma ORM's API design is in these cases.
Filtering
TypeORM primarily leans on SQL operators for filtering lists or records, e.g. with the find
method. Prisma ORM on the other hand, provides a more generic set of operators that are intuitive to use. It should also be noted that, as explained in the type-safety section below, TypeORM loses type-safety in filter queries in many scenarios.
A good example of how the filtering APIs of both TypeORM and Prisma ORM differ is by looking at string
filters. While TypeORM primarily provides the filter based on the ILike
operator which comes directly from SQL, Prisma ORM provides more specific operators that developers can use, e.g.: contains
, startsWith
and endsWith
.
const posts = await prisma.post.findMany({
where: {
title: 'Hello World',
},
})
const posts = await postRepository.find({
where: {
title: ILike('Hello World'),
},
})
const posts = await prisma.post.findMany({
where: {
title: { contains: 'Hello World' },
},
})
const posts = await postRepository.find({
where: {
title: ILike('%Hello World%'),
},
})
const posts = await prisma.post.findMany({
where: {
title: { startsWith: 'Hello World' },
},
})
const posts = await postRepository.find({
where: {
title: ILike('Hello World%'),
},
})
const posts = await prisma.post.findMany({
where: {
title: { endsWith: 'Hello World' },
},
})
const posts = await postRepository.find({
where: {
title: ILike('%Hello World'),
},
})
Pagination
TypeORM only offers limit-offset pagination while Prisma ORM conveniently provides dedicated APIs for both limit-offset but also cursor-based. You can learn more about both approaches in the Pagination section of the docs or in the API comparison below.
Relations
Working with records that are connected via foreign keys can become very complex in SQL. Prisma ORM's concept of virtual relation field enables an intuitive and convenient way for application developers to work with related data. Some benefits of Prisma ORM's approach are:
- traversing relationships via the fluent API (docs)
- nested writes that enable updating/creating connected records (docs)
- applying filters on related records (docs)
- easy and type-safe querying of nested data without worrying about JOINs (docs)
- creating nested TypeScript typings based on models and their relations (docs)
- intuitive modeling of relations in the data model via relation fields (docs)
- implicit handling of relation tables (also sometimes called JOIN, link, pivot or junction tables) (docs)
Data modeling and migrations
Prisma models are defined in the Prisma schema while TypeORM uses classes and experimental TypeScript decorators for model definitions. With the Active Record ORM pattern, TypeORM's approach often leads to complex model instances that are becoming hard to maintain as an application grows.
Prisma ORM on the other hand generates a lightweight database client that exposes a tailored and fully type-safe API to read and write data for the models that are defined in the Prisma schema, following the DataMapper ORM pattern rather than Active Record.
Prisma ORM's DSL for data modeling is lean, simple and intuitive to use. When modeling data in VS Code, you can further take advantage of Prisma ORM's powerful VS Code extension with features like autocompletion, quick fixes, jump to definition and other benefits that increase developer productivity.
model User {
id Int @id @default(autoincrement())
name String?
email String @unique
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
authorId Int?
author User? @relation(fields: [authorId], references: [id])
}
import {
Entity,
PrimaryGeneratedColumn,
Column,
OneToMany,
ManyToOne,
} from 'typeorm'
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number
@Column({ nullable: true })
name: string
@Column({ unique: true })
email: string
@OneToMany((type) => Post, (post) => post.author)
posts: Post[]
}
@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number
@Column()
title: string
@Column({ nullable: true })
content: string
@Column({ default: false })
published: boolean
@ManyToOne((type) => User, (user) => user.posts)
author: User
}
Migrations work in similar fashions in TypeORM and Prisma ORM. Both tools follow the approach of generating SQL files based on the provided model definitions and provide a CLI to execute them against the database. The SQL files can be modified before the migrations are executed so that any custom database operation can be performed with either migration system.
Type safety
TypeORM has been one of the first ORMs in the Node.js ecosystem to fully embrace TypeScript and has done a great job in enabling developers to get a certain level of type safety for their database queries.
However, there are numerous situations where the type safety guarantees of TypeORM fall short. The following sections describe the scenarios where Prisma ORM can provide stronger guarantees for the types of query results.
Selecting fields
This section explains the differences in type safety when selecting a subset of a model's fields in a query.
TypeORM
TypeORM provides a select
option for its find
methods (e.g. find
, findByIds
, findOne
, ...), for example:
- `find` with `select`
- Model
const postRepository = getManager().getRepository(Post)
const publishedPosts: Post[] = await postRepository.find({
where: { published: true },
select: ['id', 'title'],
})
@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number
@Column()
title: string
@Column({ nullable: true })
content: string
@Column({ default: false })
published: boolean
@ManyToOne((type) => User, (user) => user.posts)
author: User
}
While each object in the returned publishedPosts
array only carries the selected id
and title
properties at runtime, the TypeScript compiler doesn't have any knowledge of this. It will allow you to access any other properties defined on the Post
entity after the query, for example:
const post = publishedPosts[0]
// The TypeScript compiler has no issue with this
if (post.content.length > 0) {
console.log(`This post has some content.`)
}
This code will result in an error at runtime:
TypeError: Cannot read property 'length' of undefined
The TypeScript compiler only sees the Post
type of the returned objects, but it doesn't know about the fields that these objects actually carry at runtime. It therefore can't protect you from accessing fields that have not been retrieved in the database query, resulting in a runtime error.
Prisma ORM
Prisma Client can guarantee full type safety in the same situation and protects you from accessing fields that were not retrieved from the database.
Consider the same example with a Prisma Client query:
- `findMany` with `select`
- Model
const publishedPosts = await prisma.post.findMany({
where: { published: true },
select: {
id: true,
title: true,
},
})
const post = publishedPosts[0]
// The TypeScript compiler will not allow this
if (post.content.length > 0) {
console.log(`This post has some content.`)
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
authorId Int?
author User? @relation(fields: [authorId], references: [id])
}
In this case, the TypeScript compiler will throw the following error already at compile-time:
[ERROR] 14:03:39 ⨯ Unable to compile TypeScript:
src/index.ts:36:12 - error TS2339: Property 'content' does not exist on type '{ id: number; title: string; }'.
42 if (post.content.length > 0) {
This is because Prisma Client generates the return type for its queries on the fly. In this case, publishedPosts
is typed as follows:
const publishedPosts: {
id: number
title: string
}[]
It therefore is impossible for you to accidentally access a property on a model that has not been retrieved in a query.
Loading relations
This section explains the differences in type safety when loading relations of a model in a query. In traditional ORMs, this is sometimes called eager loading.
TypeORM
TypeORM allows to eagerly load relations from the database via the relations
option that can be passed to its find
methods.
Consider this example:
- `find` with `relations`
- Models
const postRepository = getManager().getRepository(Post)
const publishedPosts: Post[] = await postRepository.find({
where: { published: true },
relations: ['author'],
})
@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number
@Column()
title: string
@Column({ nullable: true })
content: string
@Column({ default: false })
published: boolean
@ManyToOne((type) => User, (user) => user.posts)
author: User
}
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number
@Column({ nullable: true })
name: string
@Column({ unique: true })
email: string
@OneToMany((type) => Post, (post) => post.author)
posts: Post[]
}
Unlike with select
, TypeORM does not provide autocompletion, nor any type-safety for the strings that are passed to the relations
option. This means, the TypeScript compiler is not able to catch any typos that are made when querying these relations. For example, it would allow for the following query:
const publishedPosts: Post[] = await postRepository.find({
where: { published: true },
// this query would lead to a runtime error because of a typo
relations: ['authors'],
})
This subtle typo would now lead to the following runtime error:
UnhandledPromiseRejectionWarning: Error: Relation "authors" was not found; please check if it is correct and really exists in your entity.
Prisma ORM
Prisma ORM protects you from mistakes like this and thus eliminates a whole class of errors that can occur in your application at runtime. When using include
to load a relation in a Prisma Client query, you can not only take advantage of autocompletion to specify the query, but the result of the query will also be properly typed:
- `find` with `relations`
- Models
const publishedPosts = await prisma.post.findMany({
where: { published: true },
include: { author: true },
})
model User {
id Int @id @default(autoincrement())
name String?
email String @unique
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
authorId Int?
author User? @relation(fields: [authorId], references: [id])
}
Again, the type of publishedPosts
is generated on the fly and looks as follows:
const publishedPosts: (Post & {
author: User
})[]
For reference, this is what the User
and Post
types look like that Prisma Client generates for your Prisma models:
- `User`
- `Post`
// Generated by Prisma ORM
export type User = {
id: number
name: string | null
email: string
}
// Generated by Prisma ORM
export type Post = {
id: number
title: string
content: string | null
published: boolean
authorId: number | null
}
Filtering
This section explains the differences in type safety when filtering a list of records using where
.
TypeORM
TypeORM allows to pass a where
option to its find
methods to filter the list of returned records according to specific criteria. These criteria can be defined with respect to a model's properties.
Loosing type-safety using operators
Consider this example:
- `find` with `select`
- Model
const postRepository = getManager().getRepository(Post)
const publishedPosts: Post[] = await postRepository.find({
where: {
published: true,
title: ILike('Hello World'),
views: MoreThan(0),
},
})
@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number
@Column()
title: string
@Column({ nullable: true })
content: string
@Column({ nullable: true })
views: number
@Column({ default: false })
published: boolean
@ManyToOne((type) => User, (user) => user.posts)
author: User
}
This code runs properly and produces a valid query at runtime. However, the where
option is not really type-safe in various different scenarios. When using a FindOperator
like ILike
or MoreThan
that only work for specific types (ILike
works for strings, MoreThan
for numbers), you're losing the guarantee of providing the correct type for the model's field.
For example, you can provide a string to the MoreThan
operator. The TypeScript compiler will not complain and your application will only fail at runtime:
const postRepository = getManager().getRepository(Post)
const publishedPosts: Post[] = await postRepository.find({
where: {
published: true,
title: ILike('Hello World'),
views: MoreThan('test'),
},
})
The code above results in a runtime error that the TypeScript compiler doesn't catch for you:
error: error: invalid input syntax for type integer: "test"
Specifying non-existing properties
Also note that the TypeScript compiler allows you to specify properties on the where
option that don't exist on your models – again resulting in runtime errors:
const publishedPosts: Post[] = await postRepository.find({
where: {
published: true,
title: ILike('Hello World'),
viewCount: 1,
},
})
In this case, your application again fails at runtime with the following error:
EntityColumnNotFound: No entity column "viewCount" was found.
Prisma ORM
Both filtering scenarios that are problematic with TypeORM in terms of type-safety are covered by Prisma ORM in a fully type-safe way.
Type-safe usage of operators
With Prisma ORM, the TypeScript compiler enforces the correct usage of an operator per field:
const publishedPosts = await prisma.post.findMany({
where: {
published: true,
title: { contains: 'Hello World' },
views: { gt: 0 },
},
})
It would not be allowed to specify the same problematic query shown above with Prisma Client:
const publishedPosts = await prisma.post.findMany({
where: {
published: true,
title: { contains: 'Hello World' },
views: { gt: 'test' }, // Caught by the TypeScript compiler
},
})
The TypeScript compiler would catch this and throw the following error to protect you from a runtime failure of the app:
[ERROR] 16:13:50 ⨯ Unable to compile TypeScript:
src/index.ts:39:5 - error TS2322: Type '{ gt: string; }' is not assignable to type 'number | IntNullableFilter'.
Type '{ gt: string; }' is not assignable to type 'IntNullableFilter'.
Types of property 'gt' are incompatible.
Type 'string' is not assignable to type 'number'.
42 views: { gt: "test" }
Type-safe definition of filters as model properties
With TypeORM, you are able to specify a property on the where
option that doesn't map to a model's field. In the above example, filtering for viewCount
therefore led to a runtime error because the field actually is called views
.
With Prisma ORM, the TypeScript compiler will not allow to reference any properties inside of where
that don't exist on the model:
const publishedPosts = await prisma.post.findMany({
where: {
published: true,
title: { contains: 'Hello World' },
viewCount: { gt: 0 }, // Caught by the TypeScript compiler
},
})
Again, the TypeScript compiler complains with the following message to protect you from your own mistakes:
[ERROR] 16:16:16 ⨯ Unable to compile TypeScript:
src/index.ts:39:5 - error TS2322: Type '{ published: boolean; title: { contains: string; }; viewCount: { gt: number; }; }' is not assignable to type 'PostWhereInput'.
Object literal may only specify known properties, and 'viewCount' does not exist in type 'PostWhereInput'.
42 viewCount: { gt: 0 }
Creating new records
This section explains the differences in type safety when creating new records.
TypeORM
With TypeORM, there are two main ways to create new records in the database: insert
and save
. Both methods allow developers to submit data that can lead to runtime errors when required fields are not provided.
Consider this example:
- Create with `save`
- Create with `insert`
- Model
const userRepository = getManager().getRepository(User)
const newUser = new User()
newUser.name = 'Alice'
userRepository.save(newUser)
const userRepository = getManager().getRepository(User)
userRepository.insert({
name: 'Alice',
})
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number
@Column({ nullable: true })
name: string
@Column({ unique: true })
email: string
@OneToMany((type) => Post, (post) => post.author)
posts: Post[]
}
No matter if you're using save
or insert
for record creation with TypeORM, you will get the following runtime error if you forget to provide the value for a required field:
QueryFailedError: null value in column "email" of relation "user" violates not-null constraint
The email
field is defined as required on the User
entity (which is enforced by a NOT NULL
constraint in the database).
Prisma ORM
Prisma ORM protects you from these kind of mistakes by enforcing that you submit values for all required fields of a model.
For example, the following attempt to create a new User
where the required email
field is missing would be caught by the TypeScript compiler:
- Create with `create`
- Model
const newUser = await prisma.user.create({
data: {
name: 'Alice',
},
})
model User {
id Int @id @default(autoincrement())
name String?
email String @unique
}
It would lead to the following compile-time error:
[ERROR] 10:39:07 ⨯ Unable to compile TypeScript:
src/index.ts:39:5 - error TS2741: Property 'email' is missing in type '{ name: string; }' but required in type 'UserCreateInput'.
API comparison
Fetching single objects
Prisma ORM
const user = await prisma.user.findUnique({
where: {
id: 1,
},
})
TypeORM
const userRepository = getRepository(User)
const user = await userRepository.findOne(id)
Fetching selected scalars of single objects
Prisma ORM
const user = await prisma.user.findUnique({
where: {
id: 1,
},
select: {
name: true,
},
})
TypeORM
const userRepository = getRepository(User)
const user = await userRepository.findOne(id, {
select: ['id', 'email'],
})
Fetching relations
Prisma ORM
- Using include
- Fluent API
const posts = await prisma.user.findUnique({
where: {
id: 2,
},
include: {
post: true,
},
})
const posts = await prisma.user
.findUnique({
where: {
id: 2,
},
})
.post()
Note:
select
return auser
object that includes apost
array, whereas the fluent API only returns apost
array.
TypeORM
- Using `relations`
- Using `JOIN`
- Using earger relations
const userRepository = getRepository(User)
const user = await userRepository.findOne(id, {
relations: ['posts'],
})
const userRepository = getRepository(User)
const user = await userRepository.findOne(id, {
join: {
alias: 'user',
leftJoinAndSelect: {
posts: 'user.posts',
},
},
})
const userRepository = getRepository(User)
const user = await userRepository.findOne(id)
Filtering for concrete values
Prisma ORM
const posts = await prisma.post.findMany({
where: {
title: {
contains: 'Hello',
},
},
})
TypeORM
const userRepository = getRepository(User)
const users = await userRepository.find({
where: {
name: 'Alice',
},
})
Other filter criteria
Prisma ORM
Prisma ORM generates many additional filters that are commonly used in modern application development.
TypeORM
TypeORM provides built-in operators that can be used to create more complex comparisons
Relation filters
Prisma ORM
Prisma ORM lets you filter a list based on a criteria that applies not only to the models of the list being retrieved, but to a relation of that model.
For example, the following query returns users with one or more posts with "Hello" in the title:
const posts = await prisma.user.findMany({
where: {
Post: {
some: {
title: {
contains: 'Hello',
},
},
},
},
})
TypeORM
TypeORM doesn't offer a dedicated API for relation filters. You can get similar functionality by using the QueryBuilder
or writing the queries by hand.
Pagination
Prisma ORM
Cursor-style pagination:
const page = await prisma.post.findMany({
before: {
id: 242,
},
last: 20,
})
Offset pagination:
const cc = await prisma.post.findMany({
skip: 200,
first: 20,
})
TypeORM
const postRepository = getRepository(Post)
const posts = await postRepository.find({
skip: 5,
take: 10,
})
Creating objects
Prisma ORM
const user = await prisma.user.create({
data: {
email: 'alice@prisma.io',
},
})
TypeORM
- Using `save`
- Using `create`
- Using `insert`
const user = new User()
user.name = 'Alice'
user.email = 'alice@prisma.io'
await user.save()
const userRepository = getRepository(User)
const user = await userRepository.create({
name: 'Alice',
email: 'alice@prisma.io',
})
await user.save()
const userRepository = getRepository(User)
await userRepository.insert({
name: 'Alice',
email: 'alice@prisma.io',
})
Updating objects
Prisma ORM
const user = await prisma.user.update({
data: {
name: 'Alicia',
},
where: {
id: 2,
},
})
TypeORM
const userRepository = getRepository(User)
const updatedUser = await userRepository.update(id, {
name: 'James',
email: 'james@prisma.io',
})
Deleting objects
Prisma ORM
const deletedUser = await prisma.user.delete({
where: {
id: 10,
},
})
TypeORM
- Using `delete`
- Using `remove`
const userRepository = getRepository(User)
await userRepository.delete(id)
const userRepository = getRepository(User)
const deletedUser = await userRepository.remove(user)
Batch updates
Prisma ORM
const user = await prisma.user.updateMany({
data: {
name: 'Published author!',
},
where: {
Post: {
some: {
published: true,
},
},
},
})
TypeORM
You can use the query builder to update entities in your database.
Batch deletes
Prisma ORM
const users = await prisma.user.deleteMany({
where: {
id: {
in: [1, 2, 6, 6, 22, 21, 25],
},
},
})
TypeORM
- Using `delete`
- Using `remove`
const userRepository = getRepository(User)
await userRepository.delete([id1, id2, id3])
const userRepository = getRepository(User)
const deleteUsers = await userRepository.remove([user1, user2, user3])
Transactions
Prisma ORM
const user = await prisma.user.create({
data: {
email: 'bob.rufus@prisma.io',
name: 'Bob Rufus',
Post: {
create: [
{ title: 'Working at Prisma' },
{ title: 'All about databases' },
],
},
},
})
TypeORM
await getConnection().$transaction(async (transactionalEntityManager) => {
const user = getRepository(User).create({
name: 'Bob',
email: 'bob@prisma.io',
})
const post1 = getRepository(Post).create({
title: 'Join us for GraphQL Conf in 2019',
})
const post2 = getRepository(Post).create({
title: 'Subscribe to GraphQL Weekly for GraphQL news',
})
user.posts = [post1, post2]
await transactionalEntityManager.save(post1)
await transactionalEntityManager.save(post2)
await transactionalEntityManager.save(user)
})