Writing Your First Lambda Handler
Time to actually write some Rust code that runs on Lambda.
What you'll learn
- How Lambda receives and responds to API Gateway requests
- The difference between lambda_runtime and lambda_http
- Writing a basic handler function
Enough talk. Let's write some code. Everything so far was just infrastructure setup. Now we get to the part where Rust actually runs.
If you look at the hello.rs we set up in the previous lessons, it's using the basic lambda_runtime boilerplate. That's fine for generic events, but we're building a web API. We need to handle HTTP.
Default boilerplate
Here's what sam init generated for us:
use lambda_runtime::{run, service_fn, Error, LambdaEvent};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct Request {}
#[derive(Serialize)]
struct Response {
statusCode: i32,
body: String,
}
async fn function_handler(event: LambdaEvent<Request>) -> Result<Response, Error> {
let resp = Response {
statusCode: 200,
body: "Hello World!".to_string(),
};
Ok(resp)
}
#[tokio::main]
async fn main() -> Result<(), Error> {
tracing_subscriber::fmt()
.with_max_level(tracing::Level::INFO)
.with_target(false)
.without_time()
.init();
run(service_fn(function_handler)).await
}This is fine as a starting point, but it has a problem. It's using a custom Request and Response struct that doesn't know anything about HTTP. When API Gateway sends a request to your Lambda, it sends a very specific JSON structure with headers, path parameters, query strings, the HTTP method, and more. Our custom Request struct is completely ignoring all of that.
Switching to API Gateway events
We need to use the ApiGatewayProxyRequest and ApiGatewayProxyResponse types from the aws_lambda_events crate. These are the types that match exactly what API Gateway sends and expects back.
First, update your Cargo.toml dependencies:
[dependencies]
lambda_runtime = "0.14"
aws_lambda_events = "0.18"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1", features = ["macros"] }
tracing = { version = "0.1", features = ["log"] }
tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt"] }Info
We removed lambda_http because we're working directly with the API Gateway event types. The lambda_runtime + aws_lambda_events combo gives us more control over how we handle requests.
Notice we upgraded
lambda_runtimefrom0.6to0.14. The API is the same, but you get better performance and bug fixes.
Handler
Now replace the contents of src/bin/hello.rs with this:
use aws_lambda_events::{
apigw::{ApiGatewayProxyRequest, ApiGatewayProxyResponse},
encodings::Body,
http::HeaderMap,
};
use lambda_runtime::{run, service_fn, Error, LambdaEvent};
async fn handler(_event: LambdaEvent<ApiGatewayProxyRequest>) -> Result<ApiGatewayProxyResponse, Error> {
let body = serde_json::json!({
"statusCode": 200,
"message": "Hello World!",
"data": null
});
let mut headers = HeaderMap::new();
headers.insert("Content-Type", "application/json".parse().unwrap());
let mut response = ApiGatewayProxyResponse::default();
response.status_code = 200;
response.headers = headers;
response.body = Some(Body::Text(serde_json::to_string(&body)?));
Ok(response)
}
#[tokio::main]
async fn main() -> Result<(), Error> {
tracing_subscriber::fmt()
.with_max_level(tracing::Level::INFO)
.with_target(false)
.without_time()
.init();
run(service_fn(handler)).await
}Let's break down what changed:
LambdaEvent<ApiGatewayProxyRequest>- This is now the real event type that API Gateway sends. It has everything: headers, path params, query strings, the body, the HTTP method, all of it.ApiGatewayProxyResponse- This is what API Gateway expects back. It needs astatus_code,headers, and abody.Body::Text(...)- The response body is a JSON string wrapped in theBodyenum.HeaderMap- Standard HTTP headers. We setContent-Typetoapplication/json.
The _event parameter has an underscore prefix because we're not using it yet. Don't worry, we'll be digging into it soon to read path parameters and query strings.
How it works under the hood
When someone hits GET /hello on your API:
- API Gateway receives the HTTP request
- API Gateway converts it into a JSON event and sends it to your Lambda
- Your Lambda deserializes it into
ApiGatewayProxyRequest - Your handler function runs and returns an
ApiGatewayProxyResponse - Lambda serializes the response back to JSON
- API Gateway converts it into a proper HTTP response and sends it back to the client
The client gets:
{
"statusCode": 200,
"message": "Hello World!",
"data": null
}We have a handler, but it's not connected to anything yet. In the next post, we'll add this Lambda function to our template.yaml so AWS actually knows it exists.