Rip It Up

Starting Over With Serverless Functions

Carly Richmond
7 min readJul 14, 2023

This week I've had the fun and frustration of building my first app in a while. Life as a developer advocate has periods of busy social energy around conferences as well as quiet focus to work on content. Prior roles I've had before had the majority of one or the other, referring specifically to the manic calendar of being a team lead and engineering manager compared to the quiet focus required for developers.

So why fun? This week I’ve had time to build an app and revisit my youth by rebuilding my master’s thesis game Fu-Finder. Getting in the zone and tinkering with a coding project scratches the engineer itch I often get when I’ve not written any code in a while. So the euphoria of getting something working has been great. Kind of like the joy of winning with my favourite character in the Street Fighter games as a kid.

Wooo it works!

But it’s also been frustrating! I’ve had to make a few big design changes along the way. I ripped out a library I was using for building the search experience after finding the documentation difficult to navigate and that I spent more time getting their components to do what I wanted than just writing my own. I ripped out my server implementation as well when I started to think about hosting. My background in banking means thinking logic resides in a continually running server, which doesn’t really work when I have to consider utilization and cost. I also ripped out my monorepo manager when realising my serverless functions don’t need to reside in a dedicated server app, and that running them locally was just not happening. Error after error left me feeling rather Blanka-esque (cheers for the inspiration Keith!)

Ripping stuff out has been very frustrating

Here I walk through using serverless functions, specifically using Netlify, alongside Elasticsearch JavaScript Client and React, to show how I’ve simplified a complex monorepo implementation into a far simpler project.

What the serverl*ss?

Serverless has been a long-standing buzzword that I’ve always found slightly misleading. Traditionally, web developers deploy a running process on server hardware somewhere that will remain alive and respond to calls made from another component, most notably a frontend component as in this case.

Serverless functions do not mean that the box is gone, but that it’s abstracted away by a cloud provider. This can make it easier for new developers to get started, but for more experienced engineers like myself, it requires unlearning the server habit.

A serverless function is an ephemeral process that spins up temporarily to perform our logic before standing down. It’s also stateless, meaning that the data doesn’t persist and we as developers need to ensure our data is stored elsewhere such as a database, Elasticsearch cluster, Kafka broker or queue.

Prerequisites

All of the code discussed in this post is available here on GitHub. Additionally, you will need to have the following elements installed:

  1. NPM package manager for installation and package management.
  2. Netlify CLI for local running of serverless functions.

It’s worth noting there are loads of different tutorials online for writing Netlify functions. I took inspiration from two articles from CSS Tricks and Netlify.

Configuration

Serverless providers, including Netlify which I am using here, require configuration to pick up your functions. Here we specify a netlify.toml file to explain how the site is built, and deployed (if you're not making use of the GitHub connection and configuration via the UI which I did).

There are many options covered in the documentation that you can specify. I found I needed to specify build options to have my React app running locally alongside the functions.

[build]
command = "npm run build" # React app build command
functions = "src/functions/" # functions location
publish = "build" # static content path

A common option that I didn’t use is redirects, which allows you to specify redirection logic. This is particularly useful in the Netlify case where the path by default resolves to .netlify/functions/<my-url-path>. When building an enterprise application, or one for something other than a pet project, you probably don't want to expose the provider. One cool thing you can do is stitch old and new together using the :splat operator:

[[redirects]] 
from = ".netlify/functions/*"
to = "/api/:splat"
status = 200
force = true

The above configuration would mean automatic redirection from a default path such as .netlify/functions/document to api/document. It's not something I explicitly needed for a prototype, but I've included it here as I expect to use it for future projects.

Function Writing

Initially, I took the approach of wrapping my Express server in a single function using the serverless-http module, meaning that my paths would be one level below. So for example, my /document endpoint to get an individual document from Elasticsearch by ID sat alongside was present in the functions/api.ts file, resulting in the function api/document. Conceptually I found this difficult to rationalise in my head as I was making changes.

API endpoint design best practices recommend making URLs easier to read and making use of nouns where you can. Adhering to this means avoiding deep paths and using separate handlers. Below is the function used in my game through POST .netlify/functions/document.

// Note: Netlify deploys this function at the endpoint 
// /.netlify/functions/document
export async function handler(event, context) {
const request = convertRequest(event.body);

if (!request.documentID) {
return generateResponse(500, 'Document ID not specified');
}

try {
const results = await getDocumentByID(request.documentID);

return generateResponse(200, results);
}
catch(e) {
console.log(e);

return generateResponse(500, e);
}

Awaiting Workloads

The stateless nature of these functions means that data must be persisted somewhere and accessed from a store. Here I have made use of a utility using the Elasticsearch JavaScript Client to perform the required searches. The example below makes use of an to return an individual document.

import { Client } from "@elastic/elasticsearch";

const client = new Client({
cloud: { id: cloudID },
auth: { apiKey: apiKey },
});

export function getDocumentByID(documentID: string): Promise<any> {
return client.search({
index: index,
query: {
ids: {
values: documentID,
},
},
});
}

Unlike traditional server implementations where to paraphrase Beyoncé, you shoulda put a spinner on it to communicate a long-running delay we consider necessary, we need to be mindful of the return time from data services. Here I’m making use of the free functions from Netlify, which have memory and usage limits depending on your plan. Limit responses to essential fields and documents to ensure response within limits, as well as to avoid the dreaded Elasticsearch circuit-breaker errors.

Request and Response Handling

The generateResponse function is a helper used to generate the response in the expected format. It's important to use HTTP error codes to denote success and failure and allow for error detection and handling on the UI, which is why success is denoted by 200: OK compared to error 500: Internal Server Error. The generateResponse method ensures the response content is stringified for return to the UI, along with the status code in the expected int format:

export function generateResponse(statusCode: number, data: any) {
return {
statusCode: statusCode,
body: JSON.stringify(data)
};
};

It’s worth noting that the response also needs to be parsed into valid JSON using the JSON.parse JavaScript method, which I've extracted to another utility method convertRequest:

export function convertRequest(data: any) {
return JSON.parse(data);
}

Running Locally and Deploying

One of the reasons I’ve avoided serverless functions for so long was the ambiguity of how to run and validate locally. As an advocate running demos locally is a useful backup for unreliable internet, and as a frontend engineer in banking running the app locally was imperative for debugging and E2E test development. Pushing changes and finding issues when deploying to Netlify is not ideal.

Let’s recall our netlify.toml, and the command configuration:

[build]
command = "npm run build" # React app build command
functions = "src/functions/" # functions location
publish = "build" # static content path

Kicking off the Netlify CLI using netlify dev will spin up not just my React frontend as I have the command configured, but also each of my serverless functions. Huzzah!

Deployment is pretty easy given the GitHub integration that Netlify has, especially when our netlify.toml already has the build command and publish options configured. I'd recommend following the guide in their blog.

Conclusions

Despite ripping out my Nx monorepo and replacing the Express app with functions being a frustrating and confusing ride this week, I’m glad I did it. One of the best things about my job as an advocate is that I am already learning new things and building my skills. My experience in banking tech was that building financial software doesn’t always grant you the luxury of playing with shiny new tech toys.

All my fellow software engineers will attest to the satisfaction of getting something working and deployed. But there’s one other fantastic yet underrated feeling: when you get to delete redundant code. With 31,672 additions and 25,091 deletions, I’m satisfied that I have a better-structured project that will be far easier for others to understand.

Happy serverless coding!

Originally published at http://carlyrichmond.com on July 14, 2023.

--

--

Carly Richmond

Developer Advocate with a strong interest in UI, UX, Usability and Agility. Lover of cooking, tea, photography and gin! All views expressed are my own.