The Fun of Functional (Part 3)

by Rob Zoszak
25 May 20223 min read

Good code requires the right abstraction. In the last post we saw how using the single responsibility principal at the function level can help us achieve more readable and testable code. In this post we'll elevate that idea to our modules.

Onward!

Let's write a hello world transport/business layer ready for adoption in our fictional todo API.

Our dependencies:

npm i restana --save

  • Restana is webserver written in NodeJS. According to them it's fast and efficient. I'm not in the business of writing that stuff myself so I'll take their word for it... You could also use Koa or Express.

Let's find our feet!

When I open an existing API project that I've not worked on before, I look for a file which looks something like this:

src/api/transport.js

const http = require('http');
const app = require('restana')();
const hello = require('./business/hello');

// Our router. It's easy to get our bearings and see what this api looks like from the outside world.
app.get('/hello', hello);

// Could move this but we'll keep it here for completeness
http.createServer(app).listen(8080);

It's very basic stuff, you will have seen this before?

Let's highlight the line

app.get('/hello', hello);
. The hello function implementation is not within this file! The important point is that we're separating concerns. Later in the series we'll add more routes to this file, we don't want to get bombarded with too many details. The single responsibility of the transport layer is to define routes.

Let's take a quick peek at the business.hello implementation:

const send200 = (res) => (msg) => res.send(msg, 200);
module.exports = (req, res) => send200(res)({ hello: 'world' });

We can abstract out the

send200
to a utils module where it can be reused. Let's do that now:

const sendify = (status) => (res) => (msg) => res.send(msg || null, status);

module.exports.send200 = (res) => sendify(200)(res);
module.exports.send404 = (res) => sendify(404)(res);
// ... etc

There's our functional composition trick in action again! I'll let you digest that one in your own time.

Notes:

  • Util functions are usually written and not looked at again, which is the point. It's good to test these functions thoroughly while writing them. Aim for a clear usage pattern which is documented by their usage elsewhere in the code.
  • Modifying utils functions is risky. It's best to use the open/closed principle whereby we are open for extension but closed for modification.

...and using our new utils module, here's how our business layer looks:

const { send200 } = require('../utils');
module.exports = (req, res) => send200(res)({ hello: 'world' });

But wait a second...

Usually a business layer needs to do something asynchronous. The structure of our utils helper functions will make our final code business look nice and neat:

const { send200 } = require('../utils');

const asMessage = () => ({ hello: 'world' });

module.exports = (req, res) =>
  Promise.resolve()
    .then(asMessage)
    .then(send200(res));

It doesn't look like much but there's a lot going on if you're not familiar with the style we're using. By setting up a promise chain we can pass our data down from top to bottom. Functional composition allows us to push function calls into the chain and keep the code clean and easy to read, almost like plain english at times.

In closing

We've created three files here today:

  • src/api/transport.js
    • holds our routing information. At a later stage we can throw some middleware in here too.
  • src/api/utils.js
    • holds our common module functions.
    • open for extension, closed for modification.
  • src/api/business/hello.js
    • holds our implementation details.

P.S here's a small test suite used to make sure all was well in the world.

const business = require('./src/api/business');

const throwError = (t) => {
  throw new Error(`Test #${t} failed`);
};
const assert = (t) => (a) => (b) => a === b || throwError(t);

const mockRes = (send) => ({ send });

assert('1')('function')(typeof business);

const testHello = () => {
  const onSend = ({ hello }) => assert('2')(hello)('world');
  const res = mockRes(onSend);
  return business(null, res);
};

Promise.resolve() //
  .then(testHello)
  .then(() => console.log('pass'));

Sign up and stay in the loop!

Your email address

No spam and no sharing of your details. Just useful thoughts and ideas in your inbox. :)