Welcome to this mini workshop! Here you will learn to build and deploy Reading List API using RESTful API, AWS Lambda, Amazon API Gateway, Amazon Cognito, and Amazon DynamoDB. Please take a look the architecture below.
The content of this mini workshop may be updated and if you have questions or find issues in this mini workshop, please file them as an Issue.
reading-list-api/
ββ functions/
ββ .gitignore
ββ architecture.png
ββ LICENSE
ββ README.md
functions/
contains lambda function code for the API.architecture.png
is an overview of the resources to be deployed.README.md
contains guide for this mini workshop.
Before starting this mini workshop, the following runtime/tools must be met and configured properly.
- Active AWS Account.
- NodeJS
v16
or latest with npm or yarn installed. - AWS CLI version 2.
- API Testing Tools.
Note
This mini workshop will usecurl
to test the APIs. However, you can use your favorite API testing tools (e.g. Postman/Insomnia/Thunder Client/etc). - (optional) OS based on Linux.
Note
Build script for package lambda function code and it's dependecies require Linux/Unix shell to operate. If you are using an OS other than Linux and/or your device doesn't support Linux shell commands you can customize this build script to make sure it runs properly.
Some of the services from AWS that are used in this mini workshop are as follows:
π‘ TIP
If the settings/configurations are not specified in this guide, you can leave them as default or you can specify the values with your own.
-
Select AWS region.
-
Create dynamoDB tables with following configuration:
- Table name:
reading-list-db
- Partition key:
readingID
(string) - Sort key:
userID
(string) - Table class: DynamoDB Standard
- Capacity mode: On-demand
- Table name:
-
Select AWS region.
-
Create Cognito User pool with following configurations:
i. Sign-In Experience
- Provider type: Cognito user pool
- Cognito user pool sign-in options:
username
,email
- Multi-factor authentication: No MFA
ii. Sign-Up Experience
- Enable Self-registration: true
- Allow Cognito to automatically send messages to verify and confirm: true
iii. Messaging / Message Delivery
- Email provider: Send email with Cognito
iv. App Integration
- User pool name:
user-auth-reading-list-api
- App type: Public client
- App client name: lambda-jwt-auth
- Authentication flow:
ALLOW_CUSTOM_AUTH
,ALLOW_REFRESH_TOKEN_AUTH
,ALLOW_USER_PASSWORD_AUTH
,ALLOW_USER_SRP_AUTH
- (optional) Enable Cognito Hosted UI: true
Note
For Domain section, you can use either Cognito domain or custom domain if you have. If you choose Cognito domain, make sure it unique. For example, you can use your name for your cognito domain (e.ghttps://shinji-reading-list-api.auth.ap-southeast-1.amazoncognito.com
).- Callback URLs:
http://localhost
This URL is used for testing purposes only. You can change it to your site or your API endpoint afterwards.
- OpenID Connect scopes: Email, OpenID, Phone, Profile
- Access token expiration: 1 day(s)
- ID token expiration: 1 day(s)
- Callback URLs:
-
Attach Lambda trigger to Cognito user pool.
π‘ TIP
You can setup this after completing the Lambda section.
- Trigger type: Authentication
- Authentication: Pre token generation trigger
- Lambda function: use Lambda Pre-Token Generation.
-
Clone this repository
git clone https://github.com/zrierc/reading-list-api.git
-
Navigate to
functions/
cd functions/
-
Install required dependencies
yarn
or
npm i
-
Build your lambda functions code and dependencies to zip
yarn build
or
npm run build
- Role name:
reading-list-fn-role
- Trusted entitiy Type: AWS service
- Use cases: Lambda
- Permission policies:
AWSLambdaBasicExecutionRole
(AWS Managed)AmazonDynamoDBFullAccess
(AWS Managed)
i. Create Lambda function with following configurations:
- Function name:
preTokenGenerationFn
- Runtime: Node.js 16.x
- Handler:
cognito-auth/preTokenGeneration.handler
- Timeout: 60s
ii. Publish functions code that you already build before.
-
Navigate to
functions/
directorycd functions/
-
Publish code via AWS CLI
aws lambda update-function-code \ --function-name preTokenGenerationFn \ --zip-file fileb://functions.zip
i. Create Lambda function with following configurations:
- Function name:
listBookFn
- Runtime: Node.js 16.x
- Handler:
book/listBook.handler
- Execution role: select
reading-list-fn-role
that you already created before. - Timeout: 60s
- Environment variables:
TABLE_NAME
: dynamoDB table name (reading-list-db
).COGNITO_USER_POOL_ID
: Id of Amazon Cognito user pool that you already created before.COGNITO_CLIENT_ID
: Id of user pool app client that you already created before.
ii. Publish functions code that you already build before.
-
Navigate to
functions/
directorycd functions/
-
Publish code via AWS CLI
aws lambda update-function-code \ --function-name listBookFn \ --zip-file fileb://functions.zip
i. Create Lambda function with following configurations:
- Function name:
getBookFn
- Runtime: Node.js 16.x
- Handler:
book/getBook.handler
- Execution role: select
reading-list-fn-role
that you already created before. - Timeout: 60s
- Environment variables:
TABLE_NAME
: dynamoDB table name (reading-list-db
).COGNITO_USER_POOL_ID
: Id of Amazon Cognito user pool that you already created before.COGNITO_CLIENT_ID
: Id of user pool app client that you already created before.
ii. Publish functions code that you already build before.
-
Navigate to
functions/
directorycd functions/
-
Publish code via AWS CLI
aws lambda update-function-code \ --function-name getBookFn \ --zip-file fileb://functions.zip
i. Create Lambda function with following configurations:
- Function name:
saveBookFn
- Runtime: Node.js 16.x
- Handler:
book/saveBook.handler
- Execution role: select
reading-list-fn-role
that you already created before. - Timeout: 60s
- Environment variables:
TABLE_NAME
: dynamoDB table name (reading-list-db
).COGNITO_USER_POOL_ID
: Id of Amazon Cognito user pool that you already created before.COGNITO_CLIENT_ID
: Id of user pool app client that you already created before.
ii. Publish functions code that you already build before.
-
Navigate to
functions/
directorycd functions/
-
Publish code via AWS CLI
aws lambda update-function-code \ --function-name saveBookFn \ --zip-file fileb://functions.zip
i. Create Lambda function with following configurations:
- Function name:
deleteBookFn
- Runtime: Node.js 16.x
- Handler:
book/deleteBook.handler
- Execution role: select
reading-list-fn-role
that you already created before. - Timeout: 60s
- Environment variables:
TABLE_NAME
: dynamoDB table name (reading-list-db
).COGNITO_USER_POOL_ID
: Id of Amazon Cognito user pool that you already created before.COGNITO_CLIENT_ID
: Id of user pool app client that you already created before.
ii. Publish functions code that you already build before.
-
Navigate to
functions/
directorycd functions/
-
Publish code via AWS CLI
aws lambda update-function-code \ --function-name deleteBookFn \ --zip-file fileb://functions.zip
Check this common issues:
β Failed running build command
- Make sure npm or yarn already installed on your machine.
- Make sure zip and unzip already installed on your machine. Click here to read guide how to install zip and unzip in Linux machine.
- Make sure your machine support Linux/Unix commands to run build script. For Windows user, you can try to install WSL 2 to run Linux on your Windows machine OR you can create custom build script using powershell/cmd. Don't forget to change
build
script command insidepackage.json
.
β Error when publish code to Lambda using AWS CLI
- Make sure you code already built and
function.zip
in the same directory asfunctions/
exists. - Make sure you already installed AWS CLI on your machine. Click here to read more about how to install the AWS CLI.
- Setup IAM credentials for authentication AWS CLI. Click here to read more about how to setup credentials.
Note
Make sure your IAM credentials for AWS CLI hasAWSLambda_FullAccess
policy.
-
Select AWS region.
-
Create Amazon API Gateway with following configurations:
- API type: HTTP API
- API name:
reading-list-api
-
Create routes:
-
Route for list books
- method:
GET
- path:
/book
- method:
-
Route for add new books
- method:
POST
- path:
/book
- method:
-
Route for get detail book
- method:
GET
- path:
/book/{id}
- method:
-
Route for delete book
- method:
DELETE
- path:
/book/{id}
- method:
-
-
Attach lambda integrations:
Note
Make sure you enable 'Grant API Gateway permission to invoke your Lambda function' option.-
Integration for list books API
- Route:
GET /book
- Integration type: Lambda function
- Target: choose
listBookFn
- Route:
-
Integration for add new books API
- Route:
POST /book
- Integration type: Lambda function
- Target: choose
saveBookFn
- Route:
-
Integration for get detail book API
- Route:
GET /book/{id}
- Integration type: Lambda function
- Target: choose
getBookFn
- Route:
-
Integration for delete book API
- Route:
DELETE /book
- Integration type: Lambda function
- Target: choose
deleteBookFn
- Route:
-
-
Add cognito as API authorization:
-
Create authorizer with following configurations:
- Authorizer type: JWT
- Name:
JWTCognitoAuth
- Issuer URL:
https://cognito-idp.<aws-region>.amazonaws.com/<your_cognito_userpool_id>
- Audience:
<cognito_app_client_id_of_userpool>
π‘ TIP
<your_cognito_userpool_id>
in Issuer URL should be same asCOGNITO_USER_POOL_ID
value on Lambda environment variables. While, Audience value should be same asCOGNITO_CLIENT_ID
value on Lambda enviroment variables. -
Attach authorizer for all routes.
-
You can create via AWS CLI:
-
Create user
aws cognito-idp sign-up \ --client-id <cognito_app_client_id_of_userpool> \ --username <your_username> \ --user-attributes Name=email,Value=<your_mail> \ --password <your_password>
-
Verify user
aws cognito-idp admin-confirm-sign-up \ --user-pool-id <your_cognito_userpool_id> \ --username <your_username>
You can also create user via Amazon Cognito console. If you enable hosted UI when setup Cognito, you can create user via hosted UI.
π§ͺβ Scenario
For testing, you're going to do 3 scenarios:
- Using random string as authorization token and without using authorization token β which will make the requests failed
- Using JWT token from Amazon Cognito which will led to successful request
- Test all API endpoint
-
First scenario:
-
Open terminal
-
Execute following commands:
without authorization token
curl https://<your-http-api-gateway-endpoint>/book
with random string as authorization token
curl -H "Authorization: MY-RANDOM-STRING" https://<your-http-api-gateway-endpoint>/book
-
If you see the following response, then your cognito JWT authorizer works correctly
{ "message": "Unauthorized" }
-
-
Second scenario:
-
Open terminal
-
Get JWT token from Cognito using AWS CLI
aws cognito-idp initiate-auth \ --auth-flow USER_PASSWORD_AUTH \ --client-id <cognito_app_client_id_of_userpool> \ --auth-parameters USERNAME=<your-username>,PASSWORD='<your-password>'
If you see the following response, copy token from
IdToken
{ "ChallengeParameters": {}, "AuthenticationResult": { "AccessToken": "a1b2c3d4e5c644444555556666Y2X3Z1111", "ExpiresIn": 3600, "TokenType": "Bearer", "RefreshToken": "xyz654cba321dddccc1111", "IdToken": "a1b2c3d4e5c6aabbbcccddd" } }
-
-
Test
GET /book
endpoint to list all booksExecute the following command:
curl -H 'Authorization: <paste-your-IdToken>' \ https://<your-http-api-gateway-endpoint>/book
If you see the following response, then it works correctly
{ "data": [] }
-
Test
POST /book
endpoint to add new booksSample data (single):
{ "bookTitle": "Cosmos", "bookAuthor": "Carl Sagan" }
Sample multiple data:
[ { "bookTitle": "Chainsaw Man, Vol. 1", "bookAuthor": "Tatsuki Fujimoto" }, { "bookTitle": "Chainsaw Man, Vol. 2", "bookAuthor": "Tatsuki Fujimoto" }, { "bookTitle": "Chainsaw Man, Vol. 3", "bookAuthor": "Tatsuki Fujimoto" } ]
Execute the following commands:
Send data (single)
curl -X POST \ https://<your-http-api-gateway-endpoint>/book \ -H 'Authorization: <paste-your-IdToken>' \ -H 'Content-Type: application/json' \ -d '{"bookTitle": "Cosmos", "bookAuthor": "Carl Sagan"}'
Send multiple data
curl -X POST \ https://<your-http-api-gateway-endpoint>/book \ -H 'Authorization: <paste-your-IdToken>' \ -H 'Content-Type: application/json' \ -d '[{"bookTitle":"Chainsaw Man, Vol. 1","bookAuthor":"Tatsuki Fujimoto"},{"bookTitle":"Chainsaw Man, Vol. 2","bookAuthor":"Tatsuki Fujimoto"},{"bookTitle":"Chainsaw Man, Vol. 3","bookAuthor":"Tatsuki Fujimoto"}]'
If you see the following response, then it works correctly
{ "message": "success" }
π‘ TIP
To make sure data already stored on database, you can check the data that you sent in DynamoDB Console. Learn more.
If successful, the data that you sent before will exist in the DynamoDB table as items. -
Test
GET /book/{id}
endpoint to get detail bookGet current book that available
curl -H 'Authorization: <paste-your-IdToken>' \ https://<your-http-api-gateway-endpoint>/book
If you see the following response, copy id from
readingID
Note
readingID
may be different value than the example. Adjust the id with your output.{ "data": [ { "readingID": "abcd1234-ab12-cd23-ef45-abcdef123456", "userID": "john", "bookTitle": "Cosmos", "bookAuthor": "Carl Sagan", ... }, ... ] }
Get detail book:
curl -X GET \ https://<your-http-api-gateway-endpoint>/book/<readingID> \ -H 'Authorization: <paste-your-IdToken>'
If you see the following response, then it works correctly
{ "readingID": "abcd1234-ab12-cd23-ef45-abcdef123456", "userID": "john", "bookTitle": "Cosmos", "bookAuthor": "Carl Sagan", "dateAdded": 1690370601856, "readingStatus": "added", "lastUpdated": 1690370601856, "lastPageRead": null "dateFinished": null, }
-
Test
DELETE /book/{id}
endpoint to delete bookGet current book that available
curl -H 'Authorization: <paste-your-IdToken>' \ https://<your-http-api-gateway-endpoint>/book
If you see the following response, copy id from
readingID
Note
readingID
may be different value than the example. Adjust the id with your output.{ "data": [ { "readingID": "abcd1234-ab12-cd23-ef45-abcdef123456", "userID": "john", "bookTitle": "Cosmos", "bookAuthor": "Carl Sagan", ... }, ... ] }
Delete book
curl -X DELETE \ https://<your-http-api-gateway-endpoint>/book/<readingID> \ -H 'Authorization: <paste-your-IdToken>' \ -H 'Content-Type: application/json'
If you see the following response, then it works correctly
{ "message": "success" }
π‘ TIP
To make sure data already deleted on database, you can check the data that you sent in DynamoDB Console. Learn more.
If successful, the data you deleted before will no longer exist in the DynamoDB table.
This workshop uses AWS services that are mostly covered by the Free Tier allowance - ONLY if your account is less than 12 months old. For accounts passed the free tier eligibility, it may incur some costs. To minimize the cost, make sure you delete resources used in this workshop when you are finished.
Follow the sequence according to the steps to delete the AWS resources:
-
Delete HTTP API Gateway via Amazon API Gateway console.
-
Delete Cognito User Pool via Amazon Cognito console.
For detail guide, please follow tutorial from official docs.
-
Delete following Lambda functions via AWS Lambda Functions console:
preTokenGenerationFn
listBookFn
saveBookFn
getBookFn
deleteBookFn
Note
You can also delete custom IAM role for Lambda that you've created before via IAM console. Learn more. -
Delete DynamoDB table via DynamoDB console.
Click here to read more how to delete tables in DynamoDB.