Serverless Love Story: NestJS & Lambda — Part 4: Sharing Code & Dependencies between Functions

Tobias Schmidt
AWS in Plain English
5 min readJun 3, 2021

--

Photo by Jackson David from Pexels

Last part of the series explained how to integrate with DynamoDB via its data mapper. Today’s agenda is sharing code between multiple functions as well as extracting our dependencies into a Layer.

This reduces our deployment times drastically and helps us to reuse code in independent processes easily.

Wrap up about the next chapters:

  • Project folder structure —concept on how to structure our files and different technologies/frameworks inside our project.
  • Lambda Layers & docker-based packaging — how to create a layer and built & package its containment so it’s always fitting for Lambda’s runtime.
  • Sharing code between functions an easy way of sharing code between multiple Lambdas, without creating any overhead.
  • Code-only deployments — deploying our new function code without updating our whole stack.
  • Enhanced code share with a mono repository — extend our sharing by including our frontends.
  • Final thoughts

Project folder structure

After a certain amount of trying out different approaches, I’m at a point where I’m comfortable with my setup because it’s a good mix of does what I want & not too much overhead/complexity.

Actually, it’s pretty simple.

root\
|-- do.sh
|-- app\
|-- buildspecs\
|-- infra\
| |-- accounts\
| |-- preview\
| |-- prod\
| |-- modules\
| |-- module1\
| |-- module2\
|-- lambda\
| |-- functions\
| |-- function1\
| |-- function2\
| |-- shared\
| |-- serverless.yml
| |-- package.json
  • I’m using a mono repository, which also includes the frontend applications; those will be kept at the app folder.
  • All infrastructure — which is not related to API Gateway & Lambda — is located at infra inside my Terraform templates and blueprints.
  • The lambda folder contains my NestJS (and other, single purpose) functions as well as the Serverless template and the package.json containing all dependencies for all functions.
  • buildspecs holds my configurations for CodeBuild.

The do.sh file on root includes all packaging & deploy scripts I regularly use. I can call it with different parameters to execute different scripts. That also brings the benefit that I’m only using this script within my CodeBuild projects.

Lambda Layers & docker-based packaging

We don’t want to deploy our dependencies with each code release. And we surely don’t want to package all of our dependencies into every function. They rarely change and this would make our packaging and our deployments slow and way to dependent to our current internet uplink speed (as I’m living in Germany, this is indeed a real pain ⚡️).

Let’s create a Lambda Layer for everything, which we then attach to all of our functions.

one layer can be attached to multiple functions & one function can have multiple layers (n:m relationship)

💡 Your function size is limited — the total unzipped size of the function and all layers can’t exceed the unzipped deployment package size limit of 250 MB.

It’s recommended that we’re installing our dependencies on an environment that’s as close as possible to the one AWS’ provides for Lambda. I’m using the docker image lambci/lambda.

Let’s create a small script for installing & creating our zip file, expecting that our package.json — which contains all needed dependencies — is at ./lambda.

rm -rf tmp/layer-common
rm -f dist/layer-common.zip
mkdir -p tmp/layer-common/nodejs
mkdir -p dist
cp lambda/package.json tmp/layer-common/nodejs
pushd tmp/layer-common/nodejs > /dev/null 2>&1
docker run -v "$PWD":/var/task lambci/lambda:build-nodejs12.x npm install --no-optional --production
popd > /dev/null 2>&1
pushd tmp/layer-common > /dev/null 2>&1
rm nodejs/package.json
zip -r ../../dist/layer-common.zip . > /dev/null 2>&1
popd > /dev/null 2>&1

We’re making use of our docker image to install our dependencies, so we won’t get into any trouble if one of your dependencies is using some platform dependent implementation.

Let’s add the layer to our Serverless template and use it in one of our functions.

layers:
common:
package:
artifact: ../../dist/layer-common.zip
name: common-layer
compatibleRuntimes:
- nodejs12.x
retain: false
functions:
function1:
package:
artifact: ../../dist/function1.zip
handler: functions/function1/src/lambda-proxy.handler
name: function1
runtime: nodejs12.x
layers:
- { Ref: CommonLambdaLayer }
memorySize: 1024

Simple method of sharing code between functions

You’ve probably seen the shared folder at ./lambda/shared. I’m using this one for all code which can be used by multiple functions — like a service to access DynamoDB or my domain model classes.

💡 The folder structure will be mirrored to your transpiled JS files —the folder structure of your functions which are using code from the shared folder, will be also visible in your distribution. So your handler function is not on root anymore, but for example on functions/function1/handler.ts. This is due to the fact that TypeScript also needs to include the files from our shared folder, which is above the function’s root.

I figured that this works out great and has less operations than having a private module. I have no need that my Lambdas are using some fixed version of my shared code which was published in the past, because updates to the shared code should be directly reflected in all of my functions that are using it.

Code-only deployments

Now we can finally harvest our fruits: we can just transpile our code via TypeScript, zip it and deploy it directly via sls deploy --stage=preview function -f function1.

If we deploy our complete stack, Serverless will notice if there are changes to our layer. If the hash of the zip stays the same, Serverless will not upload it again. If we’re changing our package.json in the future, we can re-package our layer and Serverless will update it accordingly in the next deployment.

Enhanced code share with a mono repository

Having your frontend also in the same repository gives you the advantage of sharing even more. Classes like the data transfer objects can be directly used by it. It’s also nice to have a shared backend-routes.ts file, which contains all routes of all request handlers. This reduces duplicated code and typos. Also, changing shared code will immediately put you in the place of updating all related code, whether it’s in your Lambda function or your frontend.

Sharing Code with Functions & the Frontend Application

Surely, this only works out if your project is solely JavaScript-based. In my case, the frontend is built with Angular — so there are only TypeScript files everywhere.

Final thoughts

AWS, related frameworks, and software development, in general, is changing and evolving rapidly. With this article, I wanted to share something that has worked out really great in the past, but I’m sure there are even better ways for more simplicity and even less hassle— especially regarding Serverless applications.

I’m interested in your story and how you’re building your State-of-the-Art Serverless application! Which tools, technologies, or project setups makes life the easiest?

There’s more…

I’m writing a book on AWS fundamentals to help others learn about the core building blocks. The focus is on learning for the real world, not only for certifications! 📚
You can subscribe to the newsletter at awsfundamentals.com to get bite-sized preview content & other lessons free to your inbox every two weeks.

More content at plainenglish.io

--

--

Software Engineer & Serverless Enthusiast, focusing on AWS & Azure as well as Kotlin & Node.js. Always learning & looking to meet people on the same journey!