post-office

A toolkit which provides a strongly-typed message mapping to internal service calls.

Usage no npm install needed!

<script type="module">
  import postOffice from 'https://cdn.skypack.dev/post-office';
</script>

README

Post Office Logo

NPM Version NPM Downloads

A toolkit which provides a strongly-typed message mapping to internal service calls.

What's the point?

The idea behind post-office is to allow application developers to work with strongly-typed messages and focus on service-level implementation code. Post Office was inspired by the idea of Data Transfer Objects.

For example, consider a service to save a User. A current implementation with no service layer may look like this:

app.post('/users', function(req, res, next) {

  // validation logic...
  // sanitization logic

  // Begin "Service-ish Code"

  User
    .exists(req.body.email, function(err, exists) {
      if (err) {
        return next(err);
      }

      if (exists) {
        return next(new Error('User exists'));
      }

      User
        .create({
          name: req.body.name,
          email: req.body.email,
          address: req.body.address
        })
        .save(function(err, user) {
          if (err) {
            return next(err);
          }

          res.status(201).json({user:user});
        });
    });
});

There are a lot of reasons why this code is less than optimal. For one, this is far from unit-testable. Another is the mixing of different logics such as parsing, validation, and sanitization. Finally, any changes to what should be returned for a user need to be reimplemented on every method that returns a user. The issues quickly become apparent as the project grows in size.

Ideally, I should be able to break down my service code into a very simple collection of methods. This is where the idea of message-based design really shines. In message-based design, you write services that will accept a plain object, and will return some kind of plain object. This object will be verified to look a certain way before entering the service layer, and will be verified to look a certain way when leaving the service layer. Here is an example of the same scenario implemented by a user service with message-based design:

// Example User Service

exports.saveUser = function(msg) {
  return User
    .exists(msg.email)
    .then(function(exists) {
      if (exists) {
       throw new Error('User exists');
      }
    })
    .then(function() {
      return User.create({
        name: msg.name,
        email: msg.email,
        address: msg.address
      });
    })  
    .then(function(user) {
      return {user: user};
    });
};

As you can see: no more req, res, or next. Testing this code is much more straight forward. Errors are simple. We just worry about the business logic, and we let some magic do the rest (mapping, validation, error handling, etc). Post Office aims to be some magic. It is a toolkit that allows you to build "strongly-typed" messages (it's javascript so "strongly-typed" has a somewhat different meaning here) for your services.

Quick Example

var app = require('express')();
var postOffice = require('post-office');
var PropTypes = postOffice.PropTypes;

// 1. create a container

var c = postOffice.createContainer();

// 2. make request envelope

var saveUserEnv = postOffice.createEnvelope({
  name: PropTypes.string,
  email: PropTypes.string,
  favoriteNums: [PropTypes.int]
});

// 3. make response envelope

var saveUserResponseEnv = postOffice.createEnvelope({
  name: PropTypes.STRING,
  email: PropTypes.STRING,
  favoriteNums: [PropTypes.INT],
  updatedBy:PropTypes.INT
});

// 4. implement service code

var saveUser = function(msg) {
  // do some magic to save user

  return Promise.resolve({
    name: msg.name,
    email: msg.email,
    id: 1,
    updatedBy: 12345
  });
};

// 5. tie it together and build expressjs handler

app.post('/users', c(saveUserEnv, saveUser, saveUserResponseEnv).status(201).json());

/**
 * POST /users { "name": "Tyler", "email": "tyler@goguardian.com", "favoriteNums": [22, "33"] }
 *
 * Response: { "name": "Tyler", "id": 1, "email": "tyler@goguardian.com", "favoriteNums": [22, 33], "updatedBy": 12345 }
 */

Documentation

Learn more on the wiki.