Skip to content
Sutando
Esc
↑↓navigate↵open⌘Jpreview

A Modern Alternative to Bookshelf.js: Migrating to Sutando

Bookshelf.js was once the go-to ORM for Node.js developers who wanted a simple, promise-based interface on top of Knex.js. But with no updates since 2022,…


Bookshelf.js was once the go-to ORM for Node.js developers who wanted a simple, promise-based interface on top of Knex.js. But with no updates since 2022, no TypeScript support, and the community moving on, it’s time to consider a modern alternative. This guide walks through migrating from Bookshelf.js to Sutando.

The State of Bookshelf.js

Bookshelf.js hasn’t seen a release since version 1.2.0 in June 2022. Several signs point to it being effectively abandoned:

  • No releases in years: The npm package hasn’t been updated since 2022
  • No TypeScript support: The library is pure CommonJS JavaScript with no built-in types — you need @types/bookshelf as a separate package
  • Community consensus: npm-compare, SaaSHub, and multiple blog posts all describe Bookshelf as “legacy” or “maintenance mode”
  • StackOverflow questions: Developers actively asking “Is there an actively maintained replacement for Bookshelf.js?”

Why Sutando?

Sutando is a modern Active Record ORM for Node.js, inspired by Laravel’s Eloquent. Here’s how it compares to Bookshelf.js:

Feature Bookshelf.js Sutando
Pattern Active Record (Collection-based) Active Record (Model-based)
TypeScript None (needs @types/bookshelf) First-class TS support
Module System CommonJS (require) ESM + CommonJS
Migrations Via Knex (separate) Built-in schema builder
Last Release 2022 Active
Relations forge().fetch() hasMany(), belongsTo()
Soft Deletes Manual Built-in
Hooks initialize events creating, saved etc.
Scopes Manual Built-in

Concept Mapping: Bookshelf.js → Sutando

Setup

Bookshelf.js:

import knex from 'knex';
import bookshelf from 'bookshelf';

const knexInstance = knex({
  client: 'pg',
  connection: process.env.DATABASE_URL,
});

const db = bookshelf(knexInstance);
db.plugin('registry');
db.plugin('virtuals');
db.plugin('pagination');

Sutando:

import { sutando } from 'sutando';

sutando.addConnection({
  client: 'pg',
  connection: process.env.DATABASE_URL,
});

export default sutando;

No plugins needed — virtuals, pagination, and registry-like behavior are built in.

Models

Bookshelf.js:

const User = db.model('User', {
  tableName: 'users',
  
  posts() {
    return this.hasMany('Post');
  },
  
  profile() {
    return this.belongsTo('Profile');
  },
});

// Or using extend
const User = db.Model.extend({
  tableName: 'users',
  
  posts() {
    return this.hasMany('Post');
  },
});

Sutando:

import { Model } from 'sutando';

class User extends Model {
  protected table = 'users';
  
  relationPosts() {
    return this.hasMany(Post);
  }
  
  relationProfile() {
    return this.belongsTo(Profile);
  }
}

Queries

Bookshelf.js:

// Fetch all
const users = await new User().fetch();

// Fetch with relations
const user = await new User({ id: 1 }).fetch({
  withRelated: ['posts.comments'],
});

// Where clause
const users = await User.where('age', '>', 18).fetch();

// Insert
const user = await new User({ name: 'Alice', email: 'alice@example.com' }).save();

// Update
const user = await new User({ id: 1 }).fetch();
user.set('name', 'Bob');
await user.save();

// Delete
const user = await new User({ id: 1 }).fetch();
await user.destroy();

Sutando:

// Fetch all
const users = await User.query().get();

// Fetch with relations
const user = await User.query().with('posts.comments').find(1);

// Where clause
const users = await User.query().where('age', '>', 18).get();

// Insert
const user = new User({ name: 'Alice', email: 'alice@example.com' });
await user.save();

// Update
const user = await User.query().find(1);
user.name = 'Bob';
await user.save();

// Delete
const user = await User.query().find(1);
await user.delete();

Pagination

Bookshelf.js requires the pagination plugin:

db.plugin('pagination');

const results = await new User().fetchPage({
  page: 1,
  pageSize: 20,
});
// results.models, results.pagination

Sutando has pagination built in:

const results = await User.query().paginate(1, 20);
// results.items(), results.total(), results.currentPage(), results.perPage()

Transactions

Bookshelf.js:

await db.transaction(async (t) => {
  const user = await new User({ name: 'Alice' }).save(null, { transacting: t });
  await new Post({ title: 'Hello', user_id: user.id }).save(null, { transacting: t });
});

Sutando:

const db = sutando.connection();

await db.transaction(async (trx) => {
  const user = new User({ name: 'Alice' });
  await user.save({ client: trx });
  
  const post = new Post({ title: 'Hello', user_id: user.id });
  await post.save({ client: trx });
});

Hooks / Events

Bookshelf.js:

const User = db.Model.extend({
  tableName: 'users',
  
  initialize() {
    this.on('saving', this.onSaving);
  },
  
  onSaving(model, attrs, options) {
    model.set('updated_at', new Date());
  },
});

Sutando:

class User extends Model {
  protected table = 'users';
  
  static booted() {
    this.saving(model => {
      model.updated_at = new Date();
    });
  }
}

Step-by-Step Migration Guide

Step 1: Install Sutando

npm install sutando

Step 2: Replace the Database Connection

Replace your Bookshelf initialization with Sutando:

import { sutando } from 'sutando';

sutando.addConnection({
  client: 'pg',
  connection: process.env.DATABASE_URL,
});

export default sutando;

Remove the bookshelf and @types/bookshelf packages:

npm uninstall bookshelf @types/bookshelf

Step 3: Convert Models from extend to ES6 Classes

Bookshelf’s Model.extend() pattern should be converted to ES6 class syntax:

  • tableName → protected table
  • hasMany('Post') → this.hasMany(Post) (using class references, not string names)
  • belongsTo('Profile') → this.belongsTo(Profile)
  • initialize() with event listeners → register saving, creating hooks inside static booted()

Step 4: Update Query Patterns

The biggest changes in query patterns:

  • new User().fetch() → User.query().get()
  • new User({ id: 1 }).fetch({ withRelated: [...] }) → User.query().with('...').find(1)
  • model.set('key', value) → model.key = value (direct property assignment)
  • model.destroy() → model.delete()
  • .fetchPage() → .paginate()

Step 5: Migrate Migrations

If you’re using Knex migrations, they work with Sutando’s schema builder too:

const { Migration } = require('sutando');

module.exports = class extends Migration {
  async up(schema) {
    await schema.createTable('users', (table) => {
      table.increments('id');
      table.string('name');
      table.string('email').unique();
      table.timestamps();
    });
  }

  async down(schema) {
    await schema.dropTableIfExists('users');
  }
};

What You Gain After Migration

  • First-class TypeScript: No more @types/bookshelf — types are built in and accurate
  • ESM support: Use import/export natively, no more require()
  • No plugins needed: Pagination, virtuals, and registry are built-in features
  • Direct property access: user.name = 'Bob' instead of user.set('name', 'Bob')
  • Built-in soft deletes: Apply the SoftDeletes plugin (compose(Model, SoftDeletes)) — no manual implementation
  • Built-in scopes: Define reusable query filters without custom plugin code
  • Eloquent-style API: Familiar to anyone who’s used Laravel
  • Active maintenance: Regular updates and new features

Conclusion

Bookshelf.js was a great ORM in its time, but with no updates since 2022 and no TypeScript support, it’s become a liability for modern Node.js projects. Sutando offers a natural upgrade path — same Active Record pattern, but with first-class TypeScript, built-in features that required plugins in Bookshelf, and an actively maintained codebase.

The migration is straightforward: convert models from extend() to ES6 classes, update query patterns, and remove the Bookshelf dependency. Both ORMs can coexist during the transition, so you can migrate model by model.

👉 Get started with Sutando or run npm install sutando to try it out.

Was this page helpful?