My goal is to be able to write pure routes for an Express.js server. Is that even possible?
For accessing the database and stuff I know I can use the fabulous Future
monad to keep things pure, but how about the route rendering itself?
One of the greatest difficulties I am finding is that a route could end in quite a few different ways, like:
With the Future
monad, I can handle the error and the success case, but there isn't much more granularity to the success case after that.
Is there any way to write pure and totally testable routes for Express.js?
Short answer: No - It is not possible. Explanation: In the context of functional programming, we have a data flow - the program takes some input data, transforms that and returns output data.
It's fast, unopinionated, and has a large community behind it. It is easy to learn and also has a lot of modules and middleware available for use. Express is used by big names like Accenture, IBM, and Uber, which means it's also great in a production environment.
Applying a similar concept to the project structuring of Express, the separation of the application logic from the server allows the code to be modular and follow a MVC (Model-View-Controller) model. The separation is essential to reduce coupling and to encapsulate and abstract the inside logic of application.
Node programming is a more functional style of coding that improves code reliability and simplifies the debugging and testing process.
Short answer: No - It is not possible.
Explanation: In the context of functional programming, we have a data flow - the program takes some input data, transforms that and returns output data.
In case of a server, we have two data flows. First is when you are starting a server. In this flow, you might want to read config file or command line params from external world for stuff like port, host, database string, etc. This is a side effect and hence we would typically put it in Future. Example,
readJson(process.argv[2]) // Read configuration file
.chain(app) // Get app instance (routes, middlewares, etc.)
.chain(start) // Start server
.run().promise()
.then((server) => info(`Server running at: ${server.info.uri}`))
.catch(error);
This is typical index.js file containing all side effects (reading config). Now let us turn to second data flow.
This data flow is slightly hard to imagine. The output/side-effect of first data flow is server listening on some port for external connection. Now imagine, that every request coming to this server as one independent data-flow in itself.
Just like index.js was the file meant to handle all the side effects, your route-file or route-handler function is meant to handle the side effect i.e. result request should be replied back at this route handler. Typically, a close-to-pure functional route would look like:
function handler(request, reply) {
compose(serveFile(reply), fileToServe)(request)
.orElse((err) => err.code === 'ENOENT' ? reply404(reply) : reply500(reply))
.run(); // .run() is the side effect
}
return {
method: 'GET',
path: '/employer/{files*}',
handler
};
In above snippet, everything is pure. Only impure stuff is .run() method (I am using Hapi.js and Folktale.js Task).
Same is the idea with Front-end frameworks like Angular or React. Components in these frameworks should contain all the effects/impurity. These components just like your route handlers are end-points where side effects should happen. Your models/services should be free from impurity.
Having said this, if you still want to make your routes perfectly pure, then there is hope. What you essentially want to do is - the more higher level of abstraction:
In case of front-end frameworks, as I said earlier, that your UI components will have side-effects. But there are new frameworks that are going beyond this abstraction. Cycle.js is one framework I am aware of. It applies the higher level of abstraction than Angular or React. All the side-effects are caged in observable which are then dispatched to a framework and executed making all (I literally mean 100%) your components pure.
I have attempted to create server the way you are doing using Hapi.js+Folktale.js+Ramda. My idea originally traces its roots in Cycle.js. However, I achieved mild success. Some part code was really awkward and too restrictive to write. Afterward, I gave up on having pure routes. However, rest of my code is pure and is quite readable.
On a closing note, functional programming is about coding in parts and not coding in whole. What you or me are trying to do is functional programming all the way in whole. That will feel little awkward at least in JavaScript.
I also don't think you can do that with Express.js, but I can suggest an alternative to it.
Web servers can be described as a function that takes a Request
and provides a Response
. However, if you check what happens in Express.js, you'll actually see that the signature is actually:
(IncomingMessage, ServerResponse) -> ()
For example:
const express = require('express')
const handler = (req, res, next) =>
doSomethingAsync(req.body).then(res.json).catch(next)
express()
.get('/', handler)
.listen(3000, console.error)
While we'd prefere something like:
Request -> Promise Response
Paperplane is a lightweight NodeJS web server framework that works with the above type of signature. The idea is to use pure functions to transform the response you want to send. When it comes to routing, it has two functions that allow for a clean declaration:
routes :: { k: (Request -> Promise Response) } -> (Request -> Response)
and
methods :: { k: (Request -> Promise Response) } -> (Request -> Promise Response)
Check this simple example from the Paperplane's API docs :
const http = require('http')
const { mount, routes } = require('paperplane')
const { fetchUser, fetchUsers, updateUser } = require('./lib/users')
const app = routes({
'/users': methods({
GET: fetchUsers
}),
'/users/:id': methods({
GET: fetchUser,
PUT: updateUser
})
})
http.createServer(mount({ app })).listen(3000)
It also provides other functions to deal with the difficulties you've encoutered.
Plus, it supports Algebraic Data Types (ADTs), which you can get from the lovely Crocks lib.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With