TutorialintermediatePart 14 of 18

One Lambda, Multiple Endpoints

Reducing overhead by grouping related logic into a single function.

April 25, 20263 min read

What you'll learn

  • The "Monolith-ish" Lambda pattern
  • Implementing internal routing in Rust
  • Updating template.yaml for multiple routes to one Lambda
Prerequisites:Previous post completed (Handling POST Requests)

Up until now, we've used the "one function per endpoint" pattern. It's the standard serverless pitch, but in the real world, it can be a pain to manage.

Every new function adds deployment time, more boilerplate, and more cold starts. Let's fix that by grouping our logic.

Routing strategy

Instead of AWS deciding which code to run based on the URL, we'll let AWS send everything to one Lambda, and we will decide what to do inside our Rust code using a match statement.

Internal Routing

Let's create a helper in src/util.rs to extract the HTTP method and the resource path:

src/util.rs
rust
pub fn get_route(request: &ApiGatewayProxyRequest) -> (String, String) {
    let method = request.http_method.to_string();
    let resource = request.resource.clone().unwrap_or_default();
    (method, resource)
}

Now, let's create src/bin/GreetingManager.rs (we'll replace our old binaries with this one).

src/bin/GreetingManager.rs
rust
use aws_lambda_events::apigw::{ApiGatewayProxyRequest, ApiGatewayProxyResponse};
use lambda_runtime::{run, service_fn, Error, LambdaEvent};
use rust_serverless_api::util::*;
 
async fn handler(
    event: LambdaEvent<ApiGatewayProxyRequest>,
) -> Result<ApiGatewayProxyResponse, Error> {
    let (method, resource) = get_route(&event.payload);
 
    match (method.as_str(), resource.as_str()) {
        ("GET", "/hello") => {
            Ok(create_api_response(200, "Hello World!", Some(())))
        }
        ("GET", "/hello/{name}") => {
            let name = get_path_param(&event.payload, "name").unwrap_or("Guest".into());
            let message = format!("Hello, {}!", name);
            Ok(create_api_response(200, &message, Some(())))
        }
        ("POST", "/hello") => {
            // ... (POST logic from last lesson) ...
            Ok(create_api_response(200, "Greeting received", Some(())))
        }
        _ => {
            Ok(create_api_response(404, "Endpoint not found", Some(())))
        }
    }
}
 
#[tokio::main]
async fn main() -> Result<(), Error> {
    run(service_fn(handler)).await
}

Updating template.yaml

Now, instead of three AWS::Serverless::Function resources, we only need one. But we still need multiple methods that point to it.

template.yaml
yaml
Resources:
  GreetingManager:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: .
      Handler: bootstrap
      Runtime: provided.al2023
      Architectures: ["arm64"]
 
  # Method 1: GET /hello
  GetHelloMethod:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref RustExample
      ResourceId: !Ref HelloResource
      HttpMethod: GET
      Integration:
        Type: AWS_PROXY
        Uri: !Sub "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GreetingManager.Arn}/invocations"
 
  # Method 2: GET /hello/{name}
  GetHelloNameMethod:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref RustExample
      ResourceId: !Ref HelloNameResource
      HttpMethod: GET
      Integration:
        Type: AWS_PROXY
        Uri: !Sub "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GreetingManager.Arn}/invocations"

Why do this?

  • Fewer Cold Starts: Since one Lambda is handling three routes, it stays "warm" more often.
  • Better Performance: If you connect to a database, you can initialize the connection once in the main function and reuse it for all three routes.
  • Easier Management: You have fewer binaries to build and deploy.

Info

This is the pattern used in the real production application we've been referencing. It's often called a "Lambda-lith" or a "Grouped Lambda."

Our Lambda is becoming a real manager! But it still has a lot of "hardcoded" behavior. In the next post, we'll learn how to use Environment Variables and Stages to change our API's behavior without changing the code.