Prisma Client `include`: Type‑Safe Eager Loading of Relations
Learn how Prisma Client's `include` option gives you type‑safe eager loading of relations, with a concrete schema, query example, and the practical limits you need to watch.
01 Dec 2025, 03:22 UTC

What `include` does
Prisma Client’s include option fetches related records in the same database round‑trip and returns fully typed objects that mirror the relations defined in your Prisma schema. When you write prisma.user.findMany({ include: { posts: true } }), TypeScript knows each User in the result has a posts array of Post objects, so autocomplete works for user.posts[0].title without any manual type definitions.
Worked example
1. Schema
// prisma/schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model User {
id Int @id @default(autoincrement())
email String @unique
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
author User @relation(fields: [authorId], references: [id])
authorId Int
}
Run prisma generate in the project root (requires the Prisma CLI installed globally or via npx). This creates the TypeScript client under node_modules/@prisma/client with types that reflect the posts relation.
2. Query script
// scripts/fetch-users.ts
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function main() {
const users = await prisma.user.findMany({
include: { posts: true }
})
console.log(JSON.stringify(users, null, 2))
}
main()
.catch((e) => console.error(e))
.finally(() => prisma.$disconnect())
Execute with ts-node scripts/fetch-users.ts from the project root (Node ≥ 18, ts-node installed). The script connects using the DATABASE_URL from your .env file, so the database must be reachable and the schema migrated (prisma migrate deploy or prisma db push).
3. Expected type shape
Hovering over users in an IDE shows:
const users: (User & {
posts: Post[]
})[]
Each Post contains id, title, and authorId exactly as defined. No extra type annotations are required.
Limits and practical constraints
- Data volume: Including a relation that returns thousands of rows per parent can blow memory and network budgets. Prefer pagination (
take/skip) orselectwith a field list when you only need a subset. - No filtering on the included side:
include: { posts: true }cannot addwhere,orderBy, ordistinctfor the nestedPostrecords. To constrain related data you must either:- Use a nested
selectwith awhereclause (e.g.,include: { posts: { where: { published: true } } }), or - Run a separate query for the relation.
- Use a nested
- Mixing
includeandselectat the same level is illegal: Prisma throws a runtime error if you write{ include: { posts: true }, select: { email: true } }. Choose one strategy per selection level; you can nest the other strategy deeper (e.g.,select: { posts: { include: { comments: true } } }).
Common mistakes
- Accidental N+1: Calling
user.postsinside a loop after a plainfindManywithoutincludetriggers a separate query per user. Always addinclude(or a nestedselect) when you know the relation will be accessed. - Over‑fetching large collections: A
Userwith 10 000Postrows will serialize all of them. Useinclude: { posts: { take: 20, orderBy: { createdAt: 'desc' } } }(supported via nestedselect) or a dedicated paginated endpoint. - Forgetting to regenerate the client: After changing the schema, run
prisma generateagain; otherwise TypeScript types stay stale and you’ll see mismatched property errors.
How to verify the behaviour locally
- Initialize a fresh project:
prisma init(createsprisma/schema.prismaand.env). - Paste the schema above, set
DATABASE_URLto a reachable PostgreSQL instance. - Run
prisma migrate dev --name init(requires CREATE DATABASE permissions on the target server). - Run
prisma generate. - Create the script file and execute
ts-node scripts/fetch-users.ts. - Check the console output: each user object should contain a
postsarray with the expected fields. - Confirm TypeScript compiles without errors (
npx tsc --noEmit) and that IDE hover shows the combined type.
If the output matches the schema and the compiler reports no errors, the include mechanics are working as designed.
When to choose select instead
Use select when you need only a few columns from the parent or the relation, or when you must apply where/orderBy on the nested records. select also produces a narrower type, which can reduce payload size and improve serialization speed.
Summary checklist
- Define the relation in
schema.prisma. - Run
prisma generateafter every schema change. - Use
include: { relationName: true }for simple eager loading. - Switch to nested
selectwhen you need filtering, ordering, or field limiting on the related model. - Monitor query payload size; paginate or project fields for large collections.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.