Sutando vs TypeORM: A Modern Active Record Alternative
TypeORM has been a staple in the Node.js ecosystem for years. But as TypeScript has evolved and new ORMs have emerged, TypeORM's limitations have become…
TypeORM has been a staple in the Node.js ecosystem for years. But as TypeScript has evolved and new ORMs have emerged, TypeORM’s limitations have become harder to ignore. In this article, we’ll compare TypeORM with Sutando and show why developers are switching.
TypeORM’s Problems
Before comparing features, let’s address why developers look for TypeORM alternatives:
1. Decorator Instability
TypeORM relies heavily on TypeScript decorators (@Entity, @Column, @OneToMany). The TC39 decorator proposal changed multiple times, and TypeORM still uses the legacy experimental decorator syntax. This creates ongoing compatibility concerns.
2. Maintenance Concerns
TypeORM’s development pace has slowed significantly. Issues pile up, PRs take months to merge, and the community has expressed frustration over the lack of active maintenance.
3. QueryBuilder is String-Based
TypeORM’s QueryBuilder uses string-based column and table names, meaning typos are caught at runtime, not compile time:
// TypeORM: string-based — typos not caught until runtime
const users = await getRepository(User)
.createQueryBuilder('user')
.where('user.namme = :name', { name: 'Alice' }) // typo: "namme"
.getMany();
4. Active Record Mode is Limited
TypeORM supports Active Record via BaseEntity, but the implementation is less polished than dedicated Active Record ORMs. Features like scopes, model events, and soft deletes require extra configuration.
Sutando: A Cleaner Alternative
Sutando solves these problems while keeping the Active Record pattern that TypeORM users are familiar with.
No Decorators Required
Sutando models use plain class properties instead of decorators:
// Sutando: clean, no decorators
class User extends Model {
table = 'users';
casts = {
is_admin: 'boolean',
metadata: 'json',
};
relationPosts() {
return this.hasMany(Post, 'user_id');
}
}
// TypeORM: decorator-heavy
@Entity()
class User extends BaseEntity {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column({ type: 'boolean' })
isAdmin: boolean;
@OneToMany(() => Post, post => post.user)
posts: Post[];
}
Query Comparison
Sutando:
// Clean method chaining
const users = await User.query()
.where('active', true)
.with('posts')
.orderBy('created_at', 'desc')
.limit(10)
.get();
TypeORM (Active Record):
const users = await User.createQueryBuilder('user')
.leftJoinAndSelect('user.posts', 'posts')
.where('user.active = :active', { active: true })
.orderBy('user.created_at', 'DESC')
.take(10)
.getMany();
Relationships
Sutando defines relationships as methods that return relation objects:
class User extends Model {
relationPosts() {
return this.hasMany(Post, 'user_id');
}
relationProfile() {
return this.hasOne(Profile);
}
relationRoles() {
return this.belongsToMany(Role, 'user_roles', 'user_id', 'role_id');
}
}
// Usage
const user = await User.query().with('posts', 'profile', 'roles').find(1);
TypeORM uses decorators:
@Entity()
class User extends BaseEntity {
@OneToMany(() => Post, post => post.user)
posts: Post[];
@OneToOne(() => Profile, profile => profile.user)
profile: Profile;
@ManyToMany(() => Role)
@JoinTable({ name: 'user_roles' })
roles: Role[];
}
// Usage
const user = await User.createQueryBuilder('user')
.leftJoinAndSelect('user.posts', 'posts')
.leftJoinAndSelect('user.profile', 'profile')
.leftJoinAndSelect('user.roles', 'roles')
.where('user.id = :id', { id: 1 })
.getOne();
Feature Comparison
| Feature | Sutando | TypeORM |
|---|---|---|
| Pattern | Active Record | Active Record + Data Mapper |
| Decorators | Not required | Required |
| Schema definition | Class properties | Decorators (@Entity, @Column) |
| Query builder | Method chaining | String-based QueryBuilder |
| Relationships | Method-based (hasMany, etc.) |
Decorator-based (@OneToMany, etc.) |
| Eager loading | with('posts.comments') |
leftJoinAndSelect (manual) |
| Model events | Built-in | Subscriber / Listener classes |
| Soft deletes | Built-in | @DeleteDateColumn decorator |
| Query scopes | Built-in | Not available |
| Migrations | Schema builder | Migration classes |
| Bundle size | Small | Large |
| Maintenance | Active | Slow |
| Database support | MySQL, PostgreSQL, SQLite | MySQL, PostgreSQL, SQLite, SQL Server, MongoDB |
Migration Guide: TypeORM to Sutando
Step 1: Replace Entity Definitions
// Before (TypeORM)
@Entity()
class User extends BaseEntity {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column({ unique: true })
email: string;
@CreateDateColumn()
createdAt: Date;
@OneToMany(() => Post, post => post.user)
posts: Post[];
}
// After (Sutando)
class User extends Model {
table = 'users';
relationPosts() {
return this.hasMany(Post, 'user_id');
}
}
Step 2: Replace Queries
// Before (TypeORM)
const user = await User.findOne({ where: { id: 1 }, relations: ['posts'] });
const users = await User.find({ where: { active: true }, take: 10 });
// After (Sutando)
const user = await User.query().with('posts').find(1);
const users = await User.query().where('active', true).limit(10).get();
Step 3: Replace Create/Update/Delete
// Before (TypeORM)
const user = User.create({ name: 'Alice', email: 'alice@example.com' });
await user.save();
// After (Sutando)
const user = new User();
user.name = 'Alice';
user.email = 'alice@example.com';
await user.save();
// or: const user = await User.query().create({ name: 'Alice', email: 'alice@example.com' });
When to Switch from TypeORM to Sutando
- You’re tired of decorator-related issues
- You want cleaner, more readable query syntax
- You need built-in query scopes and model events
- You want a more actively maintained ORM
- You’re looking for a Laravel Eloquent-like experience
- You want to reduce bundle size
Conclusion
TypeORM served the Node.js community well, but its decorator dependency, string-based queries, and maintenance issues make it hard to recommend for new projects. Sutando offers the same Active Record pattern with a cleaner API, no decorator requirement, and built-in features that TypeORM lacks.
If you’re starting a new project or considering a TypeORM replacement, give Sutando a try: npm install sutando — or read the documentation.