This project chooses option 2 serverless according to the project rubric.
It builds on top of my solution to the project for module 5 Develop and Deploy Serverless Apps, which is the backend for an TODO application. According to this discussion reusing this project's code base is accepted for the capstone.
Compared to the TODO application, this project features:
- All used node modules updated to current versions.
- Data model changed and extended with more fields.
- Data model has constraints on the fields, e.g. min/max string size to protect this backend application from useless or malicious requests. A client like a UI is supposed to check these constraints before sending requests to this backend application.
- When the client asks for an upload URL for the attachment to an item, it not only receives an upload URL, but also the URL under which the attachment would be downloadable if it was uploaded. This eliminates the need for the client to again request all items for a user only to receive the attachment URL.
- However, the data model will only contain an attachment URL if an attachment was really uploaded. This is implemented by means of S3 notifying the backend that an object was uploaded. In this way a client asking the backend for an upload URL but then not uploading an object would not lead anymore to the backend assuming an object was uploaded in S3.
- A client can delete the attachment without deleting the item.
- Deleting an item from DynamoDB deletes its attachments from S3 as well.
As the focus is on backend programming, I do not provide a frontend client, but rather a Postman collection, as suggested in this discussion.
This application is the backend for storing cooking recipes. It will allow creating/removing/updating/fetching recipe items. Each recipe item can optionally have one attachment, e.g. an image of an intermediate steps, or the recipe in PDF format, or whatever makes sense for the client. The attachment can only be created after the recipe item was created. The attachment can be deleted without the recipe item being deleted. Each user only has access to recipe items that s/he has created.
The application stores recipe items as JSON structure in DynamoDB and attachments as binary objects in S3.
Each recipe item stored in DynamoDB and returned to the client on recipe item creation requests contains the following fields:
userId
(string) - id of the user owning this itemrecipeId
(string) - a unique id for a recipe itemname
(string, min 1, max 100 chars) - name of a recipe item (e.g. "My famous soup")recipe
(string, min 1, max 10000 chars) - the recipe itself in a form depending on the client. Suggested using a form that the user can enter easily, and the client can render nicely, e.g. MarkDownattachmentUrl
(string, optional) - a URL pointing to a binary object attached to a recipe item. The object itself is stored in S3.
Example:
{
"userId": "someuser",
"recipeId": "5c37344f-2af2-4876-b70b-546d0cf7f0a5",
"name": "My famous soup",
"recipe": "Ingredients: ...",
"attachmentUrl": "https://some-bucket.s3.region.amazonaws.com/someuser/5c37344f-2af2-4876-b70b-546d0cf7f0a5"
}
The list of recipe items returned by the request for all recipe items of a user is an
object containing a single property recipes
being a list of recipe items.
Example:
{
"recipes": [
{
"userId": "someuser",
"recipeId": "5c37344f-2af2-4876-b70b-546d0cf7f0a5",
"name": "My famous soup",
"recipe": "Ingredients: ...",
"attachmentUrl": "https://some-bucket.s3.region.amazonaws.com/someuser/5c37344f-2af2-4876-b70b-546d0cf7f0a5"
},
{
"userId": "someuser",
"recipeId": "6325652a-026c-4e1c-b454-8b7bb0d5f48c",
"name": "My famous desert",
"recipe": "For this delicious desert you take ..."
}
]
}
Each recipe item creation or update request contains the following fields:
name
(string, min 1, max 100 chars) - name of a recipe item (e.g. "My famous soup")recipe
(string, min 1, max 10000 chars) - the recipe itself in a form depending on the client. Suggested using a form that the user can enter easily, and the client can render nicely, e.g. MarkDown
Example:
{
"name": "My famous soup",
"recipe": "Ingredients: ..."
}
The return object for the generateAttachmentUploadUrl
function. Contains the pre-signed URL to upload the
attachment with a validity of 5 min.
Also contains the URL under which the attachment would be downloadable if it was uploaded.
{
"attachmentUploadUrl": "https://some-bucket.s3.region.amazonaws.com/someuser/5c37344f-2af2-4876-b70b-546d0cf7f0a5?someauthorizationstring",
"attachmentDownloadUrl": "https://some-bucket.s3.region.amazonaws.com/someuser/5c37344f-2af2-4876-b70b-546d0cf7f0a5"
}
An attachment can be any object which makes sense for the client, e.g. an image. Each recipe item can have zero or one attachment. No other restriction so far.
Data model correctness in recipe creation and recipe update towards the application is enforced via JSON schema verification on the API gateway. As there is no requirement on shape or size of objects stored in S3, there is no check for S3.
Authentication is done via Auth0 and asymmetrically encrypted JWT tokens. Authorization is implemented via a Lambda authorizer (formerly known as a custom authorizer) using the API Gateway.
A function implementing the Lambda authorizer (formerly known as a custom authorizer) for the API Gateway.
A function to return all recipe items for the user requesting it. The user id is extracted from the JWT token that is sent by the client. The function returns the list of recipe items using the recipe items model as shown above.
A function to create a new recipe item for the user requesting it. The user id is extracted from the JWT token that is sent by the client. The request must follow the creation request model as shown above, which is enforced. The answer contains the new recipe using the recipe item model as shown above.
A function to update a recipe item for the user requesting it. The user id is extracted from the JWT token that is sent by the client. The id of the recipe item that should be updated is passed as a URL parameter. The request must follow the update request model as shown above, which is enforced. It returns an empty answer.
A function to return a pre-signed URL that can be used to upload an attachment object for a recipe item for the user requesting it. The user id is extracted from the JWT token that is sent by the client. The id of the recipe item for which an attachment URL should be generated is passed as a URL parameter. It returns an upload URL and a download URL using the model of the attachment URL generation as shown above. The upload URL's expiration timeout is 5 min.
A function not to be called by a client, but instead called from S3 whenever an S3 object (attachment) was stored or updated. Will determine from the S3 object key the userId and recipeId and then store the attachment's URL to the recipe data stored in DynamoDB for this userId and recipeId.
A function to delete an attachment object for a recipe item for the user requesting it. The user id is extracted from the JWT token that is sent by the client. The id of the recipe item for which the attachment URL should be deleted is passed as a URL parameter. It is no error if the attachment object which should be deleted does not exist. It returns an empty answer.
A function to delete a recipe item and the associated attachment for the user requesting it. The user id is extracted from the JWT token that is sent by the client. The id of the recipe item that should be deleted is passed as a URL parameter. It returns an empty answer.
The application uses structured logging which end up in CloudWatch.
The application uses distributed tracing via X-Ray.
The provided Postman collection Capstone Project.postman_collection.json contains already prefilled variables for
apiId
, region
, auth0_domain
, and auth0_audience
.
The variables auth0_client_id
and auth0_client_secret
must be filled by the reviewer with the value given in the project submission.
The Postman collection Udacity Cloud Developer Capstone Project is pre-configured to authenticate against the Auth0 service. The Auth0 service is pre-configured to allow Auth0, Google or GitHub accounts to use this recipe app. However, Google would not accept redirected login request from Postman as it considers Postman insecure. A real browser-based client could use Google.
As review please add the given values to the Postman collection variables auth0_client_id
and auth0_client_secret
.
Then on collection level go to Authorization, then click Get New Access Token and authenticate with one of the
given options, then click Use Token.
Gets all recipe items of the user represented by the JWT token. Built-in tests check on HTTP return code 200 and schema of the answer.
Creates a new recipe item for the user represented by the JWT token. Built-in tests check on HTTP return code 201 and schema of the answer.
A malformed recipe creation request which will be rejected by API gateway schema checks. Built-in tests check on HTTP return code 400 and content of failure message.
Updates a new recipe item for the user represented by the JWT token. Built-in test checks on HTTP return code 204.
A malformed recipe update request which will be rejected by API gateway schema checks. Built-in tests check on HTTP return code 400 and content of failure message.
Deletes a recipe item and its potential attachment for the user represented by the JWT token. Built-in test checks on HTTP return code 204.
Requests an URL to which the client could upload an attachment for the user represented by the JWT token. Built-in tests check on HTTP return code 200 and schema of the answer.
Suggested test: After the upload URL is received, the recipe item data model for this recipe Id is not yet changed to include the attachment. This can be tested by calling the Get all recipes API endpoint after getting the attachment URL. Then upload the attachment to the given pre-signed URL using Upload attachment request. Then call again the Get all recipes API endpoint. Now the recipe item for which the attachment was uploaded contains the attachment URL.
Stores an attachment in S3. To use it replace the whole URL with the pre-signed URL received from Get attachment URL request. Built-in test checks on HTTP return code 200.
Suggested test: See Get attachment URL. Also try to download the attachment from the URL given in the recipe item's data model.
Deletes an attachment from S3 and from the recipe item's data model. Built-in test checks on HTTP return code 204.
Suggested test: After uploading the attachment to the given pre-signed URL using Upload attachment request, call Get all recipes API endpoint to see that the attachment's URL is part of the recipe item's data model. Now delete the attachment. Then call again the Get all recipes API endpoint to see that the attachment's URL was deleted from the recipe item's data model. Try again to download the attachment from the given URL, which will fail as the object was removed from S3.