Skip to content

Instantly share code, notes, and snippets.

@ArnaudRinquin
Last active February 5, 2021 15:56
Show Gist options
  • Save ArnaudRinquin/ce14ba85518e7809b0cd to your computer and use it in GitHub Desktop.
Save ArnaudRinquin/ce14ba85518e7809b0cd to your computer and use it in GitHub Desktop.
Clean Express controllers using currying and promises

Purpose

Demonstrate how to write clean express controllers using function currying, functionnal programming and promises.

  • Promises helps avoiding callback hell
  • Currying helps code reuse
  • Functional programing makes data flow clearer

Aknowledgment

Yes, this sample shows obvious over-engineering. The provided API could be provided by simplier, clearer code. However, this code will scale on business complexity:

  • handle more 'head' and 'tail' possible values: √
  • asynchronously retrieve player data from playerName: √
  • chain more actions: √
  • add more game parameters won't change game signature: √

Install, run, test

git clone https://gist.github.com/ce14ba85518e7809b0cd.git express_curry_promise
cd express_curry_promise
npm install
node app.js

... in another shell

curl -v http://localhost:3000/flip/head
curl -v http://localhost:3000/flip/tail
curl -v http://localhost:3000/flip/head
curl -v http://localhost:3000/flip/tail

var express = require('express');
var app = express();
var flipController = require('./flip_controller');
app.get('/flip/:bet', flipController.play);
app.get('/players/:playerName/flip/:bet', flipController.play);
app.listen(3000);
var Promise = require('bluebird'); // Awesome promise library
var _ = require('lodash');
module.exports = function(game){
return Promise.resolve(_.tap({
game: game,
got: Math.round(Math.random()) ? 'head' : 'tail'
}, function(result){
result.success = result.got === game.bet.toLowerCase();
result.message = [game.playerName, (result.success ? 'wins' : 'loses')].join(' ')
})
);
};
var _ = require('lodash'); // underscore++
var Promise = require('bluebird');
var flipCoin = require('./flip_coin'); // Your actual handler, usually async
var VALID_BET_VALUES = ['head', 'tail'];
var getBetFromRequest = function(req){
var bet = req.params.bet.toLowerCase();
if(! _.contains(VALID_BET_VALUES, bet) ) {
throw new Error("missing or invalid parameter `bet`, expects 'head' or 'tail' string but got: " + bet);
}
return bet;
};
var getPlayerName = function(req){
return req.params.playerName || 'anonymous';
};
var getGameOpts = function(req){
return {
bet: getBetFromRequest(req),
playerName: getPlayerName(req)
};
}
var respond = _.curry(function(res, status, payload){
res.status(status).send(payload).end();
});
var controller = {
play: function(req, res){
Promise.try(getGameOpts, req) // expect exceptions
.then(flipCoin)
.then(respond(res, 200)) // on success, send a 200 and forward flipCoin result
.catch(_.compose(
respond(res, 400),
_.partialRight(_.pick, 'message') // pick only error message
));
}
}
module.exports = controller;
{
"name": "curry_promise_express",
"version": "1.0.0",
"description": "Example of clean Express controllers using currying and promises",
"main": "app.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "https://gist.github.com/8472f35350a7031e2cb2.git"
},
"keywords": [
"curry",
"express",
"promise"
],
"author": "Arnaud Rinquin",
"license": "MIT",
"dependencies": {
"bluebird": "^2.3.6",
"express": "^4.9.8",
"lodash": "^2.4.1"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment