bookshelf, knex, kysely, and sequelize are tools for interacting with SQL databases in Node.js applications. knex and kysely are query builders that help you write SQL statements using JavaScript code, giving you direct control over queries. sequelize and bookshelf are Object-Relational Mappers (ORMs) that let you work with database tables as JavaScript objects or classes, handling relationships and data mapping automatically. While knex is the standard for flexible query building, kysely adds strong TypeScript safety. sequelize offers a full-featured ORM experience, and bookshelf provides a lighter ORM layer built on top of knex.
When building Node.js backends, choosing how to talk to your database is a critical decision. bookshelf, knex, kysely, and sequelize represent two different approaches: Query Builders versus Object-Relational Mappers (ORMs). Frontend developers moving into full-stack roles often prioritize TypeScript support and developer experience. Let's break down how these tools handle real-world tasks.
knex and kysely are query builders. They help you construct SQL statements using JavaScript functions. You still think in terms of tables and rows.
// knex: Building a query
const users = await knex('users').where('active', true).select('id', 'name');
// kysely: Building a type-safe query
const users = await db.selectFrom('users').where('active', '=', true).select(['id', 'name']).execute();
sequelize and bookshelf are ORMs. They let you define models as classes or objects. You interact with these models instead of writing table names directly.
// sequelize: Using a Model
const users = await User.findAll({ where: { active: true }, attributes: ['id', 'name'] });
// bookshelf: Using a Model
const users = await User.where({ active: true }).fetch({ columns: ['id', 'name'] });
Type safety is crucial for frontend developers used to TypeScript.
kysely leads this category. It requires you to generate types from your database schema. If you change a column name in the database but not in your code, TypeScript will warn you before you run the app.
// kysely: Type error if 'email' does not exist in DB
const user = await db.selectFrom('person').select('email').executeTakeFirst();
sequelize supports TypeScript through model definitions, but it often requires extra configuration to keep types in sync with the database.
// sequelize: Defining types via Model
class User extends Model {
declare id: number;
declare email: string;
}
knex has community plugins for TypeScript, but it does not enforce schema safety out of the box. You might query a column that doesn't exist without knowing until runtime.
// knex: No automatic type checking on column names
const result = await knex('users').select('non_existent_column');
bookshelf has minimal TypeScript support. It relies on JavaScript patterns that predate modern TypeScript standards, making it harder to get strict type checking.
// bookshelf: Limited type inference
const user = await User.where({ id: 1 }).fetch();
// 'user' type is often generic or requires manual casting
Fetching related data (like a user and their posts) is where ORMs shine, but query builders offer more control.
sequelize makes this very easy with the include option. It handles the JOINs for you behind the scenes.
// sequelize: Eager loading relationships
const user = await User.findOne({
where: { id: 1 },
include: [{ model: Post }]
});
bookshelf uses withRelated to achieve similar results, leveraging knex under the hood.
// bookshelf: Fetching with relations
const user = await User.where({ id: 1 }).fetch({ withRelated: ['posts'] });
knex requires you to write the JOIN manually. This is more work but gives you exact control over the SQL.
// knex: Manual JOIN
const user = await knex('users')
.leftJoin('posts', 'users.id', 'posts.user_id')
.where('users.id', 1)
.select('users.*', 'posts.title as post_title');
kysely also requires manual JOINs but ensures the columns you select are valid via types.
// kysely: Type-safe JOIN
const user = await db
.selectFrom('person')
.leftJoin('post', 'post.person_id', 'person.id')
.where('person.id', '=', 1)
.selectAll('person')
.executeTakeFirst();
All four tools support migrations, which are scripts to update your database structure.
knex and bookshelf share the same migration system since Bookshelf uses Knex. You write up and down functions to change tables.
// knex/bookshelf migration
exports.up = function(knex) {
return knex.schema.createTable('users', (table) => {
table.increments('id');
table.string('email');
});
};
sequelize has its own CLI and migration format. It tracks executed migrations in a special table.
// sequelize migration
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable('Users', { id: { type: Sequelize.INTEGER } });
}
};
kysely does not bundle a migration tool by default. You typically use a separate package like kysely-bun-migrator or stick with knex for migrations while using kysely for queries.
// kysely: Often paired with external migration tools
// No built-in migration API in the core package
This is a critical factor for long-term projects.
sequelize is actively maintained and widely used in enterprise environments. It receives regular updates for security and features.
knex is also stable and actively maintained. It is a dependency for many other tools, ensuring it remains reliable.
kysely is newer but growing rapidly. It is designed for modern TypeScript workflows and has an active community pushing improvements.
bookshelf has seen significantly slower development in recent years. While not officially deprecated, it is considered legacy by many in the community. Starting a new project with it carries risk due to fewer updates and less community support.
| Feature | bookshelf | knex | kysely | sequelize |
|---|---|---|---|---|
| Type | ORM | Query Builder | Type-Safe Query Builder | ORM |
| TypeScript | Weak | Moderate (plugins) | Excellent (Generated) | Good (Model-based) |
| Relationships | Automatic (withRelated) | Manual (JOINs) | Manual (JOINs) | Automatic (include) |
| Migrations | Built-in (via Knex) | Built-in | External Tool Needed | Built-in |
| Status | Legacy / Maintenance | Stable | Active / Growing | Stable / Active |
kysely is the top choice for new TypeScript projects. The compile-time safety prevents entire classes of bugs and feels familiar to frontend developers used to strict typing.
sequelize remains a strong contender if you need a full ORM with complex relationship management and don't mind the extra overhead.
knex is perfect when you want flexibility without opinionated mapping. It is also the engine behind bookshelf.
bookshelf should generally be avoided for new work. Its maintenance status lags behind the others, and kysely or sequelize offer better long-term stability.
Final Thought: For frontend architects, type safety and maintenance are the deciding factors. kysely aligns best with modern TypeScript workflows, while sequelize offers the most features out of the box for complex data models.
Choose knex if you want full control over your SQL queries without the overhead of an ORM. It is ideal for teams that prefer writing raw SQL logic wrapped in a safe JavaScript interface. It works well when you need flexibility and do not require automatic object mapping or relationship management.
Choose bookshelf only if you are maintaining a legacy system that already depends on it. It is built on top of knex and offers a simple ORM style, but development has slowed significantly. For new projects, the lack of active updates and weaker TypeScript support makes it a risky choice compared to modern alternatives.
Choose kysely if you are building a new TypeScript project and want end-to-end type safety. It generates types directly from your database schema, catching errors at compile time instead of runtime. It is the best fit for frontend developers who value strict typing and modern developer tools.
Choose sequelize if you need a powerful ORM with built-in support for complex relationships, validations, and migrations. It is suitable for large applications where mapping database tables to objects saves significant development time. Be aware that it can be heavier and slower than query builders for simple tasks.
A SQL query builder that is flexible, portable, and fun to use!
A batteries-included, multi-dialect (PostgreSQL, MariaDB, MySQL, CockroachDB, MSSQL, SQLite3, Oracle (including Oracle Wallet Authentication)) query builder for Node.js, featuring:
Node.js versions 16+ are supported.
You can report bugs and discuss features on the GitHub issues page or send tweets to @kibertoad.
For support and questions, join our Gitter channel.
For knex-based Object Relational Mapper, see:
To see the SQL that Knex will generate for a given query, you can use Knex Query Lab
Node.js 16+
Python 3.x with setuptools installed (required for building native dependencies like better-sqlite3)
Python 3.12+ removed the built-in distutils module. If you encounter a ModuleNotFoundError: No module named 'distutils' error during npm install, install setuptools for the Python version used by node-gyp:
pip install setuptools
Windows only: Visual Studio Build Tools with the "Desktop development with C++" workload
npm install
We have several examples on the website. Here is the first one to get you started:
const knex = require('knex')({
client: 'sqlite3',
connection: {
filename: './data.db',
},
});
try {
// Create a table
await knex.schema
.createTable('users', (table) => {
table.increments('id');
table.string('user_name');
})
// ...and another
.createTable('accounts', (table) => {
table.increments('id');
table.string('account_name');
table.integer('user_id').unsigned().references('users.id');
});
// Then query the table...
const insertedRows = await knex('users').insert({ user_name: 'Tim' });
// ...and using the insert id, insert into the other table.
await knex('accounts').insert({
account_name: 'knex',
user_id: insertedRows[0],
});
// Query both of the rows.
const selectedRows = await knex('users')
.join('accounts', 'users.id', 'accounts.user_id')
.select('users.user_name as user', 'accounts.account_name as account');
// map over the results
const enrichedRows = selectedRows.map((row) => ({ ...row, active: true }));
// Finally, add a catch statement
} catch (e) {
console.error(e);
}
import { Knex, knex } from 'knex';
interface User {
id: number;
age: number;
name: string;
active: boolean;
departmentId: number;
}
const config: Knex.Config = {
client: 'sqlite3',
connection: {
filename: './data.db',
},
useNullAsDefault: true,
};
const knexInstance = knex(config);
knexInstance<User>('users')
.select()
.then((users) => {
console.log(users);
})
.catch((err) => {
console.error(err);
})
.finally(() => {
knexInstance.destroy();
});
If you are launching your Node application with --experimental-modules, knex.mjs should be picked up automatically and named ESM import should work out-of-the-box.
Otherwise, if you want to use named imports, you'll have to import knex like this:
import { knex } from 'knex/knex.mjs';
You can also just do the default import:
import knex from 'knex';
If you are not using TypeScript and would like the IntelliSense of your IDE to work correctly, it is recommended to set the type explicitly:
/**
* @type {Knex}
*/
const database = knex({
client: 'mysql',
connection: {
host: '127.0.0.1',
user: 'your_database_user',
password: 'your_database_password',
database: 'myapp_test',
},
});
database.migrate.latest();