Thomas Hunter II Thomas has contributed to dozens of enterprise Node.js services and has worked for a company dedicated to securing Node.js. He has spoken at several conferences on Node.js and JavaScript, is the author of Distributed Systems with Node.js, and is an organizer of NodeSchool SF.

Node.js ORMs: Why you shouldn’t use them

16 min read 4597

Node.js ORMs and Why You Shouldn't Use Them

Editor’s note: This post was updated 5 October 2022 to include information about using Sequelize, Knex, and other ORMs with Node.js, when to use an ORM, and the “best” ORM to use for Node.js. The article was also updated to remove outdated information and make general edits.

In a nutshell, ORM refers to object-relational mapping. And this means we map relational sequel objects — foreign keys and primary keys to actual objects.

The result of this is that ORM creates a structure for CRUD operations in our database. Consequently, it also provides a layer of abstraction over the SQL statement by allowing developers to operate on the object instead of going through the hassle of writing SQL queries. Thus, ORM improves readability, documentation, and the speed of development.

However, in this tutorial, we’ll consider three reasons why you should avoid using ORM in your project. While the concepts discussed are applicable to every language and platform, the code examples will be written in Node.js-flavored JavaScript, and we will be considering packages obtained from the npm repository.

I don’t intend to diss any of the modules mentioned in this post. A lot of hard work has gone into each and every one of them. They are used by production applications all around the world which merrily respond to plenty of requests every day. I’ve also deployed applications using ORMs and regret nothing.

What is ORM in Node.js?

ORM is the process of mapping between objects and relational database systems. Different database systems access data in myriad ways, and ORM helps you maintain objects even when the sources and apps they access change over time.

ORMs are commonly used to streamline the migration of data between databases.

Before we get to the reasons why you shouldn’t use ORM with Node.js, let’s list some benefits. When used properly, ORM in Node.js enables you to:

  • Avoid redundant code
  • Easily switch from one database to another
  • Query for multiple tables (ORM converts the object-oriented query approach to SQL)
  • Focus more on business logic and less on writing interfaces

Using ORMs with Node.js

ORMs are powerful tools. The ORMs we’ll be examining in this post are able to communicate with SQL backends such as SQLite, PostgreSQL, MySQL, and MSSQL. The examples in this post will make use of PostgreSQL, which is a very powerful open source SQL server.

There are ORMs capable of communicating with NoSQL backends, such as the Mongoose ORM backed by MongoDB, but we won’t be considering those in this post.

First, run the following commands to start an instance of PostgreSQL locally. It will be configured in such a way that requests made to the default PostgreSQL port on localhost:5432 will be forwarded to the container. It’ll also write the files to disk in your home directory so that subsequent instantiations will retain the data we’re already created:

mkdir -p ~/data/pg-node-orms
docker run 
  --name pg-node-orms 
  -p 5432:5432 
  -e POSTGRES_PASSWORD=hunter12 
  -e POSTGRES_USER=orm-user 
  -e POSTGRES_DB=orm-db 
  -v ~/data/pg-node-orms:/var/lib/postgresql/data 
  -d 
  postgres

Now that you’ve got a database running, we need to add some tables and data to the database. This will allow us to query against the data and get a better understanding of the various layers of abstraction.

Run the next command to start an interactive PostgreSQL prompt:

docker run 
  -it --rm 
  --link pg-node-orms:postgres 
  postgres 
  psql 
  -h postgres 
  -U orm-user 
  orm-db

At the prompt type in the password from the previous code block, hunter12. Now that you’re connected, copy and paste the following queries into the prompt and press the Enter key:

CREATE TYPE item_type AS ENUM (
  'meat', 'veg', 'spice', 'dairy', 'oil'
);

CREATE TABLE item (
  id    SERIAL PRIMARY KEY,
  name  VARCHAR(64) NOT NULL,
  type  item_type
);

CREATE INDEX ON item (type);

INSERT INTO item VALUES
  (1, 'Chicken', 'meat'), (2, 'Garlic', 'veg'), (3, 'Ginger', 'veg'),
  (4, 'Garam Masala', 'spice'), (5, 'Turmeric', 'spice'),
  (6, 'Cumin', 'spice'), (7, 'Ground Chili', 'spice'),
  (8, 'Onion', 'veg'), (9, 'Coriander', 'spice'), (10, 'Tomato', 'veg'),
  (11, 'Cream', 'dairy'), (12, 'Paneer', 'dairy'), (13, 'Peas', 'veg'),
  (14, 'Ghee', 'oil'), (15, 'Cinnamon', 'spice');

CREATE TABLE dish (
  id     SERIAL PRIMARY KEY,
  name   VARCHAR(64) NOT NULL,
  veg    BOOLEAN NOT NULL
);

CREATE INDEX ON dish (veg);

INSERT INTO dish VALUES
  (1, 'Chicken Tikka Masala', false), (2, 'Matar Paneer', true);

CREATE TABLE ingredient (
  dish_id   INTEGER NOT NULL REFERENCES dish (id),
  item_id   INTEGER NOT NULL REFERENCES item (id),
  quantity  FLOAT DEFAULT 1,
  unit      VARCHAR(32) NOT NULL
);

INSERT INTO ingredient VALUES
  (1, 1, 1, 'whole breast'), (1, 2, 1.5, 'tbsp'), (1, 3, 1, 'tbsp'),
  (1, 4, 2, 'tsp'), (1, 5, 1, 'tsp'),
  (1, 6, 1, 'tsp'), (1, 7, 1, 'tsp'), (1, 8, 1, 'whole'),
  (1, 9, 1, 'tsp'), (1, 10, 2, 'whole'), (1, 11, 1.25, 'cup'),
  (2, 2, 3, 'cloves'), (2, 3, 0.5, 'inch piece'), (2, 13, 1, 'cup'),
  (2, 6, 0.5, 'tsp'), (2, 5, 0.25, 'tsp'), (2, 7, 0.5, 'tsp'),
  (2, 4, 0.5, 'tsp'), (2, 11, 1, 'tbsp'), (2, 14, 2, 'tbsp'),
  (2, 10, 3, 'whole'), (2, 8, 1, 'whole'), (2, 15, 0.5, 'inch stick');

You now have a populated database. You can type quit to disconnect from the psql client and get control of your terminal back. If you ever want to run raw SQL commands again, you can run that same docker run command again.

Finally, you’ll also need to create a file named connection.json containing the following JSON structure. This will be used by the Node applications later to connect to the database:

{
  "host": "localhost",
  "port": 5432,
  "database": "orm-db",
  "user": "orm-user",
  "password": "hunter12"
}

ORMs and Node.js: Layers of abstraction

Before diving into too much code, let’s clarify a few different layers of abstraction. Just like everything in computer science, there are tradeoffs as we add layers of abstraction. With each added layer of abstraction, we attempt to trade a decrease in performance with an increase in developer productivity (though this is not always the case).



Low-level: Database driver

This is basically as low-level as you can get  —  short of manually generating TCP packets and delivering them to the database.

A database driver is going to handle connecting to a database (and sometimes connection pooling). At this level, you’re going to be writing raw SQL strings and delivering them to a database, and receiving a response from the database.

In the Node.js ecosystem there are many libraries operating at this layer. Here are three popular libraries:

  • mysql: MySQL (13k stars / 330k weekly downloads)
  • pg: PostgreSQL (6k stars / 520k weekly downloads)
  • sqlite3: SQLite (3k stars / 120k weekly downloads)

Each of these libraries essentially works the same way: take the database credentials, instantiate a new database instance, connect to the database, and send it queries in the form of a string and asynchronously handle the result.

Here is a simple example using the pg module to get a list of ingredients required to cook chicken tikka masala:

#!/usr/bin/env node

// $ npm install pg

const { Client } = require('pg');
const connection = require('./connection.json');
const client = new Client(connection);

client.connect();

const query = `SELECT
  ingredient.*, item.name AS item_name, item.type AS item_type
FROM
  ingredient
LEFT JOIN
  item ON item.id = ingredient.item_id
WHERE
  ingredient.dish_id = $1`;

client
  .query(query, [1])
  .then(res => {
    console.log('Ingredients:');
    for (let row of res.rows) {
      console.log(`${row.item_name}: ${row.quantity} ${row.unit}`);
    }

    client.end();
});

Mid-level: Query builder

This is the intermediary level between using the simpler database driver module vs. a full-fledged ORM. The most notable module which operates at this layer is Knex.

This module is able to generate queries for a few different SQL dialects. This module depends on one of the aforementioned libraries  —  you’ll need to install the particular ones you plan on using with Knex.

  • knex: Query Builder (8k stars / 170k weekly downloads)

When creating a Knex instance, you provide the connection details, along with the dialect you plan on using, and are then able to start making queries. The queries you write will closely resemble the underlying SQL queries.

One nicety is that you’re able to programmatically generate dynamic queries in a much more convenient way than if you were to concatenate strings together to form SQL (which often introduces security vulnerabilities).

Here is a simple example using the knex module to get a list of ingredients required to cook chicken tikka masala:

#!/usr/bin/env node

// $ npm install pg knex

const knex = require('knex');
const connection = require('./connection.json');
const client = knex({
  client: 'pg',
  connection
});

client
  .select([
    '*',
    client.ref('item.name').as('item_name'),
    client.ref('item.type').as('item_type'),
  ])
  .from('ingredient')
  .leftJoin('item', 'item.id', 'ingredient.item_id')
  .where('dish_id', '=', 1)
  .debug()
  .then(rows => {
    console.log('Ingredients:');
    for (let row of rows) {
      console.log(`${row.item_name}: ${row.quantity} ${row.unit}`);
    }

    client.destroy();
});

High-level: ORM

This is the highest level of abstraction we’re going to consider. When working with ORMs, we typically need to do a lot more configuration ahead of time. The point of ORM, as the name implies, is to map a record in a relational database to an object (typically, but not always, a class instance) in our application.

What this means is that we’re defining the structure of these objects, as well as their relationships, in our application code.

  • Sequelize: (16k stars / 270k weekly downloads)
  • Bookshelf: Knex based (5k stars / 23k weekly downloads)
  • Waterline: (5k stars / 20k weekly downloads)
  • Objection: Knex based (3k stars / 20k weekly downloads)

Using Sequelize with Node.js

In this example, we’re going to look at the most popular of the ORMs: Sequelize. We’re also going to model the relationships represented in our original PostgreSQL schema using Sequelize.

Here is the same example using the Sequelize module to get a list of ingredients required to cook chicken tikka masala:

#!/usr/bin/env node

// $ npm install sequelize pg

const Sequelize = require('sequelize');
const connection = require('./connection.json');
const DISABLE_SEQUELIZE_DEFAULTS = {
  timestamps: false,
  freezeTableName: true,
};

const { DataTypes } = Sequelize;
const sequelize = new Sequelize({
  database: connection.database,
  username: connection.user,
  host: connection.host,
  port: connection.port,
  password: connection.password,
  dialect: 'postgres',
  operatorsAliases: false
});

const Dish = sequelize.define('dish', {
  id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
  name: { type: DataTypes.STRING },
  veg: { type: DataTypes.BOOLEAN }
}, DISABLE_SEQUELIZE_DEFAULTS);

const Item = sequelize.define('item', {
  id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
  name: { type: DataTypes.STRING },
  type: { type: DataTypes.STRING }
}, DISABLE_SEQUELIZE_DEFAULTS);

const Ingredient = sequelize.define('ingredient', {
  dish_id: { type: DataTypes.INTEGER, primaryKey: true },
  item_id: { type: DataTypes.INTEGER, primaryKey: true },
  quantity: { type: DataTypes.FLOAT },
  unit: { type: DataTypes.STRING }
}, DISABLE_SEQUELIZE_DEFAULTS);

Item.belongsToMany(Dish, {
  through: Ingredient, foreignKey: 'item_id'
});

Dish.belongsToMany(Item, {
  through: Ingredient, foreignKey: 'dish_id'
});

Dish.findOne({where: {id: 1}, include: [{model: Item}]}).then(rows => {
  console.log('Ingredients:');
  for (let row of rows.items) {
    console.log(
      `${row.dataValues.name}: ${row.ingredient.dataValues.quantity} ` +
      row.ingredient.dataValues.unit
    );
  }

  sequelize.close();
});

Knex vs. Sequelize vs. other ORMs

We have tried to classify some ORMs in previous sections, but in this section, we will be comparing these ORMs.

Let’s start with Sequelize. Sequelize is a full-blown ORM. It is feature-packed and compels you to hide SQL behind object representation.

Knex, on the other hand, is more low-level as it is a plain query builder. Knex is great because it allows you easily find and see what is going on in the database without the abstraction of ORM. However, as the application grows and becomes more complex, we see that working with complex relations with Knex can be tedious and time-consuming.

Objection.js sits in the middle. It combines the good parts of different ORMs without compromising the power of writing raw SQL queries.

Are ORMs really necessary?

Now that you’ve seen an example of how to perform similar queries using the different abstraction layers, let’s dive into three reasons why you should be wary of using ORM.

1. You’re learning the wrong thing

Many people pick up ORM because they don’t want to take the time to learn the underlying SQL. The belief is that SQL is hard to learn and that by learning ORM, we can simply write our applications using a single language instead of two.

At first glance, this seems to hold up. ORM is going to be written in the same language as the rest of the application, while SQL is a completely different syntax.

There is a problem with this line of thinking, however. The problem is that ORMs represent some of the most complex libraries you can get your hands on. The surface area of ORM is very large and learning it inside and out is no easy task.

Once you have learned a particular ORM, this knowledge likely won’t transfer that well. This is true if you switch from one platform to another, such as JS/Node.js to C#/.NET. But perhaps even less obvious is that this is true if you switch from one ORM to another within the same platform, such as Sequelize to Bookshelf with Node.js.

Consider the following ORM examples which each generate a list of all recipe items which are vegetarian.

With Sequelize:

#!/usr/bin/env node

// $ npm install sequelize pg

const Sequelize = require('sequelize');
const { Op, DataTypes } = Sequelize;
const connection = require('./connection.json');
const DISABLE_SEQUELIZE_DEFAULTS = {
  timestamps: false,
  freezeTableName: true,
};

const sequelize = new Sequelize({
  database: connection.database,
  username: connection.user,
  host: connection.host,
  port: connection.port,
  password: connection.password,
  dialect: 'postgres',
  operatorsAliases: false
});

const Item = sequelize.define('item', {
  id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
  name: { type: DataTypes.STRING },
  type: { type: DataTypes.STRING }
}, DISABLE_SEQUELIZE_DEFAULTS);

// SELECT "id", "name", "type" FROM "item" AS "item"
//     WHERE "item"."type" = 'veg';
Item
  .findAll({where: {type: 'veg'}})
  .then(rows => {
    console.log('Veggies:');
    for (let row of rows) {
      console.log(`${row.dataValues.id}t${row.dataValues.name}`);
    }
    sequelize.close();
  });

With Bookshelf:

#!/usr/bin/env node

// $ npm install bookshelf knex pg

const connection = require('./connection.json');
const knex = require('knex')({
  client: 'pg',
  connection,
  // debug: true
});
const bookshelf = require('bookshelf')(knex);

const Item = bookshelf.Model.extend({
  tableName: 'item'
});

// select "item".* from "item" where "type" = ?
Item
  .where('type', 'veg')
  .fetchAll()
  .then(result => {
    console.log('Veggies:');
    for (let row of result.models) {
      console.log(`${row.attributes.id}t${row.attributes.name}`);
    }
    knex.destroy();
  });

With Waterline:

#!/usr/bin/env node

// $ npm install sails-postgresql waterline

const pgAdapter = require('sails-postgresql');
const Waterline = require('waterline');
const waterline = new Waterline();
const connection = require('./connection.json');

const itemCollection = Waterline.Collection.extend({
  identity: 'item',
  datastore: 'default',
  primaryKey: 'id',
  attributes: {
    id: { type: 'number', autoMigrations: {autoIncrement: true} },
    name: { type: 'string', required: true },
    type: { type: 'string', required: true },
  }
});

waterline.registerModel(itemCollection);

const config = {
  adapters: {
    'pg': pgAdapter
  },

  datastores: {
    default: {
      adapter: 'pg',
      host: connection.host,
      port: connection.port,
      database: connection.database,
      user: connection.user,
      password: connection.password
    }
  }
};

waterline.initialize(config, (err, ontology) => {
  const Item = ontology.collections.item;
  // select "id", "name", "type" from "public"."item"
  //     where "type" = $1 limit 9007199254740991
  Item
    .find({ type: 'veg' })
    .then(rows => {
      console.log('Veggies:');
      for (let row of rows) {
        console.log(`${row.id}t${row.name}`);
      }
      Waterline.stop(waterline, () => {});
    });
});

With Objection:

#!/usr/bin/env node

// $ npm install knex objection pg

const connection = require('./connection.json');
const knex = require('knex')({
  client: 'pg',
  connection,
  // debug: true
});
const { Model } = require('objection');

Model.knex(knex);

class Item extends Model {
  static get tableName() {
    return 'item';
  }
}

// select "item".* from "item" where "type" = ?
Item
  .query()
  .where('type', '=', 'veg')
  .then(rows => {
    for (let row of rows) {
      console.log(`${row.id}t${row.name}`);
    }
    knex.destroy();
  });

The syntax for a simple read operation varies greatly between these examples. As the operation you’re trying to perform increases in complexity, such as operations involving multiple tables, the ORM syntax will vary from between implementations even more.

There are at least dozens of ORMs for Node.js alone, and at least hundreds of ORMs for all platforms. Learning all of those tools would be a nightmare!

Lucky for us, there are really only a few SQL dialects to worry about. By learning how to generate queries using raw SQL you can easily transfer this knowledge between different platforms.

2. Complex ORM calls can be inefficient

Recall that the purpose of ORM is to take the underlying data stored in a database and map it into an object that we can interact within our application. This often comes with some inefficiencies when we use ORM to fetch certain data.

Consider, for example, the queries we first looked at in the section on layers of abstraction. In that query, we simply wanted a list of ingredients and respective quantities for a particular recipe. First, we made the query by writing SQL by hand. Next, we made the query by using the query builder, Knex. Finally, we made a query by using the ORM Sequelize. Let’s take a look at the queries which have been generated by those three commands.

Handwritten with pg driver.

This first query is exactly the one we wrote by hand. It represents the most succinct method to get exactly the data we want:

SELECT
  ingredient.*, item.name AS item_name, item.type AS item_type
FROM
  ingredient
LEFT JOIN
  item ON item.id = ingredient.item_id
WHERE
ingredient.dish_id = ?;

When we prefix this query with EXPLAIN and send it to the PostgreSQL server, we get a cost operation of 34.12.

Generated with Knex

This next query was mostly generated for us, but due to the explicit nature of the Knex query builder, we should have a pretty good expectation of what the output will look like:

select
  *, "item"."name" as "item_name", "item"."type" as "item_type"
from
  "ingredient"
left join
  "item" on "item"."id" = "ingredient"."item_id"
where
"dish_id" = ?;

New lines have been added by me for readability. Other than some minor formatting and unnecessary table names in my handwritten example, these queries are identical. In fact, once the EXPLAIN query is run, we get the same score of 34.12.

Generated with the Sequelize ORM

Now let’s take a look at the query generated by ORM:

SELECT
  "dish"."id", "dish"."name", "dish"."veg", "items"."id" AS "items.id",
  "items"."name" AS "items.name", "items"."type" AS "items.type",
  "items->ingredient"."dish_id" AS "items.ingredient.dish_id",
  "items->ingredient"."item_id" AS "items.ingredient.item_id",
  "items->ingredient"."quantity" AS "items.ingredient.quantity",
  "items->ingredient"."unit" AS "items.ingredient.unit"
FROM
  "dish" AS "dish"
LEFT OUTER JOIN (
  "ingredient" AS "items->ingredient"
  INNER JOIN
  "item" AS "items" ON "items"."id" = "items->ingredient"."item_id"
) ON "dish"."id" = "items->ingredient"."dish_id"
WHERE
"dish"."id" = ?;

New lines have been added by me for readability.

As you can tell this query is a lot different from the previous two queries. Why is it behaving so differently? Well, due to the relationships we’ve defined, Sequelize is trying to get more information than what we’ve asked for. In particular, we’re getting information about the dish itself when we really only care about the ingredients belonging to that dish.

The cost of this query, according to EXPLAIN, is 42.32.

3. ORM can’t do everything

Not all queries can be represented as an ORM operation. When we need to generate these queries, we have to fall back to generating the SQL query by hand. This often means a codebase with heavy ORM usage will still have a few handwritten queries strewn about it. The implications here are that, as a developer working on one of these projects, we end up needing to know BOTH the ORM syntax as well as some underlying SQL syntax.

A common situation that doesn’t work too well with ORMs is when a query contains a subquery. Consider the situation where I know that I have already purchased all the ingredients for dish #2 in our database, however, I still need to purchase whatever ingredients are needed for dish #1. In order to get this list I might run the following query:

SELECT *
FROM item
WHERE
  id NOT IN
    (SELECT item_id FROM ingredient WHERE dish_id = 2)
  AND id IN
(SELECT item_id FROM ingredient WHERE dish_id = 1);

To the best of my knowledge, this query cannot be cleanly represented using the aforementioned ORMs. To combat these situations it’s common for ORM to offer the ability to inject raw SQL into the query interface.

Sequelize offers a .query() method to execute raw SQL as if you were using the underlying database driver. With both the Bookshelf and Objection ORMs, you get access to the raw Knex object which you provide during instantiation and can use that for its query builder powers.

The Knex object also has a .raw() method to execute raw SQL. With Sequelize, you also get a Sequelize.literal() method which can be used to intersperse raw SQL in various parts of a Sequelize ORM call.

But in each of these situations, you still need to know some underlying SQL to generate certain queries.

Why use Sequelize in the first place?

In previous sections, we gave reasons to why ORM may not be required. However, as the most popular Node.js ORM and with around 1.35 million weekly downloads at the time of writing this article, Sequelize puzzles us with its popularity.

Although we made valid points in the previous section, we have to note that 1.35 million people cannot be wrong weekly. So let’s look at some of the reasons one might use Sequelize in the first place:

  • Sequelize can layer over a wide range of databases such as Oracle, Postgres, MySQL, MariaDB, SQLite, DB2, Microsoft SQL Server, and Snowflake
  • Sequelize supports raw queries. This enables developers to balance things by providing the sequelize.query method for writing raw queries where this is preferred
  • It’s easy to use
  • It has solid transaction support
  • It guards against SQL injection vulnerabilities
  • It has model validation
  • It comes with TypeScript support

 Using query builders in Node.js: The sweet spot

Using the low-level database driver modules is rather enticing. There is no overhead when generating a query for the database as we are manually writing the query. The overall dependencies our project relies upon are also minimized. However, generating dynamic queries can be very tedious, and in my opinion, is the biggest drawback of using a simple database driver.

Consider, for example, a web interface where a user can select criteria when they want to retrieve items. If there is only a single option that a user can input, such as color, our query might look like the following:

SELECT * FROM things WHERE color = ?;

This single query works nicely with the simple database driver. However, consider if the color is optional and that there’s a second optional field called is_heavy. We now need to support a few different permutations of this query:

SELECT * FROM things; -- Neither
SELECT * FROM things WHERE color = ?; -- Color only
SELECT * FROM things WHERE is_heavy = ?; -- Is Heavy only
SELECT * FROM things WHERE color = ? AND is_heavy = ?; -- Both

However, due to the aforementioned reasons, a fully featured ORM isn’t the tool we want to reach for either.

The query builder ends up being a pretty nice tool in these situations. The interface exposed by Knex is so close to the underlying SQL query that we are forced to always know what the SQL query looks like. This relationship is similar to how something like TypeScript translates to JavaScript.

Using a query builder is a fine solution as long as you fully understand the underlying SQL it’s generating. Never use it as a tool to hide from what is happening at a lower layer. Only use it as a matter of convenience and in situations where you know exactly what it’s doing.

If you ever find yourself questioning what a generated query actually looks like you can add a debug field to the Knex() instantiation call. Doing so looks like this:

const knex = require('knex')({
  client: 'pg',
  connection,
  debug: true // Enable Query Debugging
});

In fact, most of the libraries mentioned in this post include some sort of method for debugging the calls being executed.

So…should I ever use ORMs?

From our discussion so far, we have seen the weak points of ORM. However, in this subsection, we will look at certain scenarios where ORM is a good choice.

Generally, since ORMs provide a high-level abstraction over a database, it offers less control than a raw query. And for this reason, it is slower in performance than raw query. But to make up for these downsides, ORM often ships with a lot of great features:

  • Easy portability across multiple databases
  • Code generation. In a complex project with a fairly large team, there are chances that the database changes rapidly. The ability to regenerate classes and mappings from the database as part of the build process is highly needed. Your code may not be the fastest if you use ORM, but your coding will be
  • ORM tool frees you from writing boilerplate SQL queries and keeps your code DRY, thereby enabling you to concentrate on the problem domain and speeding up the development process

In summary, use ORM to achieve code standardization, security, maintainability, language abstraction, DRY, etc.

Which is the “best” ORM for Node.js?

So far, we have learned about some Node.js ORM and query builders. However, picking the “best” ORM for Node.js is not easy because they all have pros and cons. And in my opinion, the best Node.js ORM changes depending on your application needs.

Previously, we classified libraries used to query and manipulate data into three categories based on their level of abstraction:

  • Low-level: database drivers that are the fastest and offer the most control. However, concatenating strings together to form SQL is tedious and can lead to security vulnerabilities
  • Mid-level: query builders, like Knex.js, operate at a level above database clients. And they enable you to programmatically generate dynamic queries in a more convenient way. They are also fairly fast, but when working with complex relations, Knex can be tedious and time-consuming
  • Highest-level: ORMs like Sequelize that layer over databases and provide simple APIs for performing operations on the database. These are the slowest but they are feature-rich, and their simple and powerful API means a better developer experience and an increase in the speed of development

Since there is no ORM that solves all your problems, the best ORM is the ORM that is most suitable for your application need. And with the information above, you should know what you are willing to trade and what your application requires. Consequently, you can choose which ORM is best for your Node.js application.

Conclusion

We’ve looked at three different layers of abstracting database interactions, namely the low-level database drivers, query builders, and the high-level ORMs.

We’ve also examined the tradeoffs of using each layer as well as the SQL queries being generated, including the difficulty of generating dynamic queries with a database driver, the added complexity of ORMs, and finally the sweet spot of using a query generator.

Thank you for reading and be sure to take this into consideration when you build your next project.

Once you’re done following along, you may run the following commands to completely remove the docker container and remove the database files from your computer:

200’s only Monitor failed and slow network requests in production

Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third party services are successful, try LogRocket. LogRocket Network Request Monitoringhttps://logrocket.com/signup/

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens while a user interacts with your app. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.

LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.
docker stop pg-node-orms
docker rm pg-node-orms
sudo rm -rf ~/data/pg-node-orms
Thomas Hunter II Thomas has contributed to dozens of enterprise Node.js services and has worked for a company dedicated to securing Node.js. He has spoken at several conferences on Node.js and JavaScript, is the author of Distributed Systems with Node.js, and is an organizer of NodeSchool SF.

79 Replies to “Node.js ORMs: Why you shouldn’t use them”

  1. It’s true! ORM (object-relational mapping) doesn’t fit to the current needs of coding. And when it comes to Node.js ORMs, things get complicated and needs simplified solution. Therefore, i find this article good in this case.

  2. Reason one is that ORMs are some sort of super complex thing to learn, when in reality the entire reason we use them is because they’re much more developer friendly than interacting with the database directly.

    Reason two says they’re inefficient, but the reality is that the speed of our code running isn’t the speed developers should be concerned with first and foremost. Smart developers are more concerned with how they can be efficient as developers (how efficiently they can write the code), and ORMs are certainly a better solution for being efficient as a code writer. Premature optimization is one of the worst mistakes a developer can make, choosing to avoid a tool because you’re afraid it might be slow can cost you countless hours for no benefit at all.

    And for reason three, yes an ORM can’t do everything, but it can do everything that the majority of developers will ever need it two. It is foolish to avoid a tool because you think it can’t do something that you never actually.

    This is such a joke, the fact that this article is the first result in google when searching “Node ORM” completely insane. It’s so ridiculously misleading, not only because the three reasons aren’t really valid to most developers, but for the simple fact that you’re misleading developers on how they should be choosing their tools and misleading them to worry about things that don’t matter most of the time.

    Making your code efficient is not nearly as important as being efficient while making your code (in most cases). Avoiding tools because they don’t have a feature that you’ll never need is illogical. AND FOR GODS SAKES ORMS ARE NOT HARDER TO USE THAN DIRECT DATABASE INTERACTION (for most people). This is such a shallow, one sided article, and it triggers me.

    1. There are some good points here but the fundamental premise of your argument seems to be that most developers will never need to do anything an ORM can’t do. You’ve said a version of that repeatedly in your comment. I think this is your own bias.

      Some people are fluent in SQL before they ever touch an ORM and those people will be acutely aware of the points made in this article. In the inverse, someone who is comfortable doing ordinary things with an ORM is immediately thrown in the deep end of the underlying query language when they have to do something a little out of the ordinary. In my experience there’s a point where ORMs become more verbose or convoluted than the underlying SQL and that point is often immediately beyond the mundane.

      Furthermore, you misunderstand the implications of point number two. If you’re using a frontend framework like React or Vue, you often want a subset of data to send to the browser. The SELECT statement from the ORM is returning a bunch of extraneous data so unless you want to unnecessarily send it all to the browser you must prune it. So it’s not just a matter of prematurely optimizing, it’s the fact that the ORM is giving you data you don’t want which you have to deal with in code.

      Maybe you’ve never found a need to do something that the ORM can’t do but what makes you think you’re in the majority? Maybe you don’t have experience with this topic but maybe many people do and it resonates with them and that’s why it ranks so high with the search engine

    2. I don’t remember ever feeling the urge to give in to comments, but this takes the cake and I can’t resist. I have to state right away, that I’ve been coding for over 30 years with the last 15 mostly in web-dev. This article is extremely important, and possibly much more than anyone can realize. I admit that I’m old-school and prefer doing everything myself (but this also stems from the type of projects I worked on, in which 3rd-party modules aren’t permitted) but nonetheless, introducing an ORM of any sort is abstraction. Going by good software-engineering practices, abstraction can never be a positive approach, be it in imperative or declarative paradigms. On the other hand, in projects that had unrealistic deadlines and weren’t tied with no 3rd-party constraints, I’ve used Knex on Node, and, its abstraction layer (if used correctly) can be close to negligible for performance tests & query optimization. If you start off by disregarding the queries costs, you’re asking for trouble, or, you’re not proud of the work you produce. And oh, last thing, if my response is equally condescending, it’s cause it stems from the educator in me (as in the past 5 years I’ve shared my knowledge with hundreds of CS & General Programming students in several colleges in the US and Europe), which riles me up to see such attitude!!

      1. Not to disparage your experience, but “abstractions never being a positive approach” is just plain silly. It’s abstractions that allow us to create hugely complex solutions in a reasonable time in the first place.

        Everything you are using today is working on tons of layers of abstractions, from your BIOS through your OS and the applications on it. Why are you even developing in NodeJS when you could be writing assembly or even better, just inputting 1s and 0s through some morse-code like device instead of a keyboard?

        This is completely tangential to the ORM discussion, there are pros and cons to using one and you should look at your use case to figure out if the pain is worth the gain. Blanket statements and oversimplifications don’t help.

        1. I was going to write a similar response to this. Even rust and c are abstractions. I learned this in my first high school programming class. Anyone with an opinion against abstractions is misguided. Everything we do in coding is creating abstractions. Think about it!

        2. I think he’s talking about “abstraction for its own sake”. Abstraction makes sense in many cases, leads to scaleability, cleaner code and often looser coupling of artifacts.
          But, thinking about ORMs as the possibility to relieve you from learning SQL – this is no needed abstraction. The code don’t look better, the performance of the software won’t get better and you have more dependencies.

          I just use a simple code generator to generate the data Objects from the database definition with some often used CRUD functions to persist and retrieve the data. When i need some more complex SQL statements, I just add them in the code to the generated functions.
          I think, this approach is fast, because 90% of the code is generated, it’s light weight and the code is clear.

  3. Dude you’re being super condescending. You should read this out loud, is that the way you would talk with someone in person, or is this just internet tough guy taking a stance on… ORMs… If you can’t read a blog post about something this low stakes without talking down to someone, maybe you considering avoiding things that trigger you so much.

  4. Hi,
    There is little research on which technique is faster. Intuitively, Raw SQL should be faster than Eloquent ORM, but exactly how much faster needs to be researched. In particular, when one uses Raw SQL over Eloquent ORM, one makes a trade-off between ease of development, and performance. Will you write some content on it for my understanding .
    Thanks

  5. It’s true! ORM (object-relational mapping) doesn’t fit to the current needs of coding. And when it comes to Node.js ORMs, things get complicated and needs simplified solution. Therefore, i find this article good in this case.

  6. Yes, it is correct! ORM (object-relational mapping) does not meet today’s coding requirements. And when it comes to Node.js ORMs, things get a little more involved, necessitating a more straightforward approach. As a result, I believe this post is appropriate in this situation.

  7. I like how awesome ORMs can be but after trying to use PrismaJS and right from the start run into bugs that have already been reported by other users made me turn back on the choice to use one. When getting started with a ORM provides a smooth path to application development (like Mongoose), its great. But when it’s not smooth, it doesn’t seem like it’s worth the time to fight with the ORM (or go looking for others) instead of just writing raw queries.

  8. Very useful article thank you! For someone new to database access in Node generally, this is a great survey of the different options for doing so and some valid critiques on each.

    I would add that the argument would be much weaker if the Node ORM space was more mature. (Maybe this is because so much Node development just defaults to MongoDB – which is a topic for a post all of its own!) The best ORM I have worked with is Django’s. It is extremely capable at constructing complex queries, including injecting aggregates, subqueries, and the like, and in the last few years I’ve not needed to resort to plain SQL to generate optimal queries. The main advantage of this is isolation from the schema details – much can be done to the schema without requiring code changes elsewhere. Another is use of the schema definitions in, for example, automatically generating REST APIs, HTML forms, test data generation, and so on, with code that is completely generic to model. One tiny point relevant to the above, is an expertly written ORM can be more efficient than an average, hand crafted one, particularly at populating objects (although granted this is more the case for strongly typed languages). One final notable one is migrations. Gone are the days of having to maintain both the current schema, and incremental version update scripts. If the Node ecosystem ever gets an ORM offering these, without the performance compromises of Sails+Waterline you mention, (with luck), this article could need revising.

  9. I typed “node js orm” and landed here. It isn’t what I was looking but it is what I need. The article made marvelous work trying to convince why to choose query builder instead of orm.
    I’m glad I read it, it helped me make my mind. I found it really practical to query only exactly what I want. Also I read a bit comments, and I think most of them miss the point, why JS developers pick ORM in first place. Nodejs is specific tool, often chosen by FE developers that have limited knowledge about databases.
    ORM allow developers to use db not knowing it. I think this article is solid to explain why it worth to learn database, not ORM.

  10. I am using Sequelize at the moment and it is a learning curve coming from using JPA on Java. It is quite different in the way it lazy fetches relationships and I cannot figure out how to create an @OrderColumn. But going back to pure SQL queries seems like a step backwards. You have to understand how the ORM works in order to avoid big queries

  11. It’s rare when a blog article so succinctly answers the exact question one is pondering. Thank you.

  12. This is a wildly cold take. some ORMs are better than others, but a good ORM and knowledge of how to use it correctly can make your code much more sustainable long term, not to mention making your application as a whole more DRY.

    If your main selling point is “learn SQL over an ORM” then your issue is your existing knowledge, not the ORM.

    ORMs don’t do everything for you, but as always an application has a million little pieces and an ORM is a very important piece of the puzzle.

  13. Yes, I agree. Many people use ORM wrong way – as a silver bullet for everything. The core task of ORM is to let the programmer write SQL queries by hand and map the result to object(s). Many people forget it – no automatic queries building, no relationship. Then, the second core task is to do simple CRUD. Very useful is, if ORM support queries with named parameters. And that’s enough to have a good ORM. See C# Dapper ORM! (Is there any JS ORM like Dapper?)

    All other stuff – query builder, relations ships, etc. use very cautiously. Even Gavin King, authot of the famous Java Hibernate ORM said, that ORM is to do 50-90% of your tedious work, never try to use it for everything.

  14. I don’t k ow I have to the node ecosystem. I’ve been using jpa for years, and it does the job effectively. And when it gets to complicated, you can always write raw Sql. Just in case you need to dust off your Sql knowledge. So, I disagree a lot with this post.

  15. Whenever I see someone talk about performance without providing actual benchmarks, I know they are talking nonsense. The difference in raw vs generated SQL could be minimal, and it would not matter anyway in most cases — unless you are working on an e-commerce website or writing a server that handles 30K requests per second, performance is likely not the first thing you need to worry about.

Leave a Reply