bookshelf, drizzle-orm, knex, and sequelize are tools for interacting with databases in Node.js, but they serve different roles. sequelize and drizzle-orm are Object-Relational Mappers (ORMs) that map database tables to code classes, handling relationships and data validation. knex is a SQL query builder that helps write database queries without raw strings, while bookshelf is an ORM built on top of knex. Frontend architects often evaluate these for full-stack projects, Serverless functions, or Backend-for-Frontend (BFF) layers where TypeScript support and maintenance status are critical.
When building full-stack JavaScript applications, choosing how your code talks to the database is a critical decision. bookshelf, drizzle-orm, knex, and sequelize all solve this problem, but they approach it differently. Some are full ORMs that manage relationships for you, while others are query builders that give you more control. Let's break down how they handle real-world tasks.
sequelize and drizzle-orm are ORMs. They let you define models as classes or objects, and they handle saving, loading, and relationships automatically.
knex is a query builder. It helps you construct SQL queries using JavaScript functions, but it does not manage data models or relationships out of the box.
bookshelf is an ORM built on top of knex. It adds model logic to knex queries, but it inherits knex limitations and adds extra weight.
// sequelize: Defines a model class
const User = sequelize.define('User', { name: Sequelize.STRING });
// drizzle-orm: Defines a schema object
const users = pgTable('users', { id: integer('id'), name: text('name') });
// knex: No model definition, just queries
knex.schema.createTable('users', (table) => { table.string('name'); });
// bookshelf: Defines a model extending bookshelf.Model
const User = bookshelf.Model.extend({ tableName: 'users' });
Frontend developers care deeply about types. How you define your data shape changes based on the tool.
sequelize uses a definition object or class syntax. It relies on runtime validation.
// sequelize: Model definition
const User = sequelize.define('User', {
firstName: { type: DataTypes.STRING },
age: { type: DataTypes.INTEGER }
});
drizzle-orm uses a schema definition that maps closely to TypeScript types. It is static and type-safe.
// drizzle-orm: Schema definition
const users = pgTable('users', {
id: serial('id').primaryKey(),
age: integer('age'),
name: text('name')
});
knex does not define data structures in code. You write migration files to change the database schema directly.
// knex: Migration file
exports.up = (knex) => knex.schema.createTable('users', (table) => {
table.increments('id');
table.integer('age');
});
bookshelf requires you to specify the table name and rely on knex for schema changes.
// bookshelf: Model definition
const User = bookshelf.Model.extend({
tableName: 'users',
hasTimestamps: true
});
Fetching data is the most common task. The syntax varies from SQL-like to object-based.
sequelize uses an object-based query syntax. It can be verbose but abstracts SQL completely.
// sequelize: Find users older than 18
const users = await User.findAll({
where: { age: { [Op.gt]: 18 } }
});
drizzle-orm uses a chainable API that looks like SQL but is type-safe.
// drizzle-orm: Find users older than 18
const users = await db.select().from(users).where(gt(users.age, 18));
knex uses a chainable builder that mirrors SQL structure closely.
// knex: Find users older than 18
const users = await knex('users').where('age', '>', 18);
bookshelf uses a chainable API similar to knex but returns model instances.
// bookshelf: Find users older than 18
const users = await User.where('age', '>', 18).fetchAll();
Relationships (like User has many Posts) are where ORMs shine. Query builders require manual joins.
sequelize handles relationships with hasMany and belongsTo. It auto-loads related data.
// sequelize: Define relationship
User.hasMany(Post);
// Fetch with include
const user = await User.findOne({ include: [Post] });
drizzle-orm handles relationships via schema relations and explicit joins.
// drizzle-orm: Define relation
const usersRelations = relations(users, ({ many }) => ({ posts: many(posts) }));
// Fetch with join
const result = await db.select().from(users).leftJoin(posts, eq(users.id, posts.userId));
knex requires manual joins. You must write the logic yourself.
// knex: Manual join
const results = await knex('users')
.join('posts', 'users.id', 'posts.user_id')
.where('users.age', '>', 18);
bookshelf handles relationships with hasMany but relies on knex under the hood.
// bookshelf: Define relationship
const User = bookshelf.Model.extend({
tableName: 'users',
posts: function() { return this.hasMany(Post); }
});
// Fetch with related
const user = await User.where('id', 1).fetch({ withRelated: ['posts'] });
For frontend developers moving to full-stack, TypeScript support is often the deciding factor.
drizzle-orm is built for TypeScript. It infers types from your schema automatically. You get autocomplete and type errors if your query is wrong.
sequelize supports TypeScript but often requires extra configuration and generic types to work correctly. It can feel heavy.
knex has community type definitions. They work well for queries but do not enforce schema types without extra plugins.
bookshelf has weak TypeScript support. It was built before modern TypeScript standards and feels like using JavaScript with types added on top.
bookshelf is in maintenance mode. It is not receiving new features and should not be used for new projects. The community has shifted to lighter alternatives.
sequelize is stable and widely used. It is safe for enterprise projects but evolves slowly.
knex is stable and actively maintained. It remains the standard for query building.
drizzle-orm is rapidly growing. It is the modern choice for new TypeScript projects.
| Feature | sequelize | drizzle-orm | knex | bookshelf |
|---|---|---|---|---|
| Type | Full ORM | Lightweight ORM | Query Builder | ORM (Legacy) |
| TypeScript | Good | Excellent | Fair | Poor |
| Learning Curve | High | Medium | Low | Medium |
| Performance | Heavy | Light | Light | Medium |
| Status | Stable | Active | Stable | Maintenance |
drizzle-orm is the best choice for modern frontend teams building full-stack apps. It offers the best balance of type safety, performance, and developer experience.
sequelize is safe for large legacy systems or teams that need extensive ORM features across many database types.
knex is ideal if you want control without ORM magic. It pairs well with custom data mapping.
bookshelf should be avoided in new projects. If you encounter it in an existing codebase, plan to migrate away from it over time.
Final Thought: Your database layer should help you ship features faster โ not slow you down with complex setup. For most modern JavaScript teams, type safety and simplicity win.
Choose bookshelf only if you are maintaining a legacy system that already depends on it. It is currently in maintenance mode and lacks modern TypeScript features. For new projects, avoid this package because the community has moved toward more type-safe and active alternatives.
Choose drizzle-orm if you prioritize TypeScript support and performance. It offers a lightweight ORM experience with SQL-like syntax that feels familiar to frontend developers. It is ideal for modern stacks like Next.js or Serverless where bundle size and type safety matter.
Choose knex if you want full control over SQL queries without the overhead of a full ORM. It is perfect for teams that prefer writing raw SQL logic but need help with query building and migrations. It works well when you do not need automatic relationship management.
Choose sequelize if you need a mature, feature-complete ORM with support for many database types. It is suitable for large enterprise applications where stability and extensive documentation are more important than cutting-edge TypeScript features.
Bookshelf is a JavaScript ORM for Node.js, built on the Knex SQL query builder. It features both Promise-based and traditional callback interfaces, transaction support, eager/nested-eager relation loading, polymorphic associations, and support for one-to-one, one-to-many, and many-to-many relations.
It is designed to work with PostgreSQL, MySQL, and SQLite3.
Website and documentation. The project is hosted on GitHub, and has a comprehensive test suite.
Bookshelf aims to provide a simple library for common tasks when querying databases in JavaScript, and forming relations between these objects, taking a lot of ideas from the Data Mapper Pattern.
With a concise, literate codebase, Bookshelf is simple to read, understand, and extend. It doesn't force you to use any specific validation scheme, and provides flexible, efficient relation/nested-relation loading and first-class transaction support.
It's a lean object-relational mapper, allowing you to drop down to the raw Knex interface whenever you need a custom query that doesn't quite fit with the stock conventions.
You'll need to install a copy of Knex, and either mysql, pg, or sqlite3 from npm.
$ npm install knex
$ npm install bookshelf
# Then add one of the following:
$ npm install pg
$ npm install mysql
$ npm install sqlite3
The Bookshelf library is initialized by passing an initialized Knex client instance. The Knex documentation provides a number of examples for different databases.
// Setting up the database connection
const knex = require('knex')({
client: 'mysql',
connection: {
host : '127.0.0.1',
user : 'your_database_user',
password : 'your_database_password',
database : 'myapp_test',
charset : 'utf8'
}
})
const bookshelf = require('bookshelf')(knex)
// Defining models
const User = bookshelf.model('User', {
tableName: 'users'
})
This initialization should likely only ever happen once in your application. As it creates a connection pool for the current database, you should use the bookshelf instance returned throughout your library. You'll need to store this instance created by the initialize somewhere in the application so you can reference it. A common pattern to follow is to initialize the client in a module so you can easily reference it later:
// In a file named, e.g. bookshelf.js
const knex = require('knex')(dbConfig)
module.exports = require('bookshelf')(knex)
// elsewhere, to use the bookshelf client:
const bookshelf = require('./bookshelf')
const Post = bookshelf.model('Post', {
// ...
})
Here is an example to get you started:
const knex = require('knex')({
client: 'mysql',
connection: process.env.MYSQL_DATABASE_CONNECTION
})
const bookshelf = require('bookshelf')(knex)
const User = bookshelf.model('User', {
tableName: 'users',
posts() {
return this.hasMany(Posts)
}
})
const Post = bookshelf.model('Post', {
tableName: 'posts',
tags() {
return this.belongsToMany(Tag)
}
})
const Tag = bookshelf.model('Tag', {
tableName: 'tags'
})
new User({id: 1}).fetch({withRelated: ['posts.tags']}).then((user) => {
console.log(user.related('posts').toJSON())
}).catch((error) => {
console.error(error)
})
.set() on a model.Model, adding timestamps, attribute validation and some native CRUD methods.Have questions about the library? Come join us in the #bookshelf freenode IRC channel for support on knex.js and bookshelf.js, or post an issue on Stack Overflow.
If you want to contribute to Bookshelf you'll usually want to report an issue or submit a pull-request. For this purpose the online repository is available on GitHub.
For further help setting up your local development environment or learning how you can contribute to Bookshelf you should read the Contributing document available on GitHub.
Yes, you can call .asCallback(function(err, resp) { on any database operation method and use the standard (err, result) style callback interface if you prefer.
Make sure to check that the type is correct for the initial parameters passed to the initial model being fetched. For example new Model({id: '1'}).load([relations...]) will not return the same as new Model({id: 1}).load([relations...]) - notice that the id is a string in one case and a number in the other. This can be a common mistake if retrieving the id from a url parameter.
This is only an issue if you're eager loading data with load without first fetching the original model. new Model({id: '1'}).fetch({withRelated: [relations...]}) should work just fine.
The issue here is that Knex, the database abstraction layer used by Bookshelf, uses connection pooling and thus keeps the database connection open. If you want your process to exit after your script has finished, you will have to call .destroy(cb) on the knex property of your Bookshelf instance or on the Knex instance passed during initialization. More information about connection pooling can be found over at the Knex docs.
If you pass debug: true in the options object to your knex initialize call, you can see all of the query calls being made. You can also pass that same option to all methods that access the database, like model.fetch() or model.destroy(). Examples:
// Turning on debug mode for all queries
const knex = require('knex')({
debug: true,
client: 'mysql',
connection: process.env.MYSQL_DATABASE_CONNECTION
})
const bookshelf = require('bookshelf')(knex)
// Debugging a single query
new User({id: 1}).fetch({debug: true, withRelated: ['posts.tags']}).then(user => {
// ...
})
Sometimes you need to dive a bit further into the various calls and see what all is going on behind the scenes. You can use node-inspector, which allows you to debug code with debugger statements like you would in the browser.
Bookshelf uses its own copy of the bluebird Promise library. You can read up here for more on debugging Promises.
Adding the following block at the start of your application code will catch any errors not otherwise caught in the normal Promise chain handlers, which is very helpful in debugging:
process.stderr.on('data', (data) => {
console.log(data)
})
See the CONTRIBUTING document on GitHub.
While it primarily targets Node.js, all dependencies are browser compatible, and it could be adapted to work with other javascript environments supporting a sqlite3 database, by providing a custom Knex adapter. No such adapter exists though.
We found the following projects using Bookshelf, but there can be more: