TutorialbeginnerPart 6 of 18

Writing Your First Lambda Handler

Time to actually write some Rust code that runs on Lambda.

April 17, 20264 min read

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
Prerequisites:Previous post completed (Updating Configuration Files)

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:

src/bin/hello.rs
rust
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:

Cargo.toml
toml
[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_runtime from 0.6 to 0.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:

src/bin/hello.rs
rust
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 a status_code, headers, and a body.
  • Body::Text(...) - The response body is a JSON string wrapped in the Body enum.
  • HeaderMap - Standard HTTP headers. We set Content-Type to application/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:

  1. API Gateway receives the HTTP request
  2. API Gateway converts it into a JSON event and sends it to your Lambda
  3. Your Lambda deserializes it into ApiGatewayProxyRequest
  4. Your handler function runs and returns an ApiGatewayProxyResponse
  5. Lambda serializes the response back to JSON
  6. API Gateway converts it into a proper HTTP response and sends it back to the client

The client gets:

json
Source File
{
  "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.