The Fun of Functional (Part 4)

by Rob Zoszak
01 June 20224 min read

If you've been following the series and you like what you see, there's a good chance we'd like to hear from you. Airteam is hiring! Drop Jonny a line and tell him I sent you.

In this post we're going to bring it on home by joining our data/business/transport layers together to create our fictional todo API.

Let's cut a rug!

Our dependencies:

npm i body-parser --save

  • body-parser is a common middleware for formatting our API payloads into a json. Express also uses this middleware.

Firstly, let's modify our transport layer to include a

post
route which will be used to
add
a todo.

const http = require('http');
const bodyParser = require('body-parser');
const app = require('restana')();
const todo = require('./business');

app.use(bodyParser.json());

app.post('/:scope', todo.add);

http.createServer(app).listen(8080);

...and a quick sanity check inside our business layer:

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

const asMessage = (req) => () => ({ params: req.params, body: req.body });

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

Time to do

While we're still in our business layer, let's add our data layer along with

add
function.

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

const asMessage = (data) => ({ ...data });
const asApi = (req) => createApi(req.scope);

// adding the add function
const addify = (req) => asApi(req).add(req.body);

module.exports.add = (req, res) =>
  addify(req) //
    .then(asMessage)
    .then(send200(res));

Looks ok, I'm not sure I like where it's heading though. There could be a lot of repetition. We'll end up duplicating the

asMessage
and
send200
code multiple times for each implementation. Let's continue and see how it goes.

Next, we'll add the remaining routes to the transport layer:

const http = require('http');
const bodyParser = require('body-parser');
const app = require('restana')();
const todo = require('./business');

app.use(bodyParser.json());

app.get('/:scope', todo.browse);
app.get('/:scope/:id', todo.read);
app.patch('/:scope/:id', todo.edit);
app.post('/:scope', todo.add);
app.delete('/:scope/:id', todo.del);

http.createServer(app).listen(8080);

...and fill out the business layer:

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

const asMessage = (result) => ({ result });
const asApi = (req) => createApi(req.scope);

const browsify = (req) => asApi(req).browse();
const readify = (req) => asApi(req).read(req.params.id);
const editify = (req) => asApi(req).edit(req.params.id)(req.body);
const addify = (req) => asApi(req).add(req.body);
const delify = (req) => asApi(req).del(req.params.id);

const browse = (req, res) =>
  browsify(req) //
    .then(asMessage)
    .then(send200(res));

const read = (req, res) =>
  readify(req) //
    .then(asMessage)
    .then(send200(res));

const edit = (req, res) =>
  editify(req) //
    .then(asMessage)
    .then(send200(res));

const add = (req, res) =>
  addify(req) //
    .then(asMessage)
    .then(send200(res));

const del = (req, res) =>
  delify(req) //
    .then(asMessage)
    .then(send200(res));

module.exports = { browse, read, edit, add, del };

Not too bad, time to call it a night? Not so fast!

  • Our restful API is not handling return codes correctly.
  • As we suspected, there is a lot of repetition here. Maybe we could reduce it down a bit?

Let's try something else in the business layer and see how it feels:

const { maybe200, maybe204 } = require('./utils');
const { createApi } = require('./data');

const asApi = (req) => createApi(req.scope);

const from = ({ params, body }) => {
  const { id, scope } = params;
  const api = asApi(scope);
  return {
    browse: () => api.browse(),
    read: () => api.read(id),
    edit: () => api.edit(id)(body),
    add: () => api.add(body),
    del: () => api.del(id),
  };
};

const browse = (req, res) => from(req).browse().then(maybe200(res));
const read = (req, res) => from(req).read().then(maybe200(res));
const edit = (req, res) => from(req).edit().then(maybe200(res));
const add = (req, res) => from(req).add().then(maybe200(res));
const del = (req, res) => from(req).del().then(maybe204(res));

module.exports = { browse, read, edit, add, del };

Notes:

  • There is a lot less code but it's potentially over abstracted. Overly abstracted code can also be rigid. You be the judge :)
  • We're dealing with the return status code via maybe200, let me show you what that looks like in the utils module:
const sendify = (status) => (res) => (msg) => res.send(msg || null, status);

const send200 = (res) => sendify(200)(res);
const send204 = (res) => sendify(204)(res);
const send404 = (res) => sendify(404)(res);

const maybeFound = (fn) => (res) => (d) => d ? fn(res)(d) : send404(res)();
const maybe200 = maybeFound(send200);
const maybe204 = maybeFound(send204);

module.exports = {
  send200,
  send204,
  send404,
  maybe200,
  maybe204,
};

Final thoughts

A todo application is not very complicated. In reality API's don't follow a simple BREAD pattern and the opportunities to abstract, as we did in the last step, are often too complex. I usually try things out to see how it feels and then make a call. I prefer to lean towards being a little less DRY and a little more pragmatic. Always think about your future self or the words that the next developer might use to describe your code.

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. :)