Prisma’s relationship system can feel overwhelming at first glance, especially when you’re used to writing foreign keys in a single SQL table. The key to clarity lies in understanding that Prisma asks you to declare relationships from both sides of a model, but only one side actually holds the foreign key column. This approach bridges the gap between database design and JavaScript object modeling, enabling intuitive access patterns like user.jobPostings without adding redundant data.
The core principle behind Prisma relationships
The confusion often starts with Prisma’s requirement to define relationships in two places—once on the model that contains the foreign key, and again on the related model. This isn’t redundancy; it’s intentional design. The foreign key lives physically on one table, just like in MySQL, but Prisma needs the second declaration to generate the correct TypeScript types and query helpers.
The foreign key column exists in exactly one place, but both models must declare the relationship for Prisma to understand how to connect them in code.
Think of it as declaring a pointer in both directions: the database column points forward, while the Prisma schema points backward to enable fluent access patterns in your application logic.
Building a job platform to illustrate four relationship types
To make these concepts concrete, we’ll design a simple job platform with four entities:
User– The platform member who can post jobs and save favoritesProfile– Extended user information like bios and profile picturesJobPosting– A job listing created by a userSavedJob– A bookmark that links a user to a job posting
This setup covers the four fundamental relationship shapes: one-to-one, one-to-many, many-to-one, and many-to-many. We’ll implement each one step by step, comparing Prisma schemas to their MySQL equivalents.
One-to-many relationships: Users posting multiple jobs
This is the most common pattern in application development. A single user creates many job postings, while each job posting belongs to exactly one user through a foreign key.
MySQL implementation
CREATE TABLE User (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE
);
CREATE TABLE JobPosting (
id INT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(255) NOT NULL,
salary INT,
userId INT NOT NULL,
FOREIGN KEY (userId) REFERENCES User(id)
);The foreign key userId lives on the JobPosting table, mirroring how the relationship works in the real world: jobs know who created them, but users don’t need to track their own postings in a separate column.
Prisma schema breakdown
model User {
id Int @id @default(autoincrement())
name String
email String @unique
jobPostings JobPosting[] // Back-reference array
}
model JobPosting {
id Int @id @default(autoincrement())
title String
salary Int?
user User @relation(fields: [userId], references: [id])
userId Int
}Notice the three critical elements:
- The
userIdcolumn onJobPostingacts as the actual foreign key, exactly matching the MySQL implementation - The
@relationattribute specifies how the foreign key connects to the target model - The
jobPostingsarray onUserenablesuser.jobPostingssyntax without creating a new column
Querying one-to-many relationships
// Fetch all jobs posted by a specific user
const userJobs = await prisma.jobPosting.findMany({
where: { userId: 1 }
});
// Get a user along with all their posted jobs
const userWithJobs = await prisma.user.findUnique({
where: { id: 1 },
include: { jobPostings: true }
});
// Create a new job for an existing user using connect
await prisma.jobPosting.create({
data: {
title: "Senior Developer",
salary: 85000,
user: { connect: { id: 1 } }
}
});The connect operation tells Prisma to reference an existing user rather than creating a new one, saving you from manually passing foreign keys.
One-to-one relationships: Users with single profiles
A one-to-one relationship ensures each user has at most one profile, and each profile belongs to exactly one user. This requires a unique constraint on the foreign key column.
MySQL version with uniqueness constraint
CREATE TABLE Profile (
id INT PRIMARY KEY AUTO_INCREMENT,
bio TEXT,
avatar VARCHAR(255),
userId INT NOT NULL UNIQUE,
FOREIGN KEY (userId) REFERENCES User(id)
);The UNIQUE keyword prevents multiple profiles from pointing to the same user, transforming a potential one-to-many scenario into a strict one-to-one relationship.
Prisma schema for one-to-one
model User {
id Int @id @default(autoincrement())
name String
email String @unique
profile Profile? // Optional profile, singular relation
}
model Profile {
id Int @id @default(autoincrement())
bio String?
avatar String?
user User @relation(fields: [userId], references: [id])
userId Int @unique // Enforces one-to-one at schema level
}Key differences from one-to-many:
- The back-reference
profileis singular (Profile?) instead of an array - The
@uniqueattribute onuserIdensures one-to-one semantics - The
?afterProfileindicates the relationship is optional
This dual enforcement—database constraint plus schema attribute—guarantees data integrity while providing clean access patterns in code.
Many-to-many relationships: Users saving multiple job postings
Many-to-many relationships occur when multiple users can bookmark the same job, and each job can be saved by multiple users. Prisma handles this through an implicit join table.
Prisma schema for many-to-many
model User {
id Int @id @default(autoincrement())
name String
email String @unique
savedJobs SavedJob[]
}
model JobPosting {
id Int @id @default(autoincrement())
title String
salary Int?
savedBy User[] @relation("SavedJobRelations")
// No explicit join table needed
}
model SavedJob {
id Int @id @default(autoincrement())
user User @relation(fields: [userId], references: [id])
userId Int
job JobPosting @relation(fields: [jobId], references: [id])
jobId Int
createdAt DateTime @default(now())
@@unique([userId, jobId]) // Prevent duplicate saves
}Prisma automatically creates a join table behind the scenes, but you can also define it explicitly for more control. The @@unique constraint prevents the same user from saving the same job multiple times.
Practical tips for working with Prisma relationships
Start small and build incrementally. Begin by defining your primary models and their direct relationships, then expand to more complex patterns as needed. Always verify your schema against the actual database structure, especially when migrating existing applications.
Remember that Prisma’s include and connect operations dramatically reduce boilerplate code. Instead of manually joining tables or writing complex SQL, you can retrieve related data in a single query with clean, type-safe syntax.
As your application grows, consider using Prisma’s middleware to enforce business rules or audit relationships automatically. The consistent approach to declaring relationships across your entire schema will pay dividends in maintainability and developer experience.
AI summary
Learn Prisma relationship types with clear MySQL comparisons. Master one-to-one, one-to-many, and many-to-many patterns for cleaner database design and queries.