Building and deploying AWS Lambda with Serverless framework in just a few of minutes
Today I'll teach you how to create an AWS Lambda using a Serverless framework, as well as how to structure and manage your functions as projects in your repository. Serverless provides an interface for AWS settings that you may configure in your deployment configurations and function permissions for any service, including S3, SNS, SQS, Kinesis, DynamoDB, Secret Manager, and others.
AWS Lambda
Is a serverless computing solution offered by Amazon Web Services. It lets you run code without having to provision or manage servers. With Lambda, you can upload your code as functions, and AWS will install, scale, and manage the infrastructure required to perform those functions.
AWS Lambda supports a variety of programming languages, including:
- Node.js
- Python
- Java
- Go
- Ruby
- Rust
- .NET
- PowerShell
- Custom Runtime, such as Docker container
First things first
First, you should set up your Node.js environment; I recommend using nvm for this.
The serverless CLI must now be installed as a global npm package.
# (npm) install serverless as global package
npm install -g serverless
# (yarn)
yarn global add serverless
Generating the project structure
Following command will create a Node.js AWS lambda template.
serverless create --template aws-nodejs --path hello-world
Serverless Offline and Typescript support
Let's add some packages to the project.
npm install -D serverless-plugin-typescript typescript serverless-offline
# yarn
yarn add -D serverless-plugin-typescript typescript serverless-offline
# pnpm
pnpm install -D serverless-plugin-typescript typescript serverless-offline
Show the code
If you prefer, you can clone the repository.
- hello_world/selector.ts
This file includes the function that converts external data to API contracts.
import { CurrencyResponse } from './crawler'
export type Currency = {
name: string
code: string
bid: number
ask: number
}
export const selectCurrencies = (response: CurrencyResponse) =>
Object.values(response).map(
currency =>
({
name: currency.name,
code: currency.code,
bid: parseFloat(currency.bid),
ask: parseFloat(currency.ask),
} as Currency)
)
export default {
selectCurrencies,
}
- hello_world/crawler.ts
This file contains the main function, which retrieves data from a JSON API using currency values.
export type CurrencySourceData = {
code: string
codein: string
name: string
high: string
low: string
varBid: string
pctChange: string
bid: string
ask: string
timestamp: string
create_date: string
}
export type CurrencyResponse = Record<string, CurrencySourceData>
export const apiUrl = 'https://economia.awesomeapi.com.br'
export async function getCurrencies(currency) {
const response = await fetch(`${apiUrl}/last/${currency}`)
if (response.status != 200)
throw Error('Error while trying to get currencies from external API')
return (await response.json()) as CurrencyResponse
}
export default {
apiUrl,
getCurrencies,
}
- hello_world/handler.ts
Now we have a file containing a function that acts as an entrypoint for AWS Lambda.
import { getCurrencies } from './crawler'
import { selectCurrencies } from './selector'
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'
const DEFAULT_CURRENCY = 'USD-BRL,EUR-BRL,BTC-BRL' as const
export async function listCurrencies(
event: APIGatewayProxyEvent
): Promise {
try {
const currency = event.queryStringParameters?.currency || DEFAULT_CURRENCY
const currencies = selectCurrencies(await getCurrencies(currency))
return {
statusCode: 200,
body: JSON.stringify(currencies, null, 2),
}
} catch (e) {
console.error(e.toString())
return {
statusCode: 500,
body: '🫡 Something bad happened',
}
}
}
export default {
listCurrencies,
}
💡The highlight lines indicate that if we had more than one function on the same project, we could wrap promises to centralize error handling.
- hello_world/serverless.yml
This file explains how this set of code will run on AWS servers.
service: service-currencies
frameworkVersion: "3"
provider:
name: aws
runtime: nodejs18.x
functions:
api:
handler: handler.listCurrencies
events:
- httpApi:
path: /
method: get
plugins:
- serverless-plugin-typescript
- serverless-offline
- hello_world/tsconfig.json
The Typescript settings.
{
"compilerOptions": {
"preserveConstEnums": true,
"strictNullChecks": true,
"sourceMap": true,
"allowJs": true,
"target": "es5",
"outDir": "dist",
"moduleResolution": "node",
"lib": ["es2015"],
"rootDir": "./"
}
}
Execution
Let's test the serverless execution with following command:
SLS_DEBUG=* serverless offline
# or
SLS_DEBUG=* sls offline
You can look at the API response at http://localhost:3000.
We can run lambda locally without the Serverless offline plugin and get the result in the shell:
sls invoke local -f api
Tests
I use Jest to improve test coverage and illustrate how to use this wonderful approach, which is often discussed but not frequently utilized but should be 😏. I'm not here to claim full coverage, but some coverage is required.
- hello_world/__tests__ /handler.spec.ts
import {
APIGatewayProxyEvent,
APIGatewayProxyEventQueryStringParameters,
} from 'aws-lambda'
import { listCurrencies } from '../handler'
import fetchMock = require('fetch-mock')
import { getFixture } from './support/fixtures'
describe('given listen currencies http request', function () {
beforeEach(() => fetchMock.restore())
it('should raise error when Currency param is empty', async function () {
fetchMock.mock(/\/last\//, { status: 404, body: '' })
const event = { queryStringParameters: {} } as APIGatewayProxyEvent
const result = await listCurrencies(event)
expect(result).toEqual({
body: '🫡 Something bad happened',
statusCode: 500,
})
})
it('should return currency list', async function () {
fetchMock.mock(/\/last\//, {
status: 200,
body: getFixture('list_currencies_ok.json'),
})
const event = {
queryStringParameters: {
currency: 'USD-BRL,EUR-BRL,BTC-BRL',
} as APIGatewayProxyEventQueryStringParameters,
} as APIGatewayProxyEvent
const result = await listCurrencies(event)
expect(result.statusCode).toBe(200)
expect(JSON.parse(result.body)).toEqual([])
})
})
A lot of code will be required to run tests; take a look at the repository and then type:
npm test
Extra pipeline
Pipeline GitHub actions with tests, linter (eslint) and checker:
name: build
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: 'hello-world'
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v3
with:
version: 8
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: '18.x'
cache: 'pnpm'
cache-dependency-path: ./hello-world/pnpm-lock.yaml
- name: Install dependencies
run: pnpm install
- name: Run ci
run: npm run test && npm run lint && npm run check
Final Thoughts
In this post, we discussed how to setup our serverless function in the development context, execute and test it before moving it to the production environment, as it should be. So that covers up the first phase; I'll publish a second blog describing how to move our local function into production and deploy it in an AWS environment.
Thank you for your time, and please keep your kernel 🧠 updated to the most recent version. God brings us blessings 🕊️.
Time for feedback!