Serverless Rust API on AWS - Part 1: Introduction

Building a serverless API on AWS with OpenAPI support using Rust and Poem.

Blog post author avatar.
David Steiner

· 7 min read

Serverless API development on AWS offers various approaches, from deployment tools and local development experiences to databases and web frameworks. Rust, with its performance and safety features, is gaining traction in this space.

This article demonstrates implementing a simple serverless API deployed on AWS Lambda using Rust. We’ll explore my preferred tools and libraries for building Lambda-based APIs. The article is split into two parts: the first covers general considerations, while the second provides an implementation walkthrough.

This guide is aimed at developers with basic Rust knowledge who want to explore serverless API development on AWS.

Fat and thin Lambdas

When building Lambda-based APIs, two competing paradigms exist: fat and thin Lambdas. Fat Lambdas use a single function to serve multiple endpoints, handling routing internally. Thin Lambdas dedicate a function to each endpoint (or small set of endpoints), with routing implemented at the API Gateway level.

Traditionally, the wisdom has been to create small Lambda functions with well-defined, single responsibilities, naturally leading to thin Lambdas. Beyond academic benefits, this approach can result in smaller Lambda functions, potentially reducing cold start times.

However, this comes at a steep cost. Managing numerous Lambdas quickly becomes a maintenance challenge. Replicating API Gateway routing logic locally is difficult, and developers lose the opportunity to leverage battle-tested, feature-rich traditional web frameworks.

I strongly prefer running an established framework inside a fat Lambda. Benefits include:

  • Ability to leverage the framework’s ecosystem, tools and middlewares.
  • Improved developer experience through local framework execution.
  • Decoupling from AWS and Lambda, facilitating easy migration to other platforms like AWS ECS if needed.

While fat Lambdas may have drawbacks such as slightly longer cold start times for less frequent requests, their benefits often outweigh these concerns for many applications. This is especially true for Rust due to its relatively small binary sizes and excellent cold start characteristics.

Infrastructure-as-Code (IaC)

Several frameworks are commonly used for infrastructure and local development. Tools like SST and Serverless Framework aim to serve as IaC tools and assist with local development. Similarly, AWS SAM provides utilities to ease local serverless development by allowing local Lambda function invocation and mimicking resources like API Gateways.

With our approach of running a traditional framework inside Lambda, these frameworks’ additional features become less appealing. Since we only need a tool to create infrastructure resources, a flexible and powerful solution like AWS CDK (Cloud Development Kit) is sufficient. AWS CDK allows you to define cloud infrastructure using familiar programming languages.

This approach becomes even more powerful with Rust through Cargo Lambda. Cargo Lambda, which integrates seamlessly with the Rust ecosystem, makes updating the Lambda function serving the API trivial and fast. This allows developers to not only test changes locally but also quickly deploy and test on real AWS infrastructure, with a feedback loop of just a few seconds.

Choice of API Gateway

Currently, AWS offers three main options to expose Lambdas over HTTP:

  1. API Gateway REST APIs: The oldest and most feature-complete (or bloated, depending on perspective) solution.
  2. API Gateway HTTP APIs: A more modern and cost-effective approach, lacking certain features like usage plans, request validation, and private API endpoints.
  3. Lambda function URLs: The latest addition, offering minimal functionality to expose Lambdas over the internet.

Given my preference to minimise API Gateway-level operations, I find most REST API features redundant - with the notable exception of private APIs. While function URLs can suffice for simple, I often find HTTP APIs to be the ideal balance. They offer necessary functionality without the complexity of REST APIs, making them suitable for a wide range of applications.

Deploying in a VPC

Lambdas can be deployed with or without Virtual Private Cloud (VPC) integration. A VPC is a logically isolated section of the AWS cloud where you can launch AWS resources in a virtual network you define. When deploying in a VPC, Lambdas are typically placed in private subnets, as public subnets offer few advantages.

Traditionally, APIs often resided in private subnets. However, this approach partially compromises the highly scalable philosophy of Lambda:

  • Cold starts noticeably increase in private VPC subnets, often exceeding an extra second.
  • Additional network resources may become bottlenecks, constraining Lambda function hyperscaling.
  • Infrastructure costs may rise due to resources like NAT gateways.

The decision to deploy an API in a VPC depends on specific requirements for scaling, security, and compliance. VPC deployment might be necessary for accessing private resources or meeting strict security policies. For this blog post, we’ll deploy the API without VPC integration to maximise scalability and minimise complexity.

Choice of web framework

While Rust doesn’t have mature heavyweight frameworks like Django or Ruby on Rails, it offers a healthy ecosystem of microframeworks - lightweight, flexible frameworks focused on core web functionality. Options include Actix Web, Rocket, axum, Salvo, warp, and Poem. A comprehensive review of these frameworks would warrant its own article.

My preferred options are axum and Poem, and I alternate between them based on project requirements.

axum, developed by the tokio team, is a safe choice in terms of future support, maintenance, and community growth. Its major advantage is being built on top of hyper, allowing it to leverage hyper’s ecosystem of middlewares and tools.

Poem, while less popular, excels in integrated OpenAPI support - crucial for modern API documentation and client generation. Although it doesn’t integrate with hyper, Poem has its own ecosystem of excellent crates covering typical needs.

In conclusion, I find that Poem has a slight edge in design and developer experience, but axum may be preferable when prioritising stability and future-proofing. Choosing Rust is already a niche decision, and axum can help minimise associated risks.

Both frameworks support AWS Lambda. axum runs on Lambda using the lambda_http crate, while Poem offers first-party support via poem-lambda. This blog post will use Poem, but much of the code would work similarly with axum.

Databases

When building serverless APIs, I tend to reach for one of three database solutions: Postgres, MongoDB, or DynamoDB.

Relational databases have excellent support in Rust through the sqlx crate. I find sqlx’s approach to database integration superior to typical Object-Relational Mapping (ORM) libraries and I consider it one of Rust’s unique strengths for web development. My belief is that most projects should consider Postgres with sqlx as the primary database by default.

If MongoDB is more your speed, rest assured that Rust has strong support for it. Rust has an official client library with serde support for serialisation, making it easy to work with document-based data. The serverless cluster offering on MongoDB Atlas may be an appealing choice with very little initial infrastructure costs.

DynamoDB is designed for serverless use-cases and offers scalability that can match Lambda’s hyperscaling without impacting latency. Rust has official support through the AWS SDK. While working with DynamoDB can be cumbersome, crates like serde-dynamo improve the developer experience.

In many cases, DynamoDB may not be ideal as a general-purpose database. However, we’ll use it in this article because it’s the easiest and most cost-effective option to provision for demonstration purposes using AWS CDK.

The application

Given these considerations, we’ll build an application with the following components:

  1. Poem web framework to expose a simple CRUD (Create, Read, Update, Delete) API for currencies.
  2. Poem’s built-in OpenAPI support to display the schema in Swagger UI.
  3. DynamoDB as a key-value store to persist currency data.
  4. HTTP API Gateway to facilitate HTTP communication.
  5. AWS Cognito authentication to secure our API and demonstrate integration with API Gateway.
  6. AWS CDK for provisioning infrastructure resources.

In the next part of the series, we’ll dive straight into the code!

David Steiner

I'm a software engineer and architect focusing on performant cloud-native distributed systems.

About me

Back to Blog