Throughout all the changes in web development over the years, the server has been a constant. Regardless of the languages, tools, and frameworks used, there’s always a server running the code. And that’s something that hasn’t changed. What has changed is that cloud providers now make it easy for software engineers to focus on writing their code, without having to focus on the underlying server.
In this course, you'll build a serverless web application using Python 3.6. You'll use Lambda, API Gateway, S3, DynamoDB, and Cognito to create a multi-user to-do list application based on Vue.js.
Note: The Apple M1 chip isn't compatible with this course currently. We recommend using a different device.
- Outline the architecture of a serverless web application
- Set up the AWS services required for the app
- Create and deploy an API using Python 3.6
- Explain the value of creating unit tests
- Use a Cognito User Pool within your app
- DevOps Engineers
- Site Reliability Engineers
- Familiar with AWS
- Development experience
- Familiar with the CLI
Welcome back! In this lesson, we'll be walking through the front-end and back-end code, so by the end of the lesson, you should be familiar with how the back-end API is structured. Let's start off by reviewing the front-end code. This is a basic Vue.js application. If you're not familiar with Vue.js,
If we scroll down, let's look at List Rendering because this is going to be something we're going to do. You can see that this is the HTML and it's saying for each item and items, loop over and render a new list item so this whole thing will get rendered, and it's going to fill in this text section here with the item.
message. In this section here, what this is saying is find the element that has an id of example-1 and kind of make that the context for this. And then it gives us some properties. We have data, and then under that is items. So we have have our list of items, and inside of each item is a message, and there's foo and bar.
That's here. And that's going to be the context for this app. So, just make this a little bit bigger. There we go. So now you can see we have a model of username, a model of password, and then we have a click handler for login. So those get bound here. Data. Username. Password. Anytime these change, it will be reflected on the UI.
'NEEDS_UPDATE' that's for verification. Again, that's not really anything we're going to go in-depth on. I'm not going to dive into this too much, but I do wanna make sure you understand this kinda two-way binding effect. So, when you call the login function by using this click handler here by clicking on the submit, it fires of a code in this method, which is going to authenticate to Cognito.
So if we Go to Definition, you can see we're using the Amazon API. This is in the cognito-identity and cognito-sdk, this is where our functionality comes from. And we're setting up a new authentication details. We're setting up a new Cognito User, and then we're actually trying to authenticate the user.
And then we kick off our success or failures. So, when that happens, it's going to send back an off-token. That token proves that we are who we say we are. We get that in the form of a JSON web token, and then we can forward that back to the back-end, where Cognito will decode the token to access the claims.
And then we have our appUrl. This is going to be endpoint, where our API gateway is listening. So, there's some dummy values here. We'll set those later on, but just so you know, this is where you configure that. Okay, let's move on from the front-end to the back-end code and that's under the todo section and under this API directory.
So what we have here is a handful of Python files for each particular CRUD function. So we have our create, we have our read, which is a get, we have our update, and our delete, so that's our CRUD functionality. When it comes to creating native, serverless applications, each lambda function should do one thing, and one thing only.
That's the idea behind kinda the SAM Model, is you do one thing, and you do it well. That kind of philosophy will be very familiar to you, if you've done anything with Linux or Unix. So let's check this one out. We have our create function, and what we have is a handler, which has an event and a context passed in.
So this is the signature for you lambda function. Lambda is going to pass in an event, and then it will pass in context. Event is going to be relative to whatever triggered that event--remember Lambda can be triggered by a lot of different services. In our case, it will be triggered by API Gateway. So the event is going to contain information about the web request that was made.
It's going to have the event body, it's going to have query string, parameters, and things like that. Context is going to have information about the function. You're going to have things like the amount of time left before we timeout the function, the amount of memory available to us, etc. . . So if you scroll through, you can see that we're taking the event body, that is sent to us as JSON, and we're turning it into a dictionary, and we're putting in that data variable.
Okay. You can see that there is a whitelist. What this does, is it allows you to set the properties that the user is allowed to actually set in the database, because with the node-sql database, they can set any properties they want. If a user posted a JSON object with 400 properties that were all nonsense, that could be persisted if you're not filtering out all of that data.
So this is just what we want the user to be allowed to set for that record. So we set that, we grab our table name, and we're pulling that from an environment variable. Now, I propose that you use environment variables whenever possible for things that you want to be able to configure, because you don't have much say in what gets passed in.
You need some mechanism to make your code dynamic. You don't want a hard-coded table name, because when you wanna test that out, if you wanna deploy that to another environment, it gonna make it really difficult. So, using environment variables is going to allow you to kind of make that dynamic, you can change it on the fly, and you can deploy it to new regions.
You can deploy it to new environments without having to refactor your code. Same thing with region_name. This is going to automatically be set. This is going to be set to the east-1 region by default. However, lambda is also going to have this already set. So, you don't have to worry, it's pulling in a default of east-1, but since lambda is going to fetch whatever region you're using.
This is just so that the value exists for local stuff and for testing. Then we're using boto3. So, you can see we're importing boto3 up here. We're using boto3 for our interaction with AWS. In this case, it's just DynamoDB. And we're create a client, based on that. So, if you're not familiar with bodo3, it's the official Python library for AWS, and it has functionality for interacting with all of the different services.
In this case, we just need a client for DynamoDB. So we do that. We pass in the region that we want to have this set up in. And then we come down here. We parse the username from the claims. What that means is, when a user authenticates from Cognito, they're going to get that JSON web token back, and part of that is the claims.
That's the things that they're allowed access to. What we want is the username. So if we Go to Definition here, you can see we're just saying, take the event, grab the request context, the authorizer, the claims, and we want the Cognito username. Now, if this doesn't exist, this is gonna throw an exception, because it's trying to access keys that don't exist, so, if the user doesn't have access to do this, it's gonna throw an exception.
You might want to handle this differently in your own applications. For this kind of demo, this is alright. So let's go back to our create. So we fetched that, kind of, identification for the user. And then, we set the result to the create function here, but for calling respond, I'll just show you respond, and then we'll get into that create function.
If you look at this, API Gateway is going to expect that this how you're responses kind of look. You have your status code, you have your body, and your headers. I'm using 200 or 400 kind of, not really any in between here. API Gateway, depending on the error, might send its own status. You might get a 500, a 503, etc.
. . But I'm only using these two. This is not a best practice if you're going to create actual REST API's. You'll want to think about using the status code that's most appropriate: 201 for an updated item, etc. . . So, make sure that you put a little more effort into this, than this demo. This is just a starting place for you to jump off from.
So, everything is expected to be JSON, and we're allowing from multiple origins. So, we jump back to our create. The bulk of the work--so this is our handler. This is what lambda is going to call. This is going to kick off this create, by passing in all of the information we've kind of gathered. The way I like to break it out is, the lambda function is going to kick off the aggregation of all the data.
It's getting the whitelist. It's getting our table. It's getting our region and then it's gonna hand it off to some other function. To me, this makes it just a bit easier to test because we're separating the concerns. There's the code that actually does the action. In this case, it's a create. And then there's the code that kicks that off, by passing in all of the required variables.
So, create takes the client. That's our DynamoDB. It takes the user_id. It takes our data, our table, and our whitelist. If item isn't in the data, so we're looking for something in that dictionary called item, that's our todo item, then we're going to throw an exception. We set up a table, based on the table name.
We create a new dictionary, based on the whitelisted properties and we make sure that we have the user_id in there, because we don't want the user to set that. We want to set that ourselves. Otherwise, somebody could overwrite somebody else's to do list. And we set our todo item-- that's just a unique identifier--globally unique identifier, and then we set the created date.
So, this is just added as a, kind of a convenience. Some people like to create their to do lists with already completed tasks. It makes them feel better about all the things that they've already done that day. They can just see them all checked off, and that's kind of a nice feeling. So, that's why this little bit of code is here, if that didn't quite make sense.
And then we actually call the table, put_item, to put the whitelisted_data into DynamoDB. So that's just going to save that to DynamoDB. Then we return that. And that's what getting sent here in our response. We're just serializing the results of that, and sending it back to the user through API Gateway.
Now, let's look at get. This is going to look very familiar. Look it's pretty much the same thing. We have most of the same imports. We have our table_name. We have our region. We create our DynamoDB client. Our user_id: we're parsing it from claims. And our todo item is None. We try and fetch it from the query string params.
It's just an id. If it doesn't exist, that's okay. We're just gonna assume that the user wants to get all of the todo items for that user. And then we call the get_all or get_one. Get_one is just going to get the item, based on the user_id and the todo_id. If you remember when we set up the DynamoDB table, I said we're creating a composite key.
And what that will allow us to do, is say, I want all the results for a user, or I want this specific todo item. You'll always need the user_id, because while the user_id is unique, you're grabbing it for a specific user, so, you don't want somebody to be able to grab somebody else's id and then edit that todo item, in any way, so we make sure that you always have to have the user_id, which, by the way, is passed in by API Gateway through the JSON web token, so that's not something a user can fake.
Scrolling down to get_all, you can see that we do the same basic thing. We pass in the client. We pass in the table name, and the user_id, and what this will do, is it's gonna query everything based on this condition here of the key userId equals this user_id. So if there's an Items property in this dictionary, and we send back that value; otherwise, we send back an empty array.
So it should start to, kind of, feel very familiar. This looks just like the create, as far as the handler is concerned. And then it's really just the function that gets kicked off. Let's check out the update. Same basic thing. Update is going to be just like the create in that, remember we're getting some data posted to it.
And, then we grab the table name from our environment variables, we set up the region. We set up our client for DynamoDB. We grab our user_id, and then we kick off the update passing in the client, the user_id, the data, and the table_name. So update. . . It's a little different. We're not whitelisting properties.
We're kind of defining them here in this if statement. If the item is not in the data, or completed is not in the data, than we want to throw an exception, because you don't need to change the item. So this update allows you to change items. Maybe you're just clicking complete, and that's okay. Or maybe you're actually changing the item because there was a typo, so, either one of these could be allowed.
Also, you wanna make sure that the todo item exists in that. So you must have that set. And then this is a bit verbose, but basically, what this is trying to do, is create a update statement for the item, if it exists, for completed, if it exists, because we don't need to have an update statement that tries to update the item if there is no change to it, so, if it exists, we try and set the attribute name, we set the attribute value, and then we append this kind of string, which uses this syntax here of the item equals the value.
Okay, so if you dive into the bodo documentation, you'll see more about the syntax, but basically, all this is doing is saying, look up an item by it's key: user and todo, and then update any properties, if they exist. So, it's gonna update the item, or the completed, or both. So, now let's look at delete.
Delete's pretty simplistic. What's it's doing is saying, set the table, set the region, get a client, a user_id, get a todo_id. And it's trying to pull the id from a query string, and then it's just going to delete that item, so delete client, user_id, todo, table_name. And then it's just going to respond with a deleted of true, because if this fails, it's not going to get to this call here to respond.
So that's it for the API code. If you're used to using web application frameworks, this probably looks a bit sparse, however, that is intentional. Native serverless functions each do one thing as simply as possible. Since such things as routing an endpoint to a function happens at the API Gateway layer, you don't need a web framework to handle routing.
Since we're handling authentication with Cognito-- via Gateway authorizers--again, we don't need a framework to handle that in code. And since we're not doing anything all that complicated with the database, we don't need to deal with an ORM. Alright, let's wrap up here and summarize the key takeaways.
The front-end is a modified version of the todoMVC for Vue.js. The back-end code consists of one code file per use case. In this app, there's one for get, post, hook, and delete. Okay, in our next lesson, we're going to go through the unit test, so that you're familiar with them when we run them later.
So, if you're ready to check out some tests, then I'll see you in the next lesson.
Ben Lambert is a software engineer and was previously the lead author for DevOps and Microsoft Azure training content at Cloud Academy. His courses and learning paths covered Cloud Ecosystem technologies such as DC/OS, configuration management tools, and containers. As a software engineer, Ben’s experience includes building highly available web and mobile apps. When he’s not building software, he’s hiking, camping, or creating video games.