Stephen Haberman I'm working on own ORM too and just want to share my ideas/vision on those points: But net-net is that you get the same "just an empty model" "class Author {}" when starting out Migration is responsible for changing db schema, not only for creating tables, but also changing, renaming, sometimes we do data updates in migration. And in the model we define current columns, it doesn't take much time, developer can easily jump to User and see the fields. But I decided to go further, let it be possible to define a validation schema right in the model! If you know Zod library, I mean to define columns in similar way like they do. Something like: const User = model({ schema: ( t ) => ({ id: t.serial().primaryKey(), email: t.text().email(), }) }) And now it's not only db types, but also validation types. Later in the controller we can reuse this schema to validate payload: const createUserSchema = User.schema.pick({ email: true , name: true , // ... }) router.post( '/user' , ( req, res ) => { const data = createUserSchema.parse(req.body) data // TS knows exactly what the type is }) but that means you can't test any code that commits transactions (or maybe you could use subtransactions?) Yes :) Just published package for this: https://www.npmjs.com/package/pg-transactional-tests There is explanation how it handles nested transactions, previously it was a script I copied from project to project and it helped me a lot even on a production project. Yep! That makes sense, but the wrinkle is: is book.reviews a BookReview[] or Promise<BookReview[]>? I'm going to implement nested loads with json_agg , i.e postgres can aggregate data to json arrays by it's own. As for the interface, .include method should be fine: const result = await Post.include( 'comments' ) result // is an array of posts const post = result[ 0 ] post.comments // array of comments Also it should accept sub queries, so we can load filtered comments: const result = await Post.include({ comments: Comment.where({ ...conditions }).order({ createdAt: 'DESC' }) }) By the way, is it possible in Joist to load Post with their Comments ordered by comment field or apply some condition? Regarding whether book.reviews is a BookReview[] or Promise<BookReview[]> - well, I suspect it's tricky to customize result of em.load , but in theory it's possible to return BookReview[] type only if this resource was preloaded, and Promise<BookReview[]> otherwise. Regarding Unit of Work, I simply don't understand it's benefits, I like the concept of simple structures with fewer magic, when you can see clearly in the code what's going on. Maybe you could suggest an example where it shines? const user = await User.find( 1 ) const data: Partial<UserType> = {} // ...some logic to change user if (someParam) { data.foo = 1 data.bar = '...' } // and in the end: await User.update(user, data) Here is quite typical update flow, without Unit of Work so no need for creating some session , track any states, etc. Even without "ActiveRecord" pattern, User.update is a static method, while user is a plain lightweight JS object. So in my opinion, Unit of Work adds a bit of boilerplate, overhead, complicates things, or am I wrong? I don't mean to criticize Joist, just have different preferences and beliefs. So maybe node.js has no decent ORM to use with pleasure (yet?), but we have so huge variety of libs which are mixing completely different ideas, and continuing building new ones, there is something good in it (I guess).