Using the Prisma Client

Learn how to use the Prisma client for type-safe database operations in MakerKit.

Import the Prisma client as db from @kit/database to perform type-safe database operations. The client provides findMany, findUnique, create, update, and delete methods for all models defined in your schema, with full TypeScript autocomplete.

This guide is part of the Database Configuration documentation.

Using the Prisma Client

Perform type-safe database operations.

Basic Operations

Import the client from @kit/database:

import { db } from '@kit/database';

CRUD Operations

import { db } from '@kit/database';
import { generateId } from '@kit/shared/utils';
// Find all users
const allUsers = await db.user.findMany();
// Create a new organization
const newOrganization = await db.organization.create({
data: {
id: generateId(),
name: 'Acme Corp',
slug: 'acme-corp',
createdAt: new Date(),
},
});
// Find user by email
const user = await db.user.findUnique({
where: { email: 'alice@acme-corp.com' },
});
// Update a user
await db.user.update({
where: { id: userId },
data: { name: 'Alice Johnson' },
});
// Delete a user
await db.user.delete({
where: { id: userId },
});

Tenant-Scoped Queries

Always filter by organizationId for tenant-scoped data to ensure proper isolation:

Never query tenant-scoped tables without filtering by organizationId. Omitting this filter leaks data across tenants - a critical security vulnerability.

// Correct - respects multi-tenancy
const projects = await db.project.findMany({
where: { organizationId: currentOrgId },
orderBy: { createdAt: 'desc' },
take: 20,
});
// DANGER - leaks data across tenants
const allProjects = await db.project.findMany(); // Never do this

For queries that need user context:

// Find all organizations a user belongs to
const memberships = await db.member.findMany({
where: { userId: currentUserId },
include: { organization: true },
});
const organizations = memberships.map((m) => m.organization);

Relations and Includes

Use include to fetch related data in a single query:

// Fetch organization with members and their user profiles
const orgWithMembers = await db.organization.findUnique({
where: { id: orgId },
include: {
members: {
include: { user: true },
},
},
});
// Access nested data
orgWithMembers?.members.forEach((member) => {
console.log(member.user.email, member.role);
});

Use select for performance when you only need specific fields:

// Only fetch id and name - faster for large tables
const orgNames = await db.organization.findMany({
select: {
id: true,
name: true,
},
});

Transactions

Use transactions for operations that must succeed or fail together:

import { generateId } from '@kit/shared/utils';
// Create organization with initial member in a transaction
const result = await db.$transaction(async (tx) => {
const org = await tx.organization.create({
data: {
id: generateId(),
name: 'New Startup',
slug: 'new-startup',
createdAt: new Date(),
},
});
const member = await tx.member.create({
data: {
id: generateId(),
organizationId: org.id,
userId: currentUserId,
role: 'owner',
createdAt: new Date(),
},
});
return { org, member };
});

Decision Rules

Use findUnique when:

  • Querying by primary key or unique constraint
  • You expect exactly one result or null
  • Performance matters (uses indexed lookup)

Use findMany when:

  • Querying multiple records with filters
  • You need pagination with skip/take
  • Ordering or aggregating results

Use include when:

  • You need related data immediately
  • The relation is small (few records)
  • You'll access the nested data in your code

Use select when:

  • You only need specific fields
  • Table has many columns or large text fields
  • Optimizing response size for client components

If unsure: Start with findUnique/findMany without include. Add relations when you discover you need them.

Query Pitfalls

  • Forgetting organizationId filter - Leaks data across tenants; always scope queries to the current organization
  • Using findMany() without limits - Can crash memory on large tables; always use take: N for safety
  • Nested include creating N+1 queries - Deeply nested includes can explode into many queries; prefer select for specific fields
  • Not handling null from findUnique - Returns null when not found; always check before accessing properties
  • Updating without where constraint - Can update more records than intended; be explicit with your where clause
  • Missing indexes on filtered columns - findMany filters become slow on large tables; add indexes for frequently queried columns

For more Prisma patterns, see the Prisma Client documentation.


Next: Rate Limit Service →