The goal of this project is to build and deploy a serverless application on AWS. To accomplish this task, I decided to build a "Would You Rather" game with a React frontend and a Node.JS backend using a serverless framework.
I used a CloudFormation Template to deploy:
- An API gateway
- Multiple Lambda Functions with proper IAM permissions
- 2 DynamoDB tables to store records of Users and Questions
- S3 bucket to store images uploaded by Users
On top of this architecture, I added:
- A S3 bucket to host my website
- A registered domain name: hmisonne.com on Route53
- A CloudFront distribution to accelerate the content delivery
- A SSL certificates issued from AWS Certificate Manager to secure network communications through https traffic.
This project is also using a fully secured Authentification system through Auth0.
This application allows users to play the would you rather game by answering, creating questions and uploading images.
This application is currently divided into 4 screens:
A list of all polls posted is displayed on the home screen. The user can toggle a button to see either the polls that has been answered or unanswered by himself. The option to delete a question or upload a picture will only be available for questions that has been authored by the specific user.
The user can upload a picture to a question that he created. Once uploaded, the picture will be shown on the Dashboard.
By clicking on the green button on one specific question from the Dashboard, the user is able to vote provided that he has not done so before. There is 2 options for each poll. Upon voting in a poll, information about how many people voted for one particular option is displayed along with the user's response.
The user can submit a new question by entering 2 options. Once created, the new question will be added to the unanswered questions of the dashboard.
A version of this website is already running on https://hmisonne.com . If you wish to run this application locally follow these instructions:
- Update the callbackUrl on the client/config.ts file
export const authConfig = {
...
callbackUrl: 'https://localhost:3000/callback'
}
- and run the following commands:
cd client
npm install
npm run start
The frontend will be running on http://localhost:3000/
- Node.js
- AWS account
- Auth0 account
-
Install serverless
npm install -g serverless
-
On your AWS account, set up a new user in IAM named "serverless" with Programmatic access and with AdministratorAccess policy attached and save the access key and secret key.
-
Configure serverless to use the AWS credentials you just set up:
sls config credentials --provider aws --key YOUR_ACCESS_KEY --secret YOUR_SECRET_KEY --profile serverless
-
On your Auth0 account, create a new application with React framework.
-
On the Settings -> Application URIs -> Allowed Callback URLs insert:
http://${hosted_domain}/callback
, and Allowed Web Origins:http://${hosted_domain}/
If deployed locally, replace the hosted_domain with localhost:3000
To deploy this application from the backend folder and generate your apiId, use the following commands: sls deploy -v
. Make sure before running this command that you have run: npm install
To have the application running on your local machine, edit the client/src/config.ts
file to set correct parameters. After running sucessfully sls deploy -v
, the endpoints generated by the sls command will be displayed with this configuration https://${apiId}.execute-api.us-east-1.amazonaws.com/dev
. Replace apiId with the value
const apiId = 'YOUR_API_ID'
Then replace, the domain and clientId with the values found on your Auth0 account:
export const authConfig = {
domain: 'YOUR_AUTH0_DOMAIN',
clientId: 'YOUR_CLIENT_ID',
callbackUrl: 'http://localhost:3000/callback'
}
Then run the following commands:
cd client
npm install
npm run start
The backend is using DynamoDB databases provisioned on the serverless file by AWS.
The application store QUESTION items, and each QUESTION item contains the following fields:
questionId
(string) - a unique id for an itemuserId
(string) - the user id that created this QUESTION itemtimestamp
(string) - date and time when an item was createdoptionOneText
(string) - name of 1st option (e.g. "Win the lottery")optionTwoText
(string) - name of 2nd option (e.g. "Have many friends")optionOneVote
(array) - List of userId(s) who voted for the 1st optionoptionTwoVote
(array) - List of userId(s) who voted for the 2nd optionattachmentUrl
(string) (optional) - a URL pointing to an image attached to a QUESTION item
The application store User information, and each USER item contains the following fields:
userId
(string) - a unique id for a useranswers
(object) - with the questionId as attribute and the option selected (optionOne or optionTwo) for each answered question. This allows to filter questions for a specific user by "answering status"
Auth
- this function implement a custom authorizer for API Gateway that has been added to all other functions.
GetQuestions
- return all QUESTIONs for a current user . A user id can be extracted from a JWT token that is sent by the frontend. Receive 2 optional parameters:
- nextKey: Next key to continue scan operation if necessary
- limit: Maximum number of elements to return
GET https://{{apiId}}.execute-api.us-east-1.amazonaws.com/dev/questions?limit=2
It returns data that looks like this:
{
"items": [
{
"optionTwoText": "Go West",
"optionOneText": "Go East",
"optionTwoVote": [
"4551045727"
],
"questionId": "286c3233-a2fa-496a-9bc9-9d8cab3ed5f5",
"attachmentUrl": "https://serverless-wyr-files-dev.s3.amazonaws.com/286c3233-a2fa-496a-9bc9-9d8cab3ed5f5",
"userId": "4551045727",
"timestamp": "2020-06-09T21:58:00.982Z"
},
{
"optionTwoText": "Two things",
"optionOneText": "One thing",
"optionTwoVote": [
"1234567891"
],
"questionId": "91ea0481-e7c3-4b39-83a6-edaf3f2ff46e",
"userId": "4551045727",
"attachmentUrl": "https://serverless-wyr-files-dev.s3.amazonaws.com/91ea0481-e7c3-4b39-83a6-edaf3f2ff46e",
"optionOneVote": [
"4551045727",
"1234567891"
],
"timestamp": "2020-06-06T01:47:03.364Z"
}
]
}
GetQuestion
- return a single QUESTION item by userId and by questionId.
GET https://{{apiId}}.execute-api.us-east-1.amazonaws.com/dev/users/{userId}/questions/{questionId}
It returns data that looks like this:
{
"item": {
"optionTwoText": "Two things",
"optionOneText": "One thing",
"optionTwoVote": [
"4551045727",
"5588959907"
],
"questionId": "91ea0481-e7c3-4b39-83a6-edaf3f2ff46e",
"attachmentUrl": "https://serverless-wyr-files-dev.s3.amazonaws.com/91ea0481-e7c3-4b39-83a6-edaf3f2ff46e",
"userId": "4551045727",
"optionOneVote": [
"4551045123",
],
"timestamp": "2020-06-06T01:47:03.364Z"
}
}
CreateQuestion
- create a new QUESTION for a current user. A shape of data send by a client application to this function can be found in therequests/CreateQuestionRequest.ts
file
POST https://{{apiId}}.execute-api.us-east-1.amazonaws.com/dev/questions
body: {
"optionOneText": "Go East",
"optionTwoText": "Go West"
}
It returns a new QUESTION item that looks like this:
{
"item": {
"optionTwoText": "Go West",
"optionOneText": "Go East",
"questionId": "286c3233-a2fa-496a-9bc9-9d8cab3ed5f5",
"userId": "4551045727",
"timestamp": "2020-06-09T21:58:00.982Z"
}
}
SubmitVote
- update a QUESTION item with the vote submitted by a current user. It uses the userId which is here the creator of the question (not the current user) and the questionId in the path to identify a question.
PATCH https://{{apiId}}.execute-api.us-east-1.amazonaws.com/dev/users/{userId}/questions/{questionId}
It receives an object that contains that the option selected:
{
"optionSelected": "OptionOne",
}
The id of the current user (responder of the poll) is extracted from a JWT token passed by the client.
This function will update the QUESTION item as well as the USER item:
- It will append the userId (responder id) to the optionSelected parameter (optionOneVote/optionTwoVote) of the QUESTION item:
{
...
"optionOneVote": [..., "4551045123"]
}
- It will add a new element to the answers parameter of the USER item with the questionId as key and optionSelected as value.
{
...
"answers": {
...,
"286c3233-a2fa-496a-9bc9-9d8cab3ed5f5": "optionOne"
}
}
It returns an empty body.
DeleteQuestion
- delete a QUESTION item created by a current user. Expects an id of a QUESTION item to remove.
DELETE https://{{apiId}}.execute-api.us-east-1.amazonaws.com/dev/questions/{questionId}
It returns an empty body.
GenerateUploadUrl
- it is used to add a picture to a QUESTION item.
POST https://{{apiId}}.execute-api.us-east-1.amazonaws.com/dev/questions/{questionId}/attachment
It returns a JSON object that looks like this:
{
"uploadUrl": "https://{{s3-bucket-name.s3}}.eu-west-2.amazonaws.com/{{image-name}}.png"
}
This pre-signed URL is then used to upload an attachment file to a s3 bucket for a QUESTION item. It then updates the QUESTION item by adding an attachmentUrl key with the s3 location of the item as value {attachmentUrl: "https://${bucketName}.s3.amazonaws.com/${questionId}"}
createUser
- add a new user to the database:
POST https://{{apiId}}.execute-api.us-east-1.amazonaws.com/dev/users
it returns a JSON object that looks like this:
{
"user": [
{
"answers": {},
"userId": "5588959907"
}
]
}
getResponsesByUser
- returns user information for a current user using the JWT token that is sent by the frontend.
GET https://{{apiId}}.execute-api.us-east-1.amazonaws.com/dev/users
{
"user": [
{
"answers": {
"286c3233-a2fa-496a-9bc9-9d8cab3ed5f5": "optionOne",
"286c3233-a2fa-496a-9bc9-9d8cab3ed5f5": "optionTwo"
},
"userId": "5588959907"
}
]
}
All functions are already connected to appropriate events from API Gateway.
An id of a user can be extracted from a JWT token passed by a client.
The serverless.yml
file includes all of these functions as well as a DynamoDB table and a S3 bucket in the resources
.
To test the endpoints of this application download the postman collection: ServerlessWYR.postman_collection.json
I used Travis to automically deploy and update the application. Each time a new commit is pushed to the master branch, a new build is triggered. A commit pushed to the development branch won't trigger a new deployment, which allows to integrate changes without affecting the production environment.