zanith

Relations

Relations describe how your models connect. Zanith uses them to build JOINs automatically — you never write JOIN SQL by hand.

Why relations matter

Without relations, getting data from two tables means writing manual SQL JOINs or making multiple queries and stitching results together. With relations, you write.with({ author: true }) and Zanith looks up the foreign key, builds the JOIN, and returns typed results — automatically.

Relations also enable multi-hop traversal (Contract → Organization → Owner), many-to-many through junction tables, and automatic nullable propagation for LEFT JOINs.

Visual overview

Entity relationship diagram — each arrow becomes an automatic JOIN

belongsTo

The most common relation. Use it when this modelhas a foreign key column that points to another model's primary key.

TSPost belongs to User
// Post has an authorId column → points to User.id
relations: {
author: m.belongsTo(User, {
field: 'authorId', // FK column on THIS model
references: 'id', // PK column on the TARGET model
onDelete: 'cascade', // optional: cascade, set_null, restrict
}),
}

When you query with .with({ author: true }), Zanith generates:

TSgenerated SQL
LEFT JOIN "users" AS "author"
ON "posts"."author_id" = "author"."id"

hasMany

The reverse of belongsTo. Use it when another model has a foreign key pointing to this model. The FK lives on the other side.

TSUser has many Posts
// User doesn't have a FK — Post.authorId points to User
relations: {
posts: m.hasMany(() => Post, {
foreignKey: 'authorId', // FK column on the OTHER model
}),
}

hasOne

One-to-one relationship. Like hasMany but only one record on the other side. Typically used for profile/settings patterns.

TSUser has one Profile
relations: {
profile: m.hasOne(() => Profile, {
field: 'id',
references: 'userId',
}),
}

manyToMany

When two models relate through a junction table. Requires a separate model for the junction. Zanith generates two JOINs automatically.

TSPost has many Tags (through PostTag)
// The junction model
const PostTag = defineModel((m) => ({
name: 'PostTag',
table: 'post_tags',
fields: {
...m.id(),
postId: m.uuid().index(),
tagId: m.uuid().index(),
},
relations: {
post: m.belongsTo(Post, { field: 'postId', references: 'id' }),
tag: m.belongsTo(Tag, { field: 'tagId', references: 'id' }),
},
}));
 
// On the Post model:
relations: {
tags: m.manyToMany(Tag, {
through: PostTag, // the junction model
fromField: 'postId', // junction FK → this model
toField: 'tagId', // junction FK → target model
}),
}

When you query .with({ tags: true }), Zanith generates:

TSgenerated SQL
LEFT JOIN "post_tags" AS "tags_junction"
ON "posts"."id" = "tags_junction"."postId"
LEFT JOIN "tags" AS "tags"
ON "tags_junction"."tagId" = "tags"."id"

Relation types summary

TypeFK lives onWhen to useExample
belongsToThis modelThis model has the foreign keyPost → User
hasManyOther modelOther model has FK pointing hereUser → Posts
hasOneOther modelOne-to-one (other side is unique)User → Profile
manyToManyJunction tableMany-to-many through junctionPost ↔ Tag

Next steps

  • Relational Queries — how to use .with() to JOIN related models in queries
  • Enums — define fixed value sets for fields like status and role