Joist
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();}));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.
export class Author extends AuthorCodegen { // Getters & setters are hidden in the base class}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" }>` typeconst 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 validconst loaded = await author.populate("publisher");console.log(loaded.publisher.get);Efficient writes
Make changes naturally. Joist batches inserts and updates when you flush.
Explore performance →// Change 100 authors and 100 booksfor (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();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 nameauthorConfig.addRule( { books: "title", firstName: {} }, (a) => { if (a.books.get.some((b) => b.title === a.firstName)) { return "A book cannot share its author's name"; } },);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();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 booksconst author = newAuthor(em, { books: [{}, {}] });// When we flushawait em.flush();// Then the number of books is correctexpect(author.numberOfBooks.get).toBe(2);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()"));});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
