SWASTIJ CONSULTANCY

Digging Into Prisma’s Underrated Features

Exploring Prisma Beyond Relations - Working With JSON, Filters, and Nested Writes

Published On 2025-10-08

Digging Into Prisma’s Underrated Features

By Swasti Jain

1. Fetching Nested Relations

Imagine you have a User table related to a Post table, which in turn is related to a Comment table. You want to query the User table to retrieve specific users by their IDs, while also including each user’s associated posts — and for each post, the related comments as well.

In Prisma, you can do that with a single query efficiently using the include flag.

1 await prisma.user.findMany({
2    where: {
3      id: userId
4    }
5    include: {
6        post: {
7          include: {
8            comment: true
9          }
10        },
11    },
12  });

To extract only certain fields, you can combine include with select.

1 const listings = await prisma.listing.findMany({
2            where,
3            orderBy,
4            include: {
5                author: {
6                    select: { name: true },
7                },
8                location: {
9		                select: {
10			                timestamp: true
11		                },
12                    include: {
13                        coordinates: true, 
14                    },
15                },
16            },
17        });

2. Adding relations

You must be familiar with adding relations using the foreign key like this:

1await prisma.post.create({
2  data: {
3    title: "My new post",
4    userId: userId,  // directly setting FK column
5  },
6})

There is another (and better) way to do it:

Using connect

1await prisma.post.update({
2  where: { id: postId },
3  data: {
4    user: {
5      connect: { id: newUserId },
6    },
7  },
8});

What this does is create a Foreign Key under the hood. It also validates if the user exists, providing additional safety, unlike the first approach. And for the same reason, it is comparatively slower. You don’t need to know about the FK, just the relation. This is useful when:

• You’re doing multi-level nested writes.

• You care about the relation validation.

3. Uncommon operators for querying relations

For relations, there are special operators provided by Prisma. For example, we have this schema:

1model User {
2  id       String   @id @default(cuid())
3  name     String
4  posts    Post[]   // 1 user → many posts
5}
6
7model Post {
8  id        String  @id @default(cuid())
9  title     String
10  published Boolean
11  userId    String
12  user      User    @relation(fields: [userId], references: [id])
13}

a. Some — “at least one related record matches”

Returns users who have at least one post matching the condition.

1const users = await prisma.user.findMany({
2  where: {
3    posts: {
4      some: {
5        published: true,
6      },
7    },
8  },
9});

b. every — “all related records match”

Returns users for whom every post matches the condition.

1const users = await prisma.user.findMany({
2  where: {
3    posts: {
4      every: {
5        published: true,
6      },
7    },
8  },
9});

c. none — “no related record matches”

1const users = await prisma.user.findMany({
2  where: {
3    posts: {
4      none: {
5        published: true,
6      },
7    },
8  },
9});

In summary:

Blog image

4. Querying for primitive and non-primitive types

Prisma handles querying differently for primitive and non-primitive types. Here is the difference:

a. For primitive types

Primitive fields are directly stored as scalar columns on the table. You can filter them using simple comparison operators (and these are fully type-safe).

1await prisma.user.findMany({
2  where: {
3    age: { gt: 18, lt: 50 },               // numeric comparison
4    email: { contains: "@gmail.com" },     // text comparison
5    isActive: true,                        // direct boolean
6    createdAt: { gte: new Date("2024-01-01") },
7  },
8});

So:

  • Direct mapping to DB operators (=, <, >, ILIKE)
  • Index-friendly, because they correspond to scalar columns
  • Very fast and fully composable (AND, OR, NOT, etc.)

b. For non-primitive types

1. Relations

When filtering by a related table, Prisma gives you relation filters (some, every, none):

1// Users with at least one published post
2await prisma.user.findMany({
3  where: {
4    posts: {
5      some: { published: true },
6    },
7  },
8});
9
10// Users with no unpublished posts
11await prisma.user.findMany({
12  where: {
13    posts: {
14      every: { published: true },
15    },
16  },
17});

2. JSON

JSON data is stored as a blob (jsonb in Postgres). You can only use a limited set of filters because Prisma doesn’t expose arbitrary JSON path operators.

1// Match entire JSON object
2where: { skin: { equals: { name: "Red Luger" } } }
3
4// Check if array contains a value
5where: { attributes: { has: { rarity: "Godly" } } }

3. compound/list fields

If it’s an array of scalars (String[], Int[]), Prisma gives special operators like has, hasSome, isEmpty.

1// posts with at least one of these tags
2where: { tags: { hasSome: ["tech", "js"] } }
3
4// posts where tags array contains exactly one tag
5where: { tags: { has: "flutter" } }
6
7// posts with no tags
8where: { tags: { isEmpty: true } }

5. Array of JSON vs Array of relations (many-to-many relation)

In Prisma, if you have a schema that has a field of type - array of JSON, Prisma gives you JsonNullableListFilter in the where command.

So, for models like:

1model Product {
2  id     String  @id @default(cuid())
3  specs  Json[]  // ← array of JSON values in one column
4}

You actually use JsonNullableListFilter, which has operators like equals, has, hasSome, hasEvery, hasNone.

1const products = await prisma.product.findMany({
2  where: {
3    specs: {
4      isEmpty: false,
5      equals: [ { key: "color", value: "red" } ],
6    },
7  },
8});

If you have one-to-many or many-to-many relations, you use RelationFilters. Consider this schema:

1model User {
2  id    String @id @default(cuid())
3  posts Post[]
4}
5
6model Post {
7  id     String @id @default(cuid())
8  title  String
9  userId String
10  user   User @relation(fields: [userId], references: [id])
11}

Then posts is a relation field, and Prisma lets you use operators like some, every, none:

1posts: { some: { title: { contains: 'Prisma' } } } //post with atleast one title containing "Prisma"

Prisma doesn’t support partial or field-path filters on JSON values yet. So if you try to do something like the following, it won’t work.
1where: {
2	author: {
3	has: { name: { contains: "Jake" } }, // ❌ won't work
4	},
5},

6. Referential Actions

Referential actions are policies that define how a referenced record is handled by the database when you run an update or delete query.

Prisma ORM supports the following referential actions:

for a schema like this:

1model User {
2  id    Int    @id @default(autoincrement())
3  posts Post[]
4}
5
6model Post {
7  id     Int          @id @default(autoincrement())
8  title  String
9  tags   TagOnPosts[]
10  User   User?        @relation(fields: [userId], references: [id], onDelete: SetNull, onUpdate: Cascade)
11  userId Int?
12}
13
14model TagOnPosts {
15  id     Int   @id @default(autoincrement())
16  post   Post? @relation(fields: [postId], references: [id], onUpdate: Cascade, onDelete: Cascade)
17  tag    Tag?  @relation(fields: [tagId], references: [id], onUpdate: Cascade, onDelete: Cascade)
18  postId Int?
19  tagId  Int?
20}
21
22model Tag {
23  id    Int          @id @default(autoincrement())
24  name  String       @unique
25  posts TagOnPosts[]
26}

This model explicitly defines the following referential actions:

  • If you delete a Tag, the corresponding tag assignment is also deleted in TagOnPosts, using the Cascade referential action
  • If you delete a User, the author is removed from all posts by setting the field value to Null, because of the SetNull referential action. To allow this, User and userId must be optional fields in Post.

Sources and useful links: