DEV Community

Cover image for Spicing Up Your Serverless App Using Curried Functions
Charlie Thomas
Charlie Thomas

Posted on • Originally published at charliethomas.codes

Spicing Up Your Serverless App Using Curried Functions

I really, really love red curry. You can ask my wife, or inspect my DoorDash order history... it's truly a bit excessive. I also love the functional programming technique called "Currying" too, which isn't quite as expensive 😬 . In this post, we'll specifically be exploring how we can leverage this technique to simplify the way we build Node.js applications with Serverless Framework.

So what exactly is currying?

To get started, let's unpack this technique a little.

Currying is a transformation of functions that translates a function from callable as f(a, b, c) into callable as f(a)(b)(c).

Quote taken from javascript.info

Essentially, it's the practice of "unwinding" the arguments of a function by splitting each argument into a composable higher order function (or a function that returns a function). Let's take a look at a contrived example:

const boringOldAdd = (x, y) => {
  console.log('🥱');
  return x + y;
};

const spicyAdd = (x) => (y) => {
  console.log('🌶️');
  return x + y;
};
Enter fullscreen mode Exit fullscreen mode

Here we have two functions boringOldAdd and spicyAdd that look very similar at first glance, but there is a stark difference in how they are invoked:

const boringResult = boringOldAdd(1, 2);

const spicyResult = spicyAdd(1)(2);
Enter fullscreen mode Exit fullscreen mode

Both functions return the exact same result, but the invocation signature is quite different. Now that the syntax is defined and we've implemented it, it may not be entirely clear how this is actually useful and not just some silly syntax. The key is composability.

It's all about composability!

The driving reason for using this technique in practice is for composition. Building quality software is all about a balance of clean, reusable (or composable) capabilities that can be combined to bootstrap your business processes. In essence, you want to take your application and break it down into small reusable functions that can be used to make more complex functions. For those that may be familiar with Object Oriented Programming, you could draw a correlation between composability and inheritence in that they both strive to abstract capabilities in a way that could be reused in different contexts.

Let's break all this down using our contrived example from earlier. What if we wanted to add 1 to every value in a list using our functions?

const list = [1,2,3,4,5];

// 🥱
const boringList = list.map(n => boringAdd(n, 1));

// 🌶️
const add1 = spicyAdd(1);
const spicyList = list.map(add1);
Enter fullscreen mode Exit fullscreen mode

Here we start to lean into the upside our curried function offers over the normal version. The curried function results in a way to compose together capabilities in a more modular way. Again, this is a very contrived example and you would never use curried functions for something so simple, but with everything defined... let's dig into how to use this somewhere a bit more impactful.

Basic usage with Amazon SQS and Lambda!

When writing functions for your serverless application, there are common tasks that you have to do depending on what vendor you have selected for hosting. For AWS, some of these include:

  • Serializing SQS bodies and their json messages.
  • Decoding Kinesis messages from base64 into utf-8.
  • Extracting path parameters, http headers, or http bodies.

A very basic usage of curried functions could be to extract these vendor specific contracts into a curried function that then passes only the data you need to a business logic function (or your domain). Here is a quick example of doing this for Amazon SQS messages.

const SQSHandler = (businessLogicFn) => async (event) => {
  for (const record of event.Records) {
    const body = JSON.parse(record.body)
    const message = JSON.parse(body.Message)
    await businessLogicFn(message)
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, any time we need write a new SQS handler... we don't have to think about the SQS event contract! We just need to worry about the payload containing data relevant to our system. Generating a function that Serverless Framework could use now looks something like this:

import myBusinessLogic from './my-logic.js';

export const myHandler = SQSHandler(myBusinessLogic); // 🌶️
Enter fullscreen mode Exit fullscreen mode

And the correlating serverless.yml entry...

functions:
  myHandler:
    handler: handlers.myHandler
    events:
      - sqs:
          arn: # SQS Queue
Enter fullscreen mode Exit fullscreen mode

And voila! You now have a composable solution to abstracting the AWS SQS event contract from your business logic that can be used for all of your future handlers. To improve upon this you could:

  • Add default error handling!
  • Extract SQS Message Attributes or fields outside of the message body!
  • Add some debugging utilities like message logging!

But let's not stop here! We can take this a step further and create a framework for middleware around HTTP API handlers with a more advanced application of this technique.

Advanced usage with HTTP API and Lambda!

Expanding on what we did in the SQS Handler example, let's create some reusable functions that can abstract all of our default behaviors away as composable "middleware" functions that adapt the incoming requests and decorate the response from our core business logic.

export const buildMiddleware = (...middleware) => 
  input => middleware.reduce(
    (next, current) => current(next)
  , input)

export const buildHttpHandler = 
  (requestMiddleware, responseMiddleware) => 
    (handler) => async (event) => {
      return Promise
        .resolve(event)
        .then(requestMiddleware)
        .then(handler)
        .then(responseMiddleware)
    }
Enter fullscreen mode Exit fullscreen mode

In the snippet above, we export two different functions. The first of the two, buildMiddleware, takes a list of middleware functions and returns a "reducer" responsible for resolving all potential middleware functions into a final result. This will be used to build both our request middleware layer and response middleware layer. The second export, buildHttpHandler, takes two middleware arguments and return an http handler builder (just like our SQS example above).

import myBusinessLogic from './my-logic.js';
import {
  buildMiddleware,
  buildHttpHandler
} from './http-utils.js';

const requestLogger = (req) => {
  console.log(req);
  return req;
}

const responseLogger = (res) => {
  console.log(res);
  return res;
}

const requestMiddleware = buildMiddleware(requestLogger);
const responseMiddleware = buildMiddleware(responseLogger);

const HttpHandler = buildHttpHandler(
  requestMiddleware,
  responseMiddleware
);

export const myHandler = HttpHandler(myBusinessLogic);
Enter fullscreen mode Exit fullscreen mode

When we combine all of these principals together, we get a composable solution to building our http handlers with utilities that can abstract common system level tasks and enable us to focus more on the business logic. In the example above, we simply use it to add request and response logging to our functions, but you could expand on this to do things like response header decoration or generic request validation.

In Summary

  • The functional programming technique known as function currying is a very powerful tool for any type of javascript developer to have in their tool belt (backend or frontend).
  • Using curried functions can, specifically, be used to abstract common code used to build Serverless Applications!
  • Curry is just plain delicious.

References

Top comments (0)