Skip to content
This repository has been archived by the owner on Dec 10, 2021. It is now read-only.

docs: add API design overview #16

Open
wants to merge 14 commits into
base: master
Choose a base branch
from

Conversation

sf-clouder
Copy link

No description provided.

Copy link
Contributor

@afaq-sf afaq-sf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sf-clouder Your plan and it looks reasonable to me. Thank you for sharing the early draft via PR.

Please proceed with the test.


## Questions

1. Do we need to secure the API? (API key, lambda authorizer, something else?)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great question. Please secure the API with an authorizer lambda, if time allows.


1. Do we need to secure the API? (API key, lambda authorizer, something else?)

2. Should the API generate the unique user ID? Or does the user request the User ID, and the API determines it's uniqueness?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would ask the same question back to you: what would you recommend and why?


2. Should the API generate the unique user ID? Or does the user request the User ID, and the API determines it's uniqueness?

3. Do we want to store the data exactly as shown in the model? (i.e. address as a simple string). Or do we want to break address into an object with attributes like street, city, state, zip, etc?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The model shown is only for the sake of simplicity.

However, we would welcome further improvements into an object model based on what you might deem appropriate. Address as an object definitely makes sense to me, in that regard.

@sf-clouder
Copy link
Author

Hello Superformula! Please review this submission for the cloud native backend test. I have modified README.md with details about the project and how to deploy it. Thank You!

Copy link
Contributor

@afaq-sf afaq-sf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sf-clouder I have left some questions on the PR. I would like to get your thoughts on them as much as time allows.


These may be used for further challenges. You can freely skip these if you are not asked to do them; feel free to try out if you feel up to it.
## Create a User
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you ever used API Gateway export to dynamically generate API documentation?

https://docs.aws.amazon.com/cli/latest/reference/apigateway/get-export.html

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your review, Afaq. Yes, the ability to export was a primary reason behind defining the methods and validation within API Gateway instead of proxying and handling it within Lambda.

"tabWidth": 4,
"useTabs": false,
"semi": true
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed some of your files lack newlines at the end of the file.
have you considered using editorconfig or similar tool to auto-add line endings?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great suggestion. I read up on why an EOF newline can be helpful. Prior, I knew about it as an option, but didn't understand the reason behind it until you pointed it out. Thank you.

partitionKey: { name: 'pk', type: dynamodb.AttributeType.STRING },
sortKey: { name: 'sk', type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
removalPolicy: cdk.RemovalPolicy.DESTROY, // allow table delete
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you foresee any issues in real-world applications if the stack allows the table to be removed?

Copy link
Author

@sf-clouder sf-clouder Jan 14, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For sure. This line is not desired in a production setting as it would allow a delete of the table. I left it there only so you could fully remove the stack upon a 'cdk destroy' during this demo.

code: new lambda.AssetCode(crudAsset),
handler: crudHandler,
runtime: lambda.Runtime.NODEJS_12_X,
memorySize: 1024,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would you recommend memorySize configurable?
For example for different configurations between deployable environments.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be fed in with environment variables, or computed in the CDK stack itself. A variation could look to AWS SSM to avoid putting the values in the codebase. Another option is to bundle config files with each lambda with definitions for each environment. Let me know if you have any other techniques or interesting ideas here.

const item = typeof event.body === 'object' ? event.body : JSON.parse(event.body);

// Get UUID for user id from the lambda request id
const userId = context.awsRequestId;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting choice here. Out of curiosity, what is the reasoning for using aws request ID as the UUID of the new user?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the past I've used the 'uuid' package to generate ids. However, I recently learned that the Lambda request ID is itself a V4 UUID, so I thought it would be good to take advantage of it and save the import. This is similar, though not exact, to '$util.autoId()' that is available in VTL.

}

Log.error('User Create Error', new Error(err));
return httpResponse(400, err);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is 400 a reasonable default error status?

For example, what if the failure is because of inability to reach Dynamo or code-related programmer error?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, good point. I was using 400 was a catch all here, but I agree that more meaningful status codes in the 4xx and 5xx range with appropriate error messages should be returned.

} catch (err) {
// dynamo did not store because the record already exists
if (err.code === 'ConditionalCheckFailedException') {
return httpResponse(409, 'a user with this ID already exists');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please share your thoughts on the pros/cons of the approach you've taken here for checking if the user already exists.

Especially when weighted against doing an initial check against the table.

Copy link
Author

@sf-clouder sf-clouder Jan 14, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In DynamoDB, when doing a put to an existing primary key, it will overwrite an existing item, which may not be the desired effect. By adding the conditional check, verifying that a particular attribute does not exist, it blocks the overwrite. My approach to using the conditional write was to avoid having to first do a 'get' to read any potential existing item to see if it exists, and then having to follow it up with a 'put'.

Thank you for calling attention to this section as it made me realize my check was against the sort key 'sk' and should have been against the partition key 'pk'. Earlier in the design I was storing the UUID as the sort key, but then changed to store the UUID as the partition key instead and forgot to update the condition expression. This code should be updated so that the conditional check is against the partition key: 'attribute_not_exists(pk)'.

Edit: Sorry, it's been a couple weeks since I submitted the PR. Looking closer at my code it doesn't matter whether the condition expression check is against the pk or the sk, since it's simply checking for the existence of the attribute, not it's value. Checking for the existence of either works equally well in this case.

const originalParams = { TableName: TABLE_NAME, Key: { pk: userId, sk: `USER` } };
const doc = await db.get(originalParams).promise();
const { Item } = doc;
if (!Item) throw 'user does not exist';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a reasonable check.
What are your thoughts on returning 200 rather than throwing an error here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see 200 being an appropriate response in this situation as well since, like a successful delete, it is confirming that the user does not exist.

await db.put(backupParams).promise();

// delete the original
await db.delete(originalParams).promise();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would you go about building an audit log of all delete requests and capturing who initiated it as well as other details?

Copy link
Author

@sf-clouder sf-clouder Jan 14, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of monitoring the Dynamo stream for changes like this so we don't tie up the delete lambda with this extra work. We could also move the entire delete request to an SQS queue, immediately returning a 202 status code (Accepted), and then have a lambda process the queue and record the audit data.

}
};

const httpResponse = (statusCode, body) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is common code repeated within lambdas, how would you recommend abstracting it to shared tooling?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normally I would put code like this in a shared module and import it into each lambda. However, the CDK only supports deploying the lambda by specifying a folder or a ZIP file. So putting it in a common module would have required duplicating the module in each lambda's directory, or putting all lambda code in the same directory, which creates lambdas with unused code from all the other lambdas. Neither of which was ideal. Because of this, and since the HTTP response function was small, I simply chose to duplicate it in each lambda.

Eventually the AWS CDK is going to support efficiently bundling lambda code during synthesis of the CloudFormation (see here). Until then, I would use webpack or parcel to to solve this, or write a build script that creates a ZIP package for each Lambda as a pre-deploy step.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants