Skip to content

Joist

A TypeScript ORM for Majestic Monoliths
Performance

N+1 safe by default

Write natural async code. Joist batches relation loads across the loop into one query. No manual dataloader boilerplate. 🎉

Explore N+1 prevention →
const books = await author.books.load();
await Promise.all(books.map(async (book) => {
// Any "load in a loop" issues 1 SELECT for all books' reviews
const reviews = await book.reviews.load();
}));
Domain modeling

Schema-driven codegen

Joist generates fields and relations like hasOne, hasMany, etc. from your schema. Your entity files stay clean & focused on business logic, not boilerplate.

Explore code generation →
export class Author extends AuthorCodegen {
// Getters & setters are hidden in the base class
}
Type safety

Loaded types

Preload a relation and TypeScript tracks in the type system that it is safe to access synchronously. Never get runtime “relation was not loaded” errors.

Explore load-safe relations →
// Becomes a `Loaded<Author, { books: "reviews" }>` type
const author = await em.load(Author, "a:1", { books: "reviews" });
for (const book of author.books.get) {
console.log(book.reviews.get.length);
}
// Compile error on `.get`, b/c we haven't loaded `publisher`
console.log(author.publisher.get);
// Now it's valid
const loaded = await author.populate("publisher");
console.log(loaded.publisher.get);
Performance

Efficient writes

Make changes naturally. Joist batches inserts and updates when you flush.

Explore performance →
// Change 100 authors and 100 books
for (let i = 0; i < 100; i++) {
authors[i].firstName = `Ada ${i}`;
books[i].title = `Joist in Practice v${i}`;
}
// Issues 2 SQL queries, 1 `UPDATE authors` and 1 `UPDATE books`
await em.flush();
Business logic

Reactive rules

Declare business invariants across entity-boundaries and Joist will rerun them whenever any data changes.

No spaghetti code for “when this data changes, remember to re-check this other data.”

Explore reactive rules →
// Declared in `Author.ts`, enforced for all writes through `em.flush()`
// Watch for changes to books' titles and author's first name
authorConfig.addRule(
{ books: "title", firstName: {} },
(a) => {
if (a.books.get.some((b) => b.title === a.firstName)) {
return "A book cannot share its author's name";
}
},
);
Business logic

Reactive fields

Write derived values that summarize cross-entity data and automatically stay up to date whenever their source data changes. No forgetting to manually recalc dirty fields.

Explore reactive fields →
class Author extends AuthorCodegen {
// Write arbitrary business logic
readonly numberOfBooks: ReactiveField<Author, number> =
hasReactiveField("books", (author) => {
return author.books.get.length;
})
}
// `flush` knows to automatically recalc `author.numberOfBooks`
const b = em.create(Book, { author });
await em.flush();
Testing

Test Factories

Build an entity graph in one call; built-in factories fill in the required details for you.

Explore test factories →
// Given an author with two books
const author = newAuthor(em, { books: [{}, {}] });
// When we flush
await em.flush();
// Then the number of books is correct
expect(author.numberOfBooks.get).toBe(2);
Testing

Fast database resets

Start each test with a clean database in a single SQL statement. Optimized for large, 100s of tables schemas.

Explore database resets →
beforeEach(async () => {
// 1 SQL call resets all tables & sequences
await knex.select(knex.raw("flush_database()"));
});
Developer experience

AI-agent ready

Codegen ships Joist skills so coding agents can learn the right patterns.

Explore agent skills →
.agents/skills/
joist-em-basics/SKILL.md
joist-upsert/SKILL.md
.claude/skills/
joist-em-basics/SKILL.md