Nodebestpractices: How to structure your solution by components for project that uses an ORM?

Created on 21 Jun 2018  路  14Comments  路  Source: goldbergyoni/nodebestpractices

Most project using popular ORMs like Sequelize, Mongoose, or Typeorm, typically has a models folder. How would go structure it by components?

Hot Discussion new best practice question

Most helpful comment

Good one.

Each component is 100% a stand-alone application with its own models. Shared logic can be saved reused via npm package as illustrated in 1.3.. The major principle should be keeping your DAL as a data-mapper that maps domain entities into DB and NOT owning the canonical application entities. In other words, you have the domain models, simple JS objects that are agnostic to any DB concerns, which are the heart of the system - the DAL is responsible to serialize this model into DB. If you use ORM than that DAL will use its own internal models for the mapping. This is being referred to as the 'data mapper' pattern. What developers usually use is the 'active record' pattern (ORM model is the main entity), there are many blog posts on why data-mapper should be used instead of active record

Code example using Sequelize:

Defining a domain model/entity which is absolutely DB agnostic

const JSONValidator = require("jsonschema").Validator;
const CancelReasonEnum = require('./cancel-reason-enum');
const orderSchema = require('./order-schema.json');

class Order {
  constructor() {

  }

  validate() {
    const v = new JSONValidator();
    const {errors} = v.validate(this, orderSchema);

    return errors;
  }

}

module.exports = Order;

The domain service which uses the DAL for any persistence operations:

addRawOrder(newOrder) {
    logger.info(`Order service is about to add a raw order ${newOrder}`, this.context);
    if (!newOrder.validate().errors > 0) throw new errorHelpers.commonErrors.InvalidInputError(`Invalid Order`);

    return new Repository().addOrder(newOrder);

  }

And finally, the repository which is an encapsulated object, separated from the domain, that internally is using Sequelize for persistence:

const Order = dbAccess.sequelizeDBReference.define("order", {
  id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true },
  date: { type: Sequelize.DATE, defaultValue: Sequelize.NOW },
  order_price: { type: Sequelize.TINYINT },
});

class OrderRepository {
  async addOrder(newOrder) {
    await Order.create(newOrder, {
      include: [PaymentTable],
    });
  }


Does this make sense? Do you think we should add a new best practice - 'Prefer a data mapper over active record'?

All 14 comments

Good one.

Each component is 100% a stand-alone application with its own models. Shared logic can be saved reused via npm package as illustrated in 1.3.. The major principle should be keeping your DAL as a data-mapper that maps domain entities into DB and NOT owning the canonical application entities. In other words, you have the domain models, simple JS objects that are agnostic to any DB concerns, which are the heart of the system - the DAL is responsible to serialize this model into DB. If you use ORM than that DAL will use its own internal models for the mapping. This is being referred to as the 'data mapper' pattern. What developers usually use is the 'active record' pattern (ORM model is the main entity), there are many blog posts on why data-mapper should be used instead of active record

Code example using Sequelize:

Defining a domain model/entity which is absolutely DB agnostic

const JSONValidator = require("jsonschema").Validator;
const CancelReasonEnum = require('./cancel-reason-enum');
const orderSchema = require('./order-schema.json');

class Order {
  constructor() {

  }

  validate() {
    const v = new JSONValidator();
    const {errors} = v.validate(this, orderSchema);

    return errors;
  }

}

module.exports = Order;

The domain service which uses the DAL for any persistence operations:

addRawOrder(newOrder) {
    logger.info(`Order service is about to add a raw order ${newOrder}`, this.context);
    if (!newOrder.validate().errors > 0) throw new errorHelpers.commonErrors.InvalidInputError(`Invalid Order`);

    return new Repository().addOrder(newOrder);

  }

And finally, the repository which is an encapsulated object, separated from the domain, that internally is using Sequelize for persistence:

const Order = dbAccess.sequelizeDBReference.define("order", {
  id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true },
  date: { type: Sequelize.DATE, defaultValue: Sequelize.NOW },
  order_price: { type: Sequelize.TINYINT },
});

class OrderRepository {
  async addOrder(newOrder) {
    await Order.create(newOrder, {
      include: [PaymentTable],
    });
  }


Does this make sense? Do you think we should add a new best practice - 'Prefer a data mapper over active record'?

@chanlito The bottom line here is that each component has its own DB folder with all the DB concerns to keep the components autonomous and share nothing. Any further thoughts or can I close this issue?

@i0natan I feel that this way leads to repetitive code, right?

Does this best practice address this concern? @chanlito

Yeah but I don't think private npm is a solution for everyone, you can close this if you want.

You don't have two use a private npm, just put the reusable stuff in a shared/libraries folder outside the comoonents

@i0natan I am beginner developer, so please forgive if this is too trivial. Could you please share how to do this is in Mongoose, similar to the example using sequelize?

@lightbringer2994 Welcome and nothing is trivial here. I don't have mongoose examples ready, also on a travel so won't have the time to create some but maybe the following instructions might help:

  1. Create the DAL layer internal - use mongoose as illustrated in the typical tutorial, define your models, define CRUD, etc.
  2. Wrap it as a class with public methods - all the operations that you created in step 1', should be exposed via class methods, those methods will receive entities/models which are not related to mongoose - you'll have to map them to the mongoose models, this is why this layer is called the data mapper e.g.
    class myDAL{ addNewUser(user){//your mongoose code here, accepting a user object and converting/mapping it to the user model } }

  3. Create your domain/business-logic/core layer - create all the entities (i.e. models) again, this time without any reference to some library like mongoose rather plain JS objects. Write your custom code and features, whenever you need some db services, approach the DAL from object 2'

The idea here to isolate your app core (domain) from step 3', from the persistence layer that saves it into db at step 2

@i0natan Is there a good open source project I can look at which implements this?

@lightbringer2994 I'm not familiar with one, I suggest that you read about the data mapper pattern (vs active record) and try to implement yourself. Feel free to ask here and share snippets

/remind me in a week

@i0natan set a reminder for Oct 14th 2018

Closing as it seems to address most concerns, sorry I couldn't help further with the Mongoose thing

:wave: @i0natan,

Was this page helpful?
0 / 5 - 0 ratings

Related issues

noway picture noway  路  3Comments

YukiOta picture YukiOta  路  3Comments

bikingbadger picture bikingbadger  路  6Comments

heyfirst picture heyfirst  路  6Comments

halfzebra picture halfzebra  路  4Comments