Part 3: Schema Design, Relationships, and Migrations

Introduction

In Part 2, we set up our development environment and created a basic schema. Now comes the challenging part: designing a schema that will serve your application well as it grows. I learned this lesson the hard way in an e-commerce project where poor initial schema design led to weeks of painful migrations and data transformations.

In this article, I'll share the schema design principles I follow, how to model different types of relationships, and how to manage schema evolution through migrations. We'll build on the foundation from Part 2 using TypeScript, Prisma, and PostgreSQL.

Schema Design Principles

Principle 1: Start with Your Domain, Not Your Database

When I first started, I'd think about tables and columns. Now, I think about entities and their relationships in my application domain.

For a blog application I built, I started by mapping out:

  • Users can write Posts

  • Posts can have Comments

  • Posts can have multiple Tags

  • Users can follow other Users

Only after understanding these relationships did I design the schema.

Principle 2: Use Descriptive, Consistent Naming

Here's the naming convention I follow:

Principle 3: Think About Queries You'll Run

Before finalizing a schema, I list common queries. For a blog:

This helps me decide on relationships and indexes.

Modeling Relationships

One-to-Many Relationships

The most common relationship type. Example: One user has many posts.

Key Points:

  • posts Post[] on User is the relation field (not stored in database)

  • authorId is the foreign key (actual column in database)

  • @relation(...) defines the relationship constraints

  • onDelete: Cascade means deleting a user deletes their posts

One-to-One Relationships

Less common but useful. Example: User has one profile with extended information.

Key Difference:

  • profile Profile? (with ?) means optional one-to-one

  • userId Int @unique ensures one profile per user

Many-to-Many Relationships

Most complex but powerful. Example: Posts can have many tags, tags can be on many posts.

Prisma automatically creates a join table _PostToTag. To see it:

Generated SQL includes:

Explicit Many-to-Many (When You Need Extra Fields):

In an e-commerce project, I needed to track when a product was added to a category:

Self-Referencing Relationships

For features like user follows. This one took me a while to grasp:

Complete Blog Schema Example

Here's a complete schema from a blog system I built, demonstrating all relationship types:

Understanding Migrations

What Migrations Do

Migrations are version control for your database schema. Each migration is a set of SQL commands that transform your database from one state to another.

Creating Migrations

Workflow I follow:

Migration Best Practices

1. Small, Focused Migrations

2. Never Edit Applied Migrations

Once a migration is applied, never modify it. Instead, create a new migration:

3. Review Generated SQL

Always check the generated SQL before deploying:

Handling Schema Changes

Adding a Required Field to Existing Table:

This is tricky because existing records don't have values. Here's how I handle it:

Renaming Fields:

Prisma can't automatically detect renames, so it drops and recreates:

To preserve data, use raw SQL:

Edit the generated migration file:

Indexes for Performance

In a project with millions of posts, queries were slow until I added indexes:

When to Add Indexes:

  • Foreign keys (Prisma doesn't auto-index these)

  • Fields frequently used in WHERE clauses

  • Fields used for sorting (ORDER BY)

  • Composite indexes for common query patterns

When NOT to Add Indexes:

  • Every column (indexes slow down writes)

  • Small tables (< 1000 rows)

  • Columns rarely queried

Migration Workflow for Teams

Working with a team requires discipline:

Resetting Your Database

During development, sometimes you need a fresh start:

Never run this in production!

What's Next?

We now understand how to design schemas, model relationships, and manage migrations. In Part 4, we'll put this knowledge to use:

  • CRUD operations with Prisma Client

  • Querying related data

  • Filtering and sorting

  • Pagination

  • Transactions

Conclusion

Schema design is both an art and a science. The principles I've shared come from years of mistakes and learning. Start simple, iterate based on your application's needs, and use migrations to evolve your schema safely.

The schema we designed here supports a fully functional blog application. It demonstrates one-to-many (User → Posts), one-to-one (User → Profile), many-to-many (Posts ↔ Tags), and proper indexing for performance.


This article is part of my personal knowledge sharing based on real projects I've built. The schema patterns shown here are used in production applications handling significant traffic.

Last updated