Create Your First Lambda with Serverless, Typescript and Microgamma Part 2
Davide Cavaliere
Posted on January 26, 2020
This is part 2 of my serie on Microgamma.
TLTR;
In part 1 we saw how to set up a project with typescript, microgamma and serverless and how to deploy a very simple endpoint
In this post we will:
1 - create our first REST api
2 - make it private adding an api-key
3 - secure our lambdas with an authorizer function
If you followed the steps in the first part then make sure to update:
yarn add @microgamma/apigator@latest @microgamma/serverless-apigator@latest
If you wan to start straight from here you can checkout:
git clone git@github.com:microgamma/serverless-apigator-blue-print.git
Or, if you already clone it before:
git remote update && git pull origin master
and checkout
git checkout -b part2 my-first-apigator-part-2
Before we start, let's build, deploy and check that everything is still working:
yarn build && yarn sls deploy --stage dev
yarn sls invoke local -f index
And:
yarn sls invoke -f index --stage dev
A Tickets service
Let's now make our service a bit more useful. Let's suppose that our endpoint will handle the requests for interacting with Tickets.
So we have:
-
GET /
retrive all tickets. -
GET /{id}
retrieve a ticket by id. -
POST /
creates a new ticket (ticket is passed through request's body). -
PUT /
updates an existing ticket (ticket is passed through request's body). -
DELETE /{id}
deletes a ticket.
This is how our service will look like:
// my-first-service.ts
import { Body, Endpoint, Lambda, Path } from '@microgamma/apigator';
import { Injectable } from '@microgamma/digator';
@Endpoint({
cors: true,
name: 'my-first-service',
private: false
})
@Injectable()
export class MyFirstService {
private static tickets = [{
id: 1,
description: 'Buy beers'
}, {
id: 2,
description: 'send email'
}];
private lastIndex = 2;
@Lambda({
method: 'GET',
path: '/',
description: 'get all tickets'
})
public index() {
return MyFirstService.tickets;
}
@Lambda({
method: 'GET',
path: '/{id}',
description: 'find a ticket by id'
})
public findById(@Path('id') ticketId) {
const found = MyFirstService.tickets.find((ticket) => ticket.id === Number(ticketId));
if (!found) {
throw new Error('[404] not found');
}
return found;
}
@Lambda({
method: 'POST',
path: '/',
description: 'create a new ticket'
})
public create(@Body() ticket) {
ticket.id = ++this.lastIndex;
MyFirstService.tickets.push(ticket);
return ticket;
}
@Lambda({
method: 'PUT',
path: '/',
description: 'updates a ticket'
})
public update(@Body() ticket) {
const found = MyFirstService.tickets.find((t) => t.id === Number(ticket.id));
if (!found) {
throw new Error('[404] not found');
}
Object.assign(found, ticket);
return found;
}
@Lambda({
method: 'DELETE',
path: '/{id}',
description: 'delete a ticket by id'
})
public delete(@Path('id') ticketId) {
const found = MyFirstService.tickets.find((t) => t.id === Number(ticketId));
if (!found) {
throw new Error('[404] not found');
}
const indexOf = MyFirstService.tickets.indexOf(found);
MyFirstService.tickets.splice(indexOf, 1);
return { delete: 'ok' };
}
}
In the above snippet we can notice two new decorators: @Path('id')
which retrives id
from the path parameters and @Body()
that retrives the request's body.
Let's build again:
yarn build
And call those new lambda locally
yarn sls invoke local -f index
[
{
"id": 1,
"description": "Buy beers"
},
{
"id": 2,
"description": "send email"
}
]
yarn sls invoke local -f findById -d '{ "path": { "id": "1" } }'
{
"id": 1,
"description": "Buy beers"
}
yarn sls invoke local -f create -d '{ "body": { "id": 3, "description": "my new ticket" } }'
{
"id": 3,
"description": "my new ticket"
}
yarn sls invoke local -f update -d '{ "body": { "id": 1, "description": "buy beers and candies" } }'
{
"id": 1,
"description": "buy beers and candies"
}
yarn sls invoke local -f delete -d '{ "path": { "id": "1" } }'
{
"delete": "ok"
}
Every call should work just fine, so let's try to deploy.
yarn sls deploy --stage dev
We can call again the same commands as above without local
and adding --stage dev
Or we could use postman for a quick e2e test.
Protect your Lambdas with an Api Key
In order to achieve this you just need to change the @Endpoint
decorator as follows
@Endpoint({
cors: true,
name: 'my-first-service',
private: true
})
@Injectable()
export class MyFirstService {
And add the following to your serverless.yml
--- a/serverless.yml
+++ b/serverless.yml
@@ -2,6 +2,8 @@ service: my-first-apigator # NOTE: update this with your service name
provider:
name: aws
+ apiKeys:
+ - myFirstKey
runtime: nodejs12.x
stage: dev
region: eu-west-2
Build and deploy. We should see something like the following in our terminal.
api keys:
myFirstKey: XNzWxRlNEb3mJynlLi8mDamcdL6psXmx4MWHgBCe
So now if we try to hit our endpoint we'll get a 403 Forbidden
error.
Adding the above api key to our headers will let us reach our data.
Bear in mind that the header name should be:
x-api-key
Add an authorizer function
Having a lambda that is called by another lambda to check whether the request comes from authorized client it a common pattern. It's most simple implementation it will check the validity of a jwt token passed through the headers of the request. So that if for example the used is authenticated it would have provided a valid jwt token.
For now we're gonna implement something way simpler but still a valid point in order to show how to do it in Microgamma.
First of all we add an authorize
method to our service.
@Authorizer()
public authorize(@Header('Authorization') jwt) {
return jwt === 'jolly';
}
Decorated with @Authorizer
which tells serverless that this is an authorizing function.
We also use here @Header('Authorization')
which, alike @Path
, extracts from the headers the field Authorization
.
Then we need to set this authorizer into the lambdas we want to protect. I.e. the create
lambda
@Lambda({
method: 'POST',
path: '/',
description: 'create a new ticket',
authorizer: 'authorize'
})
public create(@Body() ticket) {
ticket.id = ++this.lastIndex;
MyFirstService.tickets.push(ticket);
return ticket;
}
After build and deploy. If try to hit our endpoint with 'POST' we'll get the following.
{
"message": "Unauthorized"
}
So now we need to provide the Authorization
header to our request.
That's all folks. Hope you enjoyed and thanks for reading. Any feedback is very welcome and stay tuned for more @microgamma sugar.
Posted on January 26, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.