📝 TL;DR
In this article, we modernized our serverless API skeleton by updating dependencies and switching from the Serverless Framework
to the Open Source Serverless Framework
fork. We also upgraded to Node.js 22.x
on AWS Lambda
, enhancing performance and future-proofing our stack. These steps streamline development, improve scalability, and ensure our application remains robust and maintainable.
⏪ Where we left off
Hey devs! Before diving in, let's do a quick recap.
In the first article of this series, we explored how serverless architectures and DevOps best practices can supercharge our Developer Experience (DX)—letting us focus on code while automation handles the rest.
We covered:
- Infrastructure as Code (IaC) for streamlined API provisioning and better cloud management.
-
Serverless Framework
and its killer plugin ecosystem (shoutout toServerless Offline
for easy local dev!). - OpenAPI for documentation, keeping everyone on the same page.
- Automated testing, monitoring, and security—critical for scalable, production-ready apps.
With that in mind, let’s jump into what’s next! 🚀
📦 Update dependecies
Alright, let's get our stack up to date!
First things first: we need to make sure we’re running the latest versions of all our dependencies. Luckily, our stack doesn’t have too many, so this should be a breeze.
🌐 Open Source Serverless Framework
But before we move on, I want to take a moment to talk about switching from Serverless Framework to Open Source serverless.
Serverless Framework
has been an amazing tool, helping thousands of developers get their projects into the cloud quickly and efficiently. Up until version 3.38
, it was fully open source. However, with version 4.x
, the maintainers made the perfectly legitimate decision to move to a closed-source model with a dedicated pricing structure.
My advice? If you're happy with the pricing (there’s a generous free tier), you should definitely keep using it. But for our stack, we want to stick with open-source tools only.
Thankfully, the awesome folks at Bref (a PHP serverless framework built on top of Serverless Framework) decided to fork version 3.x
and keep it maintained under an open-source license. You can find it here: Open Source serverless.
So, let's go ahead and install it globally to continue using serverless as recommended! 🚀
npm remove -g serverless
npm install -g osls
serverless --version
Additionally, we can integrate it into our deployment pipelines.
Also we are switching to 22
the Node version on our build machine.
version: 0.2
phases:
install:
runtime-versions:
nodejs: 22
commands:
...
# Install serverless globally
- npm install -g osls
...
build:
commands:
...
artifacts:
files:
- '**/*'
cache:
paths:
- node_modules/**/*
📦 Npm dependencies
Let's go ahead and update our development dependencies in the package.json
file.
"devDependencies": {
"@redocly/cli": "^1.0.0-beta.125",
"axios": "1.7.9",
"dotenv": "^16.4.7",
"ejs": "^3.1.10",
"jest": "^29.7.0",
"jest-openapi": "^0.14.1",
"openapi-validator": "^0.14.1",
"osls": "^3.46.0",
"serverless-api-gateway-caching": "^1.10.4",
"serverless-jetpack": "^0.11.2",
"serverless-offline": "^13.9.0",
"serverless-openapi-documenter": "^0.0.109",
"serverless-prune-plugin": "^2.1.0",
"serverless-slic-watch-plugin": "^3.2.2"
}
The goal is to achieve a fully stable setup with no warnings of any kind (vulnerabilities, deprecations, etc.)
up to date, audited 1957 packages in 6s
149 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
🚀 Update Node.js version to support latest Node.js Lambda Supported Runtime
We were using Node.js 18.x
, but since then, two new versions have been released and are now available on AWS Lambda: 20.x
and 22.x
.
So, how do we update our stack?
Easy peasy! We just need to update the runtime in our config/provider.yml
file. 🚀
runtime: nodejs22.x
And that's all we are required to do!
🔄 Change from CommonJS to ES Modules
But let's go further and modernize also our code.
One of the key upgrades we're making is switching from CommonJS to ES Modules.
Why? Because ES Modules (ESM) are the modern JavaScript standard, offering better performance, improved tree-shaking, and native compatibility with modern runtimes—including AWS Lambda with Node.js 20+.
Start adding this line in the package.json
file:
...
"type": "module",
...
Here’s what the change looks like in practice in our function code:
Before (CommonJS)
'use strict';
const utils = require('my-api-utils');
module.exports.handler = async (event) => {
return utils.prepareResponse(
{
message: 'Go Serverless v3.0! Your function executed successfully!',
input: event,
}
,200
);
};
After (ES Modules)
'use strict';
import { utils } from "my-api-utils";
export const handler = async (event) => {
return utils.prepareResponse(
{
message: 'Go Serverless v3.0! Your function executed successfully!',
input: event,
}
,200
);
};
We basically switch just one line of code for the imports!
And this is how to update our tests:
Before (CommonJS)
'use strict';
const path = require('path');
// tests for hello
// Generated by serverless-jest-plugin
const mod = require('./../../src/function/hello/index');
const jestPlugin = require('serverless-jest-plugin');
const lambdaWrapper = jestPlugin.lambdaWrapper;
const wrapped = lambdaWrapper.wrap(mod, { handler: 'handler' });
// Import jestOpenApi plugin
const jestOpenAPI = require('jest-openapi').default;
// Load an OpenAPI file (YAML or JSON) into this plugin
let relativePath = (jasmine.testPath).split("/__tests__/")[0];
let absolutePath = path.resolve(relativePath);
jestOpenAPI(absolutePath+'/doc/build/openapi.json');
describe('hello', () => {
beforeAll((done) => {
//lambdaWrapper.init(liveFunction); // Run the deployed lambda
done();
});
it('Test hello function', () => {
return wrapped.run({}).then((response) => {
//Expect response to be defined
expect(response).toBeDefined();
//Validate status
expect(response.statusCode).toEqual(200);
//Validate response against HelloResponse schema
expect(JSON.parse(response.body)).toSatisfySchemaInApiSpec("HelloResponse");
});
});
});
After (ES Modules)
'use strict';
import * as path from 'path';
import { fileURLToPath } from 'url';
import {handler} from './../../src/function/hello/index';
// Import jestOpenApi plugin
import jestOpenAPI from 'jest-openapi';
// Load an OpenAPI file (YAML or JSON) into this plugin
const __dirname = path.dirname(fileURLToPath(import.meta.url));
let relativePath = (__dirname).split("/__tests__/")[0];
let absolutePath = path.resolve(relativePath);
jestOpenAPI.default(absolutePath+'/doc/build/openapi.json');
describe('hello', () => {
beforeAll((done) => {
//lambdaWrapper.init(liveFunction); // Run the deployed lambda
done();
});
it('Test hello function', () => {
return handler({}).then((response) => {
//Expect response to be defined
expect(response).toBeDefined();
//Validate status
expect(response.statusCode).toEqual(200);
//Validate response against HelloResponse schema
expect(JSON.parse(response.body)).toSatisfySchemaInApiSpec("HelloResponse");
});
});
});
Why make the switch?
- Future-proofing – ES Modules are the standard moving forward, while CommonJS is slowly being phased out.
- Better performance – ESM allows for more efficient bundling and optimizations, leading to faster cold starts on Lambda.
- Top-level await – With ESM, we get access to await at the module level, making async workflows even cleaner.
- Interoperability – Many modern libraries are now ESM-first, reducing compatibility issues.
Last point is very important in Node ecosystem: making this change ensures our stack stays lean, modern, and ready for whatever AWS throws at us next. 🚀
☁️ AWS SDK – Out with v2, in with v3
If you’ve been using AWS Lambda for a while, you might have noticed a big change starting from Node.js 18.x
: AWS SDK v2 is no longer included in the runtime.
That means if your functions rely on the old SDK, you’ll need to explicitly install it, or—better yet—upgrade to AWS SDK v3.
Why AWS SDK v3?
AWS strongly recommends using AWS SDK v3, and for good reason.
According to the official documentation, here’s why you should make the switch:
- Modular imports – Instead of loading the entire SDK, you can import only the services you need. This reduces bundle size and speeds up execution.
- Better performance – Thanks to smaller package sizes and optimized HTTP handling, cold starts are faster.
- Middleware architecture – Customizable request handling with middleware support gives you more control over API requests.
- Top-level async/await – Native support for modern JavaScript async patterns makes the code cleaner and easier to maintain.
How to upgrade
If you’re still using v2, your imports probably look something like this:
const AWS = require('aws-sdk');
const s3 = new AWS.S3();
With v3, you switch to modular imports:
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
const s3 = new S3Client();
const uploadFile = async () => {
await s3.send(new PutObjectCommand({ Bucket: "my-bucket", Key: "file.txt", Body: "Hello World!" }));
};
Example using AWS Secret Manager
For example, using modular imports comes in handy when developing features like interacting with AWS Secrets Manager to handle sensitive data, such as database connection credentials. 🔐
import {GetSecretValueCommand,SecretsManagerClient} from "@aws-sdk/client-secrets-manager";
const getSecretValue = async (secretName = "SECRET_NAME") => {
const client = new SecretsManagerClient({});
try {
const response = await client.send(
new GetSecretValueCommand({
SecretId: secretName,
}),
);
if (response.SecretString) {
return response.SecretString;
}
if (response.SecretBinary) {
return response.SecretBinary;
}
} catch (err){
if (err.code === 'DecryptionFailureException')
// Secrets Manager can't decrypt the protected secret text using the provided KMS key.
// Deal with the exception here, and/or rethrow at your discretion.
console.log(err);
else if (err.code === 'InternalServiceErrorException')
// An error occurred on the server side.
// Deal with the exception here, and/or rethrow at your discretion.
console.log(err);
else if (err.code === 'InvalidParameterException')
// You provided an invalid value for a parameter.
// Deal with the exception here, and/or rethrow at your discretion.
console.log(err);
else if (err.code === 'InvalidRequestException')
// You provided a parameter value that is not valid for the current state of the resource.
// Deal with the exception here, and/or rethrow at your discretion.
console.log(err);
else if (err.code === 'ResourceNotFoundException')
// We can't find the resource that you asked for.
// Deal with the exception here, and/or rethrow at your discretion.
console.log(err);
}
};
🎉 Wrapping It Up – Smooth Upgrades Thanks to Solid Foundations
Looking back at all the changes we made—upgrading dependencies, switching to ES Modules, moving to AWS SDK v3, and adjusting our Node.js runtime—one thing is clear: it was all pretty straightforward.
Why? Because from the beginning, we built our stack following best practices that are language-agnostic and future-proof.
- Infrastructure as Code (IaC) meant we could tweak configurations instead of manually fixing things.
- Modular and decoupled components allowed us to swap dependencies without breaking everything.
- Open-source tools ensured we stayed in control of our stack, avoiding vendor lock-in.
🏁 Final Thoughts
This is the real power of a well-architected approach: it keeps things flexible, scalable, and easy to maintain, no matter how the ecosystem evolves.
Time to deploy and enjoy a cleaner, more modern stack! 🚀🔥
🌐 Resources
You can find a skeleton of this architecture with Python support open sourced by Eleva here.
It has a develop
branch which you can use with all these new developments.
⏭️ Next steps
So, what’s next? More iterations, more optimizations, and probably some new breaking changes in future Node.js versions (because why not? 😅). But thanks to this approach, we know that adapting will always be painless.
🙋 Who am I
I'm D. De Sio and I work as a Head of Software Engineering in Eleva.
I'm currently (Feb 2025) an AWS Certified Solution Architect Professional and AWS Certified DevOps Engineer Professional, but also a User Group Leader (in Pavia) and, last but not least, a #serverless enthusiast.
My work in this field is to advocate about serverless and help as more dev teams to adopt it, as well as customers break their monolith into API and micro-services using it.
Top comments (0)