OpenAPI Lambda for Rust takes an OpenAPI definition and generates Rust boilerplate code for running the API "serverlessly" on AWS Lambda behind an Amazon API Gateway REST API. The generated code automatically routes requests, parses parameters, marshals responses, invokes middleware to authenticate requests, and handles related errors. This project's goal is to enable developers to focus on business logic, not boilerplate.
This project is not affiliated with the OpenAPI Initiative or Amazon Web Services (AWS).
Add openapi-lambda as a dependency and openapi-lambda-codegen as a build dependency to your
crate's Cargo.toml:
[dependencies]
openapi-lambda = "0.1"
[build-dependencies]
openapi-lambda-codegen = "0.1"Both crates must have identical version numbers in Cargo.lock.
Add a build.rs Rust build script to your crate's root directory (see comments below):
use openapi_lambda_codegen::{ApiLambda, CodeGenerator, LambdaArn};
fn main() {
CodeGenerator::new(
// Path to OpenAPI definition (relative to build.rs).
"openapi.yaml",
// Output path to a directory for generating artifacts. This directory should be added to
// `.gitignore`.
".openapi-lambda",
)
// Define one or more Lambda functions for implementing the API. A single "mono-Lambda" may
// be used to handle all API endpoints, or endpoints may be grouped into multiple Lambda
// functions using filters (see docs). Note that Lambda cold start time is roughly
// proportional to the size of each Lambda binary, so consider splitting APIs into smaller
// Lambda functions to reduce cold start times.
.add_api_lambda(ApiLambda::new(
// Name of the generated Rust module that will contain the API types.
"backend",
// AWS CloudFormation logical ID or Amazon Resource Name (ARN) that the Lambda function
// will have when deployed to AWS. This value is used for adding
// `x-amazon-apigateway-integration` extensions to the OpenAPI definition, which tells
// API Gateway which Lambda function to use for handling each API request. If using
// CloudFormation/SAM with a logical ID, the ARN will be populated automatically during
// deployment.
LambdaArn::cloud_formation("BackendApiFunction.Alias")
))
.generate();
}Include the generated code in your crate's src/lib.rs:
include!(concat!(env!("OUT_DIR"), "/out.rs"));The generated file out.rs defines a module named models containing Rust types for the input
parameters and request/response bodies defined in the OpenAPI definition. It also defines one
module for each call to add_api_lambda(), which defines an Api trait with one
method for each operation (path + HTTP method) defined in the OpenAPI definition.
It is often helpful to refer to rustdoc documentation to understand the generated models and API types. To generate documentation, run:
cargo doc --openTo implement the API, implement the generated Api trait(s). To help you get started,
the code generator creates files named <MODULE_NAME>_handler.rs
in the configured output directory (e.g., .openapi-lambda/backend_handler.rs) with a placeholder
implementation of each Api trait. Copy these files into src/, define corresponding modules in
src/lib.rs (e.g., mod backend_handler),
and replace each todo!() to implement the API.
Each Api trait declares two associated types that you must define in your implementation:
AuthOk: the outcome of successful request authentication returned by your middleware (see below). This might represent a user, authentication session, or other abstraction relevant to your API. If none of the API endpoints require authentication, simply use the unit type (()).HandlerError: the error type returned by each API handler method. A typical API will define anenumtype for errors and have theApi::respond_to_handler_error()method return appropriate HTTP responses depending on the nature of the error (e.g., status code 403 for access denied errors).
The openapi_lambda::Middleware trait defines the interface for authenticating requests and
optionally wrapping each API handler to add functionality such as logging and telemetry.
A convenience
UnauthenticatedMiddleware implementation is provided for APIs with no endpoints
that require authentication.
The Middleware::AuthOk associated type represents the outcome of a successful call to the
Middleware::authenticate() trait method. This is a type you define that might represent a user,
authentication session, or
other abstraction relevant to your API. If none of the API endpoints require authentication, simply
use the unit type (()). The Middleware::AuthOk associated type must match the Api::AuthOk
associated type in your Api trait implementation(s).
The Middleware::authenticate() method provides a headers argument with access to all request
headers, allowing you to authenticate requests using headers such as
Authorization or
Cookie.
It also provides a lambda_context argument with access to Amazon Cognito identity information
if using an API Gateway
Cognito user pool authorizer.
If the request fails
to authenticate, be sure to return an HttpResponse with the appropriate HTTP status code
(i.e., 401).
Define a binary target for each Lambda function (e.g., bin/bootstrap_backend.rs) to bootstrap the
Lambda runtime. The openapi_lambda::run_lambda() function is the recommended entry point to start
the Lambda runtime and begin handling API requests:
// Replace `my_api` with the name of your crate and `backend` with the name of the module
// passed to `ApiLambda::new()`.
use my_api::backend::Api;
use my_api::backend_handler::BackendApiHandler;
use openapi_lambda::run_lambda;
#[tokio::main]
pub async fn main() {
let api = BackendApiHandler::new(...);
let middleware = ...; // Instantiate your middleware here.
run_lambda(|event| api.dispatch_request(event, &middleware)).await
}The easiest way to compile Lambda functions written in Rust is with Cargo Lambda, which handles any necessary cross-compilation from your development environment to AWS Lambda (either x86-64 or ARM-based).
In addition to installing Cargo Lambda, be sure to install the relevant target
(x86_64-unknown-linux-gnu or aarch64-unknown-linux-gnu depending on the targeted Lambda
function architecture) for your Rust toolchain (e.g., via
rustup target add).
After installing Cargo
Lambda, run the following command to build Lambda bootstrap binaries in the target/lambda/
directory:
cargo lambda build --releaseIf targeting ARM-based Lambda functions, be sure to add the --arm64 flag.
An alternative to Cargo Lambda is musl-cross,
which provides better backtrace support when
compiling on certain environments such as macOS with Apple Silicon. A
Homebrew package is available for easy
installation on macOS.
In addition to installing musl-cross, be sure to install the relevant target
(x86_64-unknown-linux-musl or aarch64-unknown-linux-musl depending on the targeted Lambda
function architecture) for your Rust toolchain (e.g., via
rustup target add).
To compile binaries for x86-64 Lambda functions, run:
CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER=x86_64-linux-musl-gcc \
cargo build --target x86_64-unknown-linux-musl --releaseTo compile binaries for ARM Lambda functions, run:
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=aarch64-linux-musl-gcc \
cargo build --target aarch64-unknown-linux-musl --releaseThe final binaries are written to the target/x86_64-unknown-linux-musl/release/ or
target/aarch64-unknown-linux-musl/release/ directory, depending on the target architecture.
Deploying to AWS involves creating one or more Lambda functions and an API Gateway REST API.
Lambda functions written in Rust should use one of the provided
Lambda runtimes.
The provided runtimes require each Lambda function to include a binary named bootstrap, which
is produced by the compilation step above.
An API Gateway REST API uses an OpenAPI definition annotated with
x-amazon-apigateway-integration
extensions that determine which Lambda function is used for
handling each API endpoint. The openapi-lambda-codegen crate writes an annotated
OpenAPI definition suitable for this purpose to a file named openapi-apigw.yaml in the output
directory specified in build.rs (e.g., .openapi-lambda/openapi-apigw.yaml). This OpenAPI
definition is modified from the input to help adhere to the
subset of OpenAPI features
supported by Amazon API Gateway. In particular, all references are merged into a single file, and
discriminator properties are removed.
As a best practice, consider using an infrastructure-as-code (IaC) solution such as AWS CloudFormation, AWS Serverless Application Model (SAM), or Terraform.
The
Petstore example
provides a working AWS SAM template (template.yaml) and accompanying Makefile.
AWS SAM provides both a streamlined version of CloudFormation tailored to serverless use cases and a command-line interface (CLI) for deploying to AWS and locally testing APIs.
When defining a SAM CloudFormation stack template, define an
AWS::Serverless::Function
resource for each Lambda function. Be sure to specify the same logical ID (i.e., YAML key) in your
build.rs Rust build script using the LambdaArn::cloud_formation() function. If
specifying an
AutoPublishAlias
property (recommended), append the .Alias suffix to the logical ID passed to
LambdaArn::cloud_formation(). This ensures that API Gateway always executes the version of your
function associated with the specified alias. Aliases help support quick rollbacks in production
by simply updating the alias to point to a previous version of the Lambda function, without waiting
for a full stack deploy.
Each AWS::Serverless::Function resource should specify
BuildMethod: makefile in the Metadata attribute (see
Building custom runtimes). The resource should also specify a CodeUri attribute that points
to a directory containing your crate. A Makefile must exist in the specified directory. The
Makefile must define a target named build-LOGICAL_ID, where LOGICAL_ID is the logical ID (YAML
key) of the resource in the SAM template. The build-LOGICAL_ID target must copy a binary named
bootstrap to the directory referenced by the ARTIFACTS_DIR environment variable (set at build
time by the AWS SAM CLI). See the
Petstore example
for details.
The SAM template must also include an
AWS::Serverless::Api
resource that defines the API Gateway REST API. Use the
AWS::Include
transform along with the annotated OpenAPI definition openapi-apigw.yaml, which automatically
resolves the logical IDs of each Lambda function to the corresponding
Amazon Resource Name (ARN)
during deployment:
Resources:
MyApi:
Type: AWS::Serverless::Api
Properties:
Name: my-api
StageName: prod
DefinitionBody:
Fn::Transform:
Name: AWS::Include
Parameters:
Location: .openapi-lambda/openapi-apigw.yamlBefore testing or deploying an AWS SAM template, build it by running:
sam buildTo start the API locally for testing, run:
sam local start-apiTo deploy the template to AWS, run:
sam deployThe Petstore example illustrates how to use this crate together with AWS SAM to build, test, and deploy an API to AWS Lambda behind an Amazon API Gateway REST API.
The minimum supported Rust version (MSRV) of this crate is 1.70.
This crate maintains a policy of supporting Rust releases going back at least 6 months. Changes that break compatibility with Rust releases older than 6 months will not be considered SemVer breaking changes and will not result in a new major version number for this crate. MSRV changes will coincide with minor version updates and will not happen in patch releases.
The generated code uses the log crate to log requests. Consider
using the log4rs or
env_logger crates to enable logging in each Lambda
function's main() entry point.
Enabling TRACE level logs will log the raw contents of each request and response. This can be
useful for debugging, but TRACE logs should never be enabled in production. In addition to
being verbose (incurring
Amazon CloudWatch Logs
charges), enabling TRACE logs in production could log sensitive secrets such as passwords and API
keys.
The code generator supports a large portion of the
OpenAPI 3.0 specification,
but gaps remain. If you encounter an unimplemented! error when generating code, please
submit a GitHub issue or open a
pull request (see
CONTRIBUTING.md).
References ($ref) found in OpenAPI definitions are supported, including references to objects in
other files. However, references that resolve to other references are currently not supported.
Every endpoint must have an operationId property, which must be unique across all endpoints. The
operationId property is used for routing requests and naming the handler method and related types
in the generated code.
By default, all API endpoints are assumed to require authentication. This means that
Middleware::authenticate() is invoked, and the AuthOk result is passed to the handler
method.
To denote an endpoint as unauthenticated, add an empty object ({}) to the
security
property for the endpoint. For example:
security:
- {}Unauthenticated endpoints will have their handlers invoked without calling
Middleware::authenticate(), and the handler method will not receive an AuthOk parameter.
Note that "unauthenticated" in this context simply means that the middleware will not be used to
authenticate requests. The handler method you implement may still perform its own authentication.
This is often useful for login endpoints (for which no authentication session exists yet), or for
webhook endpoints that require access to the raw request body in order to authenticate the request
(e.g., using an HMAC). In the latter case, a request body schema with type: string (optionally
with format: binary) should be used. The handler method can deserialize the body after verifying
the HMAC.
Request parameters must define a single schema property. The content property is currently not
supported.
Cookie parameters (in: cookie) are currently not supported. Header parameters (in: header) must
be plain string schemas.
Where supported, non-string parameter types must implement the FromStr trait for parsing. Object
types are not supported in request parameters.
Request and response bodies that define more than one media type are currently not supported.
The code generator represents request and response bodies as Rust types according to the following table. GitHub issues and pull requests that add support for other widely-used data formats are encouraged.
| Media type | Schema type |
Rust type | (De)serialization |
|---|---|---|---|
application/json |
string |
Vec<u8> for format: binary or String (UTF-8) otherwise |
None |
application/json |
Non-string |
See below | serde_json |
application/octet-stream |
Any | Vec<u8> |
None |
text/* |
Any | String (UTF-8) |
None |
| Others (fallback) | Any | Vec<u8> |
None |
String schemas that specify at least one enum variant will result in a named Rust enum
being generated. Please note that null variants are currently not supported.
Non-enum string types are determined by the format property, as indicated in the table
below:
format |
Rust type |
|---|---|
| Unspecified (default) | String |
date |
chrono::NaiveDate |
date-time |
chrono::DateTime<Utc> |
byte |
String (without base64 decoding) |
password |
String |
binary |
Vec<u8> |
| Other | Treated as a verbatim Rust type |
Integer enums are currently not supported. Non-enum integer types are determined by the format
property, as indicated in the table below:
format |
Rust type |
|---|---|
| Unspecified (default) | i64 |
int32 |
i32 |
int64 |
i64 |
| Other | Treated as a verbatim Rust type |
Number enums are currently not supported. Non-enum number types are determined by the format
property, as indicated in the table below:
format |
Rust type |
|---|---|
| Unspecified (default) | f64 |
float |
f32 |
double |
f64 |
| Other | Treated as a verbatim Rust type |
Boolean enums are currently not supported. Booleans are always represented as bool.
The table below specifies the generated Rust types depending on an object schema's
properties and additionalProperties fields. Please note that properties entries with schemas
that are objects or enums must use references ($ref) to named schemas. Other property types
may use inline schemas or references.
properties |
additionalProperties |
Rust type |
|---|---|---|
| At least one | false or unspecified |
Named struct |
| At least one | true |
Named struct + HashMap<String, serde_json::Value> with #[serde(flatten)] |
| At least one | Schema | Named struct + HashMap<String, _> with #[serde(flatten)] |
| None | false or unspecified |
openapi_lambda::models::EmptyModel |
| None | true |
HashMap<String, serde_json::Value> |
| None | Schema | HashMap<String, _> |
Array schemas with uniqueItems: true are represented as
indexmap::IndexSet<_>. All
other arrays are represented as Vec<_>.
A named Rust enum is generated for schemas utilizing oneOf, with one variant for each
entry contained in the oneOf array. If a discriminator is
specified, a Serde internally-tagged
enum is generated, with that field as the tag. Otherwise, a Serde
untagged enum is generated.
Please note that each oneOf variant must be a named reference ($ref), which determines the name
of the Rust enum variant. Each referenced schema must be either an object schema (type: object)
or utilize allOf. Inline variant schemas are not supported.
Schemas utilizing allOf are treated as objects (see above) after merging all of the component
schemas into a single schema of type: object. Each component of an allOf schema must be an
object or a nested allOf schema. At most one component may define additionalProperties.
Schemas utilizing anyOf or not are currently not supported.
Responses must specify individual HTTP status codes. Status code ranges are currently not supported.
This project is sponsored by Unflakable.