knex is a SQL query builder that provides a flexible interface for constructing database queries without enforcing an object-relational mapping (ORM) structure. sequelize, typeorm, and bookshelf are full ORMs that map database tables to JavaScript classes or objects, handling relationships and validations automatically. orange-orm is a lesser-known alternative in this space. These tools help developers interact with databases like PostgreSQL, MySQL, and SQLite using JavaScript or TypeScript, abstracting away raw SQL while offering varying levels of control and convention.
When building Node.js backends, selecting the right database layer is a critical architectural decision. The options range from raw query builders to full-featured Object-Relational Mappers (ORMs). Each tool offers a different balance of control, convenience, and type safety. Let's examine how bookshelf, knex, orange-orm, sequelize, and typeorm handle common development tasks.
knex focuses on building queries fluently without mapping results to models. You chain methods to construct SQL.
// knex: Fluent query builder
const users = await knex('users')
.where('active', true)
.select('id', 'name');
sequelize uses model methods to generate queries behind the scenes.
// sequelize: Model-based query
const users = await User.findAll({
where: { active: true },
attributes: ['id', 'name']
});
typeorm provides a Repository pattern or Entity Manager to handle queries.
// typeorm: Repository pattern
const users = await userRepository.find({
where: { active: true },
select: ['id', 'name']
});
bookshelf relies on models that wrap Knex queries internally.
// bookshelf: Model query
const users = await User.where('active', true)
.fetchAll({ columns: ['id', 'name'] });
orange-orm typically follows a standard model query approach similar to other ORMs, though documentation is sparser.
// orange-orm: Model query (generalized based on typical ORM patterns)
const users = await OrangeModel.find({
where: { active: true },
fields: ['id', 'name']
});
knex requires you to manually write joins. It does not understand relationships between tables.
// knex: Manual join
const posts = await knex('posts')
.join('users', 'posts.user_id', 'users.id')
.where('users.active', true);
sequelize defines associations in the model setup and handles joins automatically.
// sequelize: Defined association
const posts = await Post.findAll({
include: [{ model: User, where: { active: true } }]
});
typeorm uses relations defined in entity classes with decorators.
// typeorm: Entity relation
const posts = await postRepository.find({
relations: ['user'],
where: { user: { active: true } }
});
bookshelf uses the withRelated option to eager load relationships defined on the model.
// bookshelf: Eager loading
const posts = await Post.where('user_id', '!=', null)
.fetchAll({ withRelated: ['user'] });
orange-orm generally supports relationship definitions, but implementation details vary by version.
// orange-orm: Relation loading (generalized)
const posts = await OrangePost.find({
populate: ['user']
});
knex has a built-in migration system that is widely used even by projects not using Knex for queries.
// knex: Migration file
exports.up = function(knex) {
return knex.schema.createTable('users', (table) => {
table.increments('id');
table.string('name');
});
};
sequelize includes a CLI for generating and running migration files.
// sequelize: Migration file
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('users', {
id: { type: Sequelize.INTEGER, primaryKey: true },
name: { type: Sequelize.STRING }
});
}
};
typeorm can generate migrations automatically from entity changes or be written manually.
// typeorm: Migration class
export class CreateUsers1234567890123 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(new Table({ name: 'users', columns: [...] }));
}
}
bookshelf does not have its own migration tool; it relies on Knex for migrations.
// bookshelf: Uses Knex migrations
// Same as knex example above
exports.up = function(knex) {
return knex.schema.createTable('users', (table) => {
table.increments('id');
});
};
orange-orm migration support depends on the specific version and may require external tools.
// orange-orm: Migration (varies by implementation)
// Often requires manual SQL or external migration runners
await orm.migrate('create_users_table');
knex supports TypeScript but queries are dynamic. You often need to define interfaces for results.
// knex: TypeScript interface
interface User { id: number; name: string; }
const users = await knex<User>('users').where('id', 1);
sequelize added TypeScript support later. It requires defining models with generic types.
// sequelize: TypeScript model
const User = sequelize.define<User>('User', {
name: { type: DataTypes.STRING }
});
typeorm was built with TypeScript in mind. Entities are classes with decorators.
// typeorm: TypeScript entity
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
}
bookshelf has community type definitions but is primarily JavaScript-focused.
// bookshelf: TypeScript definition
interface User extends Bookshelf.Model<User> {
id: number;
name: string;
}
orange-orm TypeScript support is less documented and may vary.
// orange-orm: TypeScript (if supported)
interface OrangeUser {
id: number;
name: string;
}
knex does not have model lifecycle hooks because it has no models.
// knex: No hooks
// Must handle logic manually before/after query execution
await knex('users').insert({ name: 'Alice' });
console.log('Inserted manually');
sequelize supports hooks like beforeCreate, afterUpdate, etc.
// sequelize: Hooks
User.addHook('beforeCreate', (user) => {
user.name = user.name.toUpperCase();
});
typeorm uses listeners and subscribers on entities.
// typeorm: Listeners
@BeforeInsert()
capitalizeName() {
this.name = this.name.toUpperCase();
}
bookshelf is known for its robust event-based lifecycle.
// bookshelf: Events
const User = bookshelf.Model.extend({
tableName: 'users',
initialize: function() {
this.on('creating', this.capitalizeName);
}
});
orange-orm may support hooks, but consistency varies.
// orange-orm: Hooks (generalized)
OrangeModel.on('beforeSave', (data) => {
// modify data
});
| Feature | knex | sequelize | typeorm | bookshelf | orange-orm |
|---|---|---|---|---|---|
| Type | Query Builder | ORM | ORM | ORM | ORM |
| TS Support | Good | Moderate | Excellent | Limited | Variable |
| Migrations | Built-in | Built-in | Built-in | Via Knex | Varies |
| Relationships | Manual | Auto | Auto | Auto | Auto |
| Maintenance | High | High | High | Low | Low |
knex is the foundation. It gives you control. Use it when you want to write SQL without the magic of an ORM getting in your way. It is also the engine behind bookshelf.
sequelize is the veteran. It has been around for a long time and supports almost every SQL database. It is a safe choice for JavaScript projects that need reliability and extensive features.
typeorm is the modern TypeScript choice. If your team lives in TypeScript, this integrates best with your workflow. It enforces structure through classes and decorators.
bookshelf is the legacy option. It is built on Knex but adds an ORM layer. Unless you are maintaining an older codebase, there are more active communities elsewhere.
orange-orm is the niche option. It lacks the ecosystem of the others. Use it only if you have a specific reason that the larger libraries cannot meet.
Final Thought: For new projects, typeorm is best for TypeScript stacks, while sequelize remains strong for JavaScript. If you prefer writing SQL, knex is the industry standard for query building.
Choose knex if you want maximum control over your SQL queries without the overhead of an ORM. It is ideal for teams that prefer writing explicit queries over managing complex model relationships. This is the best fit for microservices or applications where performance and query precision matter more than developer convenience features like auto-mapping.
Choose sequelize if you need a battle-tested ORM with support for a wide variety of SQL dialects and extensive documentation. It works well for teams that want a balance between convention and configuration, with strong support for transactions and relationships. This is a solid default choice for JavaScript-heavy teams that do not require strict TypeScript typing out of the box.
Choose orange-orm only after careful verification of its current maintenance status, as it lacks the widespread adoption of other options. It may suit niche requirements where specific, lightweight behavior is needed, but community support will be limited. For most production scenarios, established libraries like Sequelize or TypeORM are safer bets due to their long-term stability.
Choose bookshelf if you are maintaining a legacy application that already relies on its specific model patterns built on top of Knex. It is generally not recommended for new projects due to lower activity compared to modern alternatives. Stick with this only if you need its specific event-driven model lifecycle without migrating to a more active ecosystem.
Choose typeorm if your project is built with TypeScript and you want strong typing integrated directly into your database models. It uses decorators and the Active Record or Data Mapper patterns to keep code organized and type-safe. This is the preferred option for modern NestJS applications or any stack where compile-time checks on database structures are a priority.
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();