Help us to make this transcription better! If you find an error, please
submit a PR with your corrections.
Eoin: Hello and welcome to AWS Bites episode 139. I'm Eoin and I'm joined again by Luciano. Building a new API on AWS presents you with a lot of options. There's tons of frameworks out there for any language you can imagine. But what happens when you decide to implement some or all of that API with AWS Lambda? It can bring some benefits, but there are a few head-scratching considerations. Not all API frameworks are designed with AWS Lambda in mind. There is one actually, that is. And today we're going to revisit power tools for AWS Lambda and dive into the amazing REST API support it offers, specifically covering the Python version of the library. We've been using this framework a lot and really want to share how it makes API development much faster, while still giving you all the features you want, like routing, validation, open API support, middleware, and more. So let's get started. AWS Bites is brought to you by fourTheorem. If you want fast, modern APIs with great developer experience, fourTheorem is your partner. We'll collaborate with you to ensure you have great performance, security, scalability, and most importantly, satisfied API users. Reach out on LinkedIn, BlueSky, or through fourtheorem.com. All of our details are in the description. Luciano, you don't always have to use Lambda for APIs, of course. There's lots of options out there. Before we get into the Lambda story, if you're running on a server or a container, what frameworks would you consider?
Luciano: Yes. So I am a big fan of Node.js, as many people are probably aware. So in Node. js, the most famous web frameworks, probably Express, has been around since almost forever, since Node.js existed. Although I have to say in the recent years, since Fastify came out, I think it's a slightly modern take on Express and much more performant, I think, has a nicer developer experience. So these days, if I have to pick a more traditional web framework for Node.js, probably Fastify will be my first choice. And I've been intrigued by a new framework that came out, I think, last year, and it's called HONO. You might have heard of it because it's quite interesting in a way that it's very minimal, but at the same time, it's built with distribution in mind, I could say, because they made it work with effectively every major JavaScript runtime.
So it works in Node, it works in Bun, it works in Deno. And also, it can work in multiple environments when it comes to picking, for instance, a serverless, so to speak, environment, like it can work well in Lambda, it works well in Cloudflare workers. And I heard people trying it in all sorts of environments, and everyone says, yes, it just works out of the box. So could be an option, you can run it in Lambda if that's your thing. But of course, we'll talk about the differences between running a more traditional web framework in Lambda and using something like Powertools.
If we have to pick other languages, people might be aware that I also like Rust. And in Rust, there is still a little bit early, I would say, because Rust is such a newer language, and the ecosystem isn't as developed as NodeJS or Python. But there are quite a few web frameworks that are quite good. And one that I've been using and I like is called Axum. And you can also use that one in Lambda effectively by embedding the entire web framework into the monolithic Lambda approach, basically. And then when it comes to Python, the most famous ones are probably Flask and Django. But again, there is a more modern take, which is called FastAPI. Again, the name Fastify, FastAPI. Maybe there is some curious overlap there. And yeah, FastAPI is really good. I've been using it in the past, and it's really nice to use. So yeah, the idea again is that you can take any of this framework we just mentioned and package everything in a Lambda lit. And that's something that people sometimes do. I don't necessarily like this approach. I generally prefer to have fine-grained Lambdas. But yeah, I guess you need to figure out exactly what are you trying to optimize for. Like if you already have a web server and it's relatively small, you just want to move it to Lambda because of scalability, because it scales to zero, then you can do it and it should work reasonably well. What do you think, Eoin? Yeah, I'd agree with all of that.
Eoin: I haven't got to try Hono yet, but it seems really, really good. But I do like, you know, something that it depends on what kind of project you're building, but something that gets you going really quickly. And, you know, Python is generally a fast language to develop with, very productive. Everybody can get involved. And I really like the FastAPI approach as well. And the approach we're talking about today with Lambda is actually modeled on FastAPI. So it's very similar. And this is all about PowerTools. And we did mention PowerTools in the past, AWS Lambda PowerTools, mostly in the context of metrics and logging and tracing, because those were the three pillars that kicked off the whole PowerTools adventure. I think the first language was supported by, the first language supported by PowerTools was Python. Back when ATR-LESA kicked off the project, I know there's a whole team behind it now. And the amount of development that has been done on it for an AWS open source project is pretty astounding. And the quality and level of documentation is one of the best I've seen for any open source project. The Python one is by far the most fully featured version of PowerTools, because it was the first. In addition to the metrics, logging and tracing, it supports middleware, a bit like MIDI does with Node.js functions. And then you've got all sorts of other features, like types for Lambda events, parameter retrieval, feature flags, streaming responses, item potency support, and then you have validation and parsing. And there's a whole web framework essentially built into PowerTools for building REST APIs. So validation, parsing, REST APIs support kind of work together to give you this really nice API framework with a good developer experience. Should we talk actually, what does an API framework need to have? What would you like to see from it?
Luciano: Yeah, exactly. I think there are some components that every web framework needs to have to give you effectively the basic tools to build an API. And the first one is, of course, routing. Like you need to be able to understand what kind of HTTP request is coming in, what is the method, the path, maybe you have some path parameters. The routing layer should be able to effectively address specific parts of your code and respond to specific requests. And ideally do all the parsing of path parameters and give you nice ways to access all this information. And with that also comes error handling, like what happens if a route doesn't exist? The framework should take care of doing common responses like a 404 for you.
Other concerns could be serialization and diserialization. You will have requests coming in. You'll need to be able to process these requests. So most of the time it's probably going to be JSON, but JSON is not the only format. You might have like a form that is being submitted and you want to handle it as part of your API. You might have uploads of files. So there can be different kinds of encodings effectively that your API needs to support. A good framework should be able to give you the tools to process all the different kinds of data and turn them into something that you can actually programmatically use. And similar with responses, you might need to serialize an object into a specific response, serializing probably JSON again, but that's not necessarily the only option you might want to support. So again, the framework should give you all the tools for serialization and diserialization. Validation is a very related topic because when you are accepting data, it's always a good practice to do validation. So hopefully that needs to be part of the tools that the framework give you. And one thing that I really like a lot to see in frameworks, and this is actually something that is not always present. Generally, you need to rely on some kind of third-party plugin. That's the OpenAPI specification. So I think I like when a framework allows you to use types and strongly typed code in general in your code, and then it's able to build an OpenAPI specification, starting from your routing definition and from the types that you used in your endpoints. And I guess if you put all of that together, you can get to a point where you have nice types, autocompletion and type safety for all the requests and responses. I think that will be kind of the golden standard for me when a framework gives you all of that. I think you end up with a very nice developer experience and you can effectively develop your API with the confidence that you are managing the data properly. You're not going to have surprises where maybe you're trying to access a field that doesn't exist, or maybe you're going to return a response that doesn't necessarily match what you promised the user you would return. So I think that's kind of my ideal framework. How does PowerTool help in this sense? Does it match this definition or is it verified? Yeah, I think it hits everything you've mentioned.
Eoin: Let's go into it in a bit of detail. So when you have Lambda behind an API, you're generally talking about API Gateway, REST API, or HTTP API, but it could also be an application load balancer, a function URL, or even a VPC lattice. And PowerTools supports all of those things. So there's a great page in the PowerTools documentation all about REST APIs, which we can link in the show notes. It's very comprehensive.
The REST API support essentially first provides a set of resolvers, and there's a resolver for API Gateway, HTTP API, application load balancer, et cetera, and they all work in a similar way. So you basically create one of these resolvers. And when you do that, you're creating a router, router that can be used to route a Lambda HTTP event to a set of functions that you define for your routes. And once you have this resolver, the Lambda handler function becomes very small. It's actually one line generally. So you have your typical Lambda handler with event in context, and you just forward that on to your resolver's resolve method. And then you can just create specific functions for each route. Let's say you have an API to do CRUD operations on a to-do list items. You have your create to-do function, and then you'll decorate it with at app.post with the path parameter to-dos. And there's lots of other options you can put into that decorator when it comes to providing additional details that you might want in your open API specifications. And that's it, really. That's how you get up and running. That's how you create your first set of APIs. And Powertools is just doing a lot of the work for you. You don't have to worry about parsing JSON or looking at the event yourself. A lot of the rest of it is just managed.
Luciano: Yeah, I think I want to spend a little bit more talking about validation and type checking and all the options you can generally have there, and then see exactly how Powertools helps there in the case of Python. But yes, we just say that validation is effectively one of the main things that an API should do first. Like, whenever you receive a request, you need to validate. And that's probably going to be one of the first lines of code that you will write in your own handler, right, if you have to write all of that manually. And this is a good practice for a few reasons. One is definitely a security best practice, because of course, if you are not going to, if you're going to validate the incoming data, then you have a little bit more certainty that you understand the shape of the data coming in. And that reduces the risk of all kinds of injections. Also, it reduces the risk of you ending up creating maybe inconsistent data in a backend system.
And maybe, I don't know, storing it in a database record or something, which will eventually lead to subtle bugs here and there, because you have all these different objects stored with slightly different, I don't know, shape. So that's definitely one of the benefits of validation. Then the other thing is that if you have a very strict definition of what your input should look like, then you could also create strongly typed interfaces to represent that input, which in the language of choice, let's say Python in this case, that will give you nice auto-completion and type checking. So once you are at that point in your code when you are using a specific type and you have validated that the input matches the type, then everything else should get so much nicer because you can easily see all the fields available, autocomplete all the types and so on. So when you combine the two, effectively that reduces the risk of bugs because you're checking that the data makes sense. And then at that point, you have a strongly typed interface. And that's generally a problem that exists with languages that are dynamic like Python and JavaScript, where if you don't have the diligence of doing all this, defining the interfaces and doing a proper validation, that's generally a very common source of bugs. So that's why I think I wanted to stress a little bit more how important is this point. And then when it comes to Powertools, they put a lot of effort into trying to give you good tools to do all of these things in a nice way and without having to write a lot of code yourself. And in Powertools, they specifically leverage a library called Padantic.
And I think it's important to explain a little bit more how Padantic works and what is the difference between validations and parsing. And that's another thing that if you just look at the list of features that Powertools has, you can see that there is a section for validation and there is a section for parsing. So it might be a little bit confusing to people, like, what is the difference between the two? And the way I will describe it, and maybe this is not necessarily a canonical definition, is that validation, you effectively are just verifying that the data you are receiving matches a specific set of rules that you are defining. And the result of that validation can be either true, like everything matches, so it's the data's good. Or if it doesn't match, you might get like a list of errors that try to describe you, I don't know, which field didn't match specific rules. And you can use that maybe to provide a response to the caller.
Parsing is a little bit more than that. It kind of solves the same problem in a way, but the approach is a little bit different. So with parsing, you are generally starting from a strongly typed model. And then you are effectively trying to read the input data and start to populate this model. And then if everything goes well, so if you are effectively able to populate the model entirely, you're respecting the types and the constraints that you define in that model, you can also say that the incoming data is valid. But the output of that operation is that it's not just a Boolean that tells you true, but you now have this object that you can use, and it's strongly typed, and you can use it in a much more structured way, as opposed to just a Boolean, and then you still need to read the raw data. And libraries like PyDantic will give you lots of tools to do also more advanced things like coercion, more advanced validation rules. So you are effectively, you can also normalize the data as you're doing all this validation and parsing. So in a way, we could say that parsing is a more powerful way of doing validation, and it gives you a lot more, like coercion, auto-completion type checking. So generally, it's what I would prefer these days. I wouldn't do just validation anymore, because I think it only solves a portion of the problem. Parsing is much better. So how does Powertools help there? Can you give us a little bit more? I think you've covered it pretty well.
Eoin: I mean, if you've used Fast API, or even similar JavaScript type frameworks using Zod, then you'll be familiar with the idea. So if you like to write typings in your Python code, then it'll definitely be very easy for you, because you just define your types as PyDantic models. That's just like creating a data class in Python, but you get all of the additional descriptive nature of PyDantic models and custom validation if you want. But it can be very simple.
You just define your request and response types as PyDantic models, and then your type declarations for the functions that manage your routes use those types. You can also do type annotations for things like query parameters, so you can strongly validate those too, and get auto-completion with them. And even headers as well can have types. Of course, that gives you, as you mentioned, auto-completion in your IDE. You get the validation and the parsing all for free. You don't have to do any serialization or deserialization from JSON or whatever you're using. And that's not just for APIs, actually.
These parser features can be used with any event type. So when we're working with EventBridge events, we also often define PyDantic models for the EventBridge events and use those parsers there too. And one of the huge benefits then is when it comes to OpenAPI specifications, these same models are used for generating all of the JSON schema and all of the documentation for your OpenAPI or Swagger docs.
And PowerDools has great support for that. It can even provide a route like slash OpenAPI, which will serve the JSON or HTML documentation for the OpenAPI spec. And it's all generated from the PyDantic models. So it becomes very easy to iterate on it. We also, generally when we're doing projects like this, we'll generate the OpenAPI spec at build time, like in the build pipeline or in a pre-commit hook even. And then if you've got front-end code or client SDKs that you want generated, you can just take that immediately and generate or update your client SDKs. And then you have TypeSafe JavaScript or whatever other language you need. So if you're doing a web application, you've already got a library that has very strongly typed, very developer-friendly bindings for the API. Yeah.
Luciano: So I guess the next question is, do you need to use the LambdaLith approach to leverage all these nice features that PowerTools for Python gives you? Yeah, that's a good question.
Eoin: And maybe we should define exactly what we mean by LambdaLith, because it's a bit of a contentious topic, I think, in the world of AWS Lambda right now. Typically, I think when Lambda first came out, and for many years, one of the benefits that was spoken about was the fact that you've got very specific single-purpose functions with very fine-grained permissions and very specific dependencies that were lightweight. And then you could tune your memory and CPU and everything very specifically to each individual function.
And then the approach with API Gateway was you would have one root in your API, which went to one Lambda function, which had the resources and only the resources and only the permissions it needed. Now, people can find that a lot to maintain, although it doesn't have to be. But the other approach is, forget all that, let's just bundle it all into one function and have all our API routes be backed by one function. Now, I can see the appeal for sure, because it simplifies your deployment.
It means that if you've got warm containers that served one route, they can also be used warm to serve other routes. You do lose some benefits because you have to have a broader set of permissions, and sometimes you might need a larger set of dependencies to bundle into your function. So there are pros and cons to both approaches. We don't necessarily have to go into what's good and bad. Now, the Powertools documentation does more or less advocate for LambdaLith functions. And it even says things like the OpenAPI specification only really works if you're using a LambdaLith. So everything, all routes in one function, but that's not necessarily true. We were able to work around that.
So you can basically set up your API resolver with Powertools, and then you just decide which functions to attach it to. So the way we do it is we have basically a separate module outside of all of your Lambda handlers where your resolver is created. And then you can just import that into the Lambda handlers that you want to be part of that resolver and share the same routing mechanism. And then when it comes to generating the OpenAPI spec, for example, so that one of the issues is that if you've got single purpose functions, each function had its own resolver. So they didn't, they weren't aware of the whole API, so they couldn't generate a full spec. But if you have a common resolver declared and you just import that in each event handler, you can just have a script locally that will basically load all of your handlers. They'll all share the same resolver at just a local time or build time. Then you generate the OpenAPI spec in a script. And then when you deploy it, you can decide whether to package each handler into separate functions or all into one function. And then, so we've done multiple projects where we do this and you basically, you get, we've done it where you do the Lambda-lit approach, which actually can work very well at the start of our project if you want to iterate really quickly, because you only had one function to worry about. But we've also done it with single purpose functions and you just have this shared resolver and then you have multiple deployments. And then you have the whole build pipeline, which is generating the OpenAPI documentation and the client bindings. And it works with both approaches. So it doesn't have to be a Lambda-lit.
Luciano: Yeah. I think if people are curious about our rationale when it comes to Lambda-lits, we have a dedicated episode, I think it's 92, where we talk about some approaches on how you can decompose an existing Lambda-lit into multiple functions. And we also talk about the benefits of doing that. Now, again, I don't want to necessarily advocate for single purpose Lambda functions, although it's generally my preference. But yeah, I know that there are some good use cases also for Lambda-lit. But that will be a little bit out of topic. So I'll leave you to the other episode in the show notes if you're curious. I guess the other question I would have, and you have a little bit more experience than me with Powertools of Python, what's the local development experience?
Eoin: There's nothing specifically in Powertools that gives you a local development experience out of the box, but it actually becomes really easy once you have this API resolver setup. It's just really well architected. And it becomes a very thin layer on top of your business logic, the API layer. It becomes really concise and very portable. So the way we would just do it normally is you have your handlers and these routes, and then all of your logic, like your services and your repository patterns and everything sit separately. You could actually easily migrate your whole API to a completely different framework if you wanted to. But when it comes to local development, obviously if it's fast API, you can just run it locally. You can do something very similar locally with Powertools. And when we are doing this approach, we generally just set up a local server script, which is like a simple Python Flask server, which has one root in it and matches every request, just as a very simple translation of the Flask request into a Lambda event, HTTP API or ALB event, and then invokes the Lambda handler's code directly. And then the Powertools API resolver will take the request from there, just as it does in a real Lambda environment. And it's just generally a few lines of code to do that. And then you've got a local simulation server that behaves like API Gateway and Lambda. And it can really speed up development because you don't have to deploy every time. I know even that some people are using Lambda Powertools in containers, like with Fargate. Because it has so many nice tools there, you can actually take a similar approach and you can just run a server like that, very lightweight proxy and forward onto AWS Lambda for Powertools, make use of the resolver. And then you get all the other nice features like tracing, metrics, logging, item potency, whatever. So it doesn't have to be with Lambda to get benefit for all this great work. Yeah, that's pretty cool.
Luciano: I guess before we wrap up, maybe worth mentioning was the status of similar features with the other versions of Powertools or the other languages. And you might be aware that Powertools is not just a Python project, exists for TypeScript, .NET, and Java. Unfortunately, Python is always like the main line. It's probably the first one where they introduced new features and this API framework development has been one of the latest additions. So unfortunately, all the other languages still have to catch up with this feature. So there is some support, for instance, in the TypeScript one, you have ZOD available if you want to do parsing, but then you don't really have a nice cohesive ecosystem of features that gives you like an entire API development type of approach as you would get with the Python version. But hopefully that will change soon. So keep an eye on the various versions. Maybe there could be an opportunity to contribute as well. Always remember that Powertools is an open source project so everyone is able to contribute if they want to. And of course, you can always build your own abstraction as well. Like if you're familiar with other frameworks in the language of choice, you can probably bring some components of this framework and use that as a way to create your own mini API framework on top of Powertools.
Eoin: I mean, I think when it comes to the more mature languages that have been doing web development for 20 plus years, like the Java and Python runtimes, sorry, the Java and .NET runtimes, there's so many libraries out there for doing routing and frameworks that have a long history. With .NET, you can integrate ASP.NET for that. I think even the CDK patterns, if I remember correctly, or maybe it's in the .NET templates, they give you patterns for doing that out of the box. So perhaps there's just not as much of a need as there is in Python. I guess in conclusion then, if you are a Python developer keen on Lambda looking for a framework that is a bit like FastAPI or one of the other ones that you may have used, this is a great option. We feel at least, but let us know what you think. And if you've got any nice alternatives or pro tips for Lambda backed APIs. Thanks very much for listening and watching, and we'll see you in the next episode. I'll see you in the next video.