One Lambda, Multiple Endpoints
Reducing overhead by grouping related logic into a single function.
What you'll learn
- The "Monolith-ish" Lambda pattern
- Implementing internal routing in Rust
- Updating template.yaml for multiple routes to one Lambda
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:
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).
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.
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
mainfunction 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.