Skip to main content
Version: 3.0 Alpha

Overview

ZenStack ORM is a schema-first ORM for modern TypeScript applications. It learnt from the prior arts and strives to provide an awesome developer experience by combining the best ingredients into a cohesive package.

Key Features

Prisma-compatible query API

ZenStack v3 is inspired by Prisma ORM but it has a completely different implementation (based on Kysely). On the surface, it replicated Prisma ORM's query API so that you can use it pretty much as a drop-in replacement. Even if you're not a Prisma user, the query API is very intuitive and easy to learn.

await db.user.findMany({
where: {
email: {
endsWith: 'zenstack.dev',
},
},
orderBy: {
createdAt: 'desc',
},
include: { posts: true }
});

Low-level query builder powered by Kysely

ORM APIs are concise and pleasant, but they have their limitations. When you need extra power, you can fall back to the low-level query builder API powered by Kysely - limitless expressiveness, full type safety, and zero extra setup.

await db.$qb
.selectFrom('User')
.leftJoin('Post', 'Post.authorId', 'User.id')
.select(['User.id', 'User.email', 'Post.title'])
.execute();

Access control

ZenStack ORM comes with a powerful built-in access control system. You can define access rules right inside the schema. The rules are enforced at runtime via query injection, so it doesn't rely on any database specific row-level security features.

model Post {
id Int @id
title String @length(1, 256)
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int

// no anonymous access
@@deny('all', auth() == null)

// author has full access
@@allow('all', authorId == auth().id)

// published posts are readable by anyone
@@allow('read', published)
}

Polymorphic models

Real-world applications often involves storing polymorphic data which is notoriously complex to model and query. ZenStack does the heavy-lifting for you so you can model an inheritance hierarchy with simple annotations, and query them with perfect type safety.

zenstack/schema.zmodel
model Content {
id Int @id
name String @length(1, 256)
type String

// the ORM uses the `type` field to determine to which concrete model
// a query should be delegated
@@delegate(type)
}

model Post extends Content {
content String
}
main.ts
const asset = await db.asset.findFirst();
if (asset.type === 'Post') {
// asset's type is narrowed down to `Post`
console.log(asset.content);
} else {
// other asset type
}

Straightforward and light-Weighted

Compared to Prisma and previous versions of ZenStack, v3 is more straightforward and light-weighted.

  • No runtime dependency to Prisma, thus no overhead of Rust/WASM query engines.
  • No magic generating into node_modules. You fully control how the generated code is compiled and bundled.
  • Less code generation, more type inference.

Documentation Conventions

Sample playground

Throughout the documentation we'll use StackBlitz to provide interactive code samples. StackBlitz's WebContainers is an awesome technology that allows you to run a Node.js environment inside the browser. The embedded samples use the sql.js (a WASM implementation of SQLite) for WebContainers compatibility, which is not suitable for production use.

If you already know Prisma

Although ZenStack ORM has a Prisma-compatible query API, the documentation doesn't assume prior knowledge of using Prisma. However, readers already familiar with Prisma can quickly skim through most of the content and focus on the differences. The documentation uses the following callout to indicate major differences between ZenStack ORM and Prisma:

🔋 ZenStack vs Prisma

Explanation of some key differences between ZenStack and Prisma ORM.

Comments
Feel free to ask questions, give feedback, or report issues.

Don't Spam


You can edit/delete your comments by going directly to the discussion, clicking on the 'comments' link below