With our crate in place we can set up our lambda code for the Netlify Function. We'll need tokio, serde_json, and lambda_runtime. We can use -p to add crates to our pokemon-api package.
cargo add -p pokemon-api tokio lambda_runtime http aws_lambda_events
tokio is an async runtime and we need it because the function signature we'll be working with from lambda_runtime is async.
lambda_runtime is the Rust runtime for AWS Lambda maintained by a team at AWS, which is what Netlify runs on behind the scenes. It's responsible for the mechanics of how lambda requests get handled.
http is a crate that contains a set of general purpose HTTP types. Things like StatusCode or constants for headers like ACCEPT and CONTENT_TYPE.
aws_lambda_events is a package full of types for various AWS Lambda events. Since Netlify Functions only interacts with the API Gateway types, we'll be using those here, but these types can also be used when deploying say, AppSync resolvers or Cognito hooks on AWS.
use aws_lambda_events::{
encodings::Body,
event::apigw::{
ApiGatewayProxyRequest, ApiGatewayProxyResponse,
},
};
use http::header::HeaderMap;
use lambda_runtime::{service_fn, LambdaEvent, Error};
#[tokio::main]
async fn main() -> Result<(), Error> {
println!("cold start");
let processor = service_fn(handler);
lambda_runtime::run(processor).await?;
Ok(())
}
async fn handler(
_: LambdaEvent<ApiGatewayProxyRequest>,
) -> Result<ApiGatewayProxyResponse, Error> {
println!("handler");
let response = ApiGatewayProxyResponse {
status_code: 200,
headers: HeaderMap::new(),
multi_value_headers: HeaderMap::new(),
body: Some(Body::Text("Boop".to_string())),
is_base64_encoded: Some(false),
};
Ok(response)
}
The setup for a lambda function can be through of as two phases:
- what happens on a cold start
- what happens to process a single incoming event
In our case, the main function runs on cold start, which means it runs once for each instance of our function Netlify or AWS would build up. Another instance of a function is generally started if there aren't enough instances available to handle the current requests.
The lambda_runtime::run call in main internally bootstraps a Stream while loop to process each event, just like we did in the Pokemon csv upload course, except this one never completes.
The handler function is the one that the lambda_runtime::run processing uses to process each event.
#[tokio::main]
async fn main() -> Result<(), Error> {
println!("cold start");
let processor = service_fn(handler);
lambda_runtime::run(processor).await?;
Ok(())
}
The main function is wrapped in the tokio::main macro, which wraps our application in the tokio async runtime. That's what lets us await the lambda runtime and process each event.
println! is being used so that later when we look at the logs we can easily see which pieces of our code are running at what times. This is the section that only runs during the cold start.
The lambda runtime crate includes a trait called Handler. Any struct that implements Handler can be passed into the lambda_runtime::run function. service_fn creates a struct that implements this trait for us, it's a lot like the pattern of calling ::new to construct a new struct that we used in the PokemonId from the csv upload course.
The processor can then be passed into the lambda_runtime::run function where it will be used to call our handler function for each event.
We await here, which for our thought models purposes never returns. It waits in a loop for new events to come in forever as long as the lambda is alive.
Our handler is much smaller and does less.
async fn handler(
_: LambdaEvent<ApiGatewayProxyRequest>,
) -> Result<ApiGatewayProxyResponse, Error> {
println!("handler");
let response = ApiGatewayProxyResponse {
status_code: 200,
headers: HeaderMap::new(),
multi_value_headers: HeaderMap::new(),
body: Some(Body::Text("Boop".to_string())),
is_base64_encoded: Some(false),
};
Ok(response)
}
The handler function accepts one argument. ApiGatewayProxyRequest includes the request path, http method used, headers, and more.
ApiGatewayProxyRequest {
resource: None,
path: Some("/.netlify/functions/pokemon-api"),
http_method: GET,
headers: {
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"accept-encoding": "br, gzip",
"accept-language": "en-US,en;q=0.9",
"client-ip": "120.70.0.92",
"connection": "keep-alive",
"forwarded": "for=67.180.63.210;proto=https",
"host": "pokemon-rust.netlify.app",
"sec-ch-ua": "\"Chromium\";v=\"94\", \"Google Chrome\";v=\"94\", \";Not A Brand\";v=\"99\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"macOS\"",
"sec-fetch-dest": "document",
"sec-fetch-mode": "navigate",
"sec-fetch-site": "none",
"sec-fetch-user": "?1",
"upgrade-insecure-requests": "1",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36",
"via": "http/1.1 Netlify[dc13cafe-8a6e-4e18-87b5-42108e558ff7] (Netlify Edge Server)",
"x-cdn-domain": "www.bitballoon.com",
"x-country": "US",
"x-datadog-parent-id": "6268151077112857393",
"x-datadog-sampling-priority": "1",
"x-datadog-trace-id": "15322485002320125628",
"x-forwarded-for": "67.180.63.210, 100.64.0.92",
"x-forwarded-proto": "https",
"x-language": "en,en;q=0.9",
"x-nf-cdn-host": "proxy-5797688bf-5zl4l",
"x-nf-client-connection-ip": "67.180.63.210",
"x-nf-connection-proto": "https",
"x-nf-request-id": "01FJ70W9S8AS23JX8WM89MFEXE",
},
multi_value_headers: {
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"accept-encoding": "br, gzip",
"accept-language": "en-US,en;q=0.9",
"client-ip": "100.64.0.92",
"connection": "keep-alive",
"forwarded": "for=67.180.63.210;proto=https",
"sec-ch-ua": "\"Chromium\";v=\"94\", \"Google Chrome\";v=\"94\", \";Not A Brand\";v=\"99\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"macOS\"",
"sec-fetch-dest": "document",
"sec-fetch-mode": "navigate",
"sec-fetch-site": "none",
"sec-fetch-user": "?1",
"upgrade-insecure-requests": "1",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36",
"via": "http/1.1 Netlify[dc13cafe-8a6e-4e18-87b5-42108e558ff7] (Netlify Edge Server)",
"x-cdn-domain": "www.bitballoon.com",
"x-country": "US",
"x-datadog-parent-id": "6268151077112857393",
"x-datadog-sampling-priority": "1",
"x-datadog-trace-id": "15322485002320125628",
"x-forwarded-for": "67.180.63.210, 100.64.0.92",
"x-forwarded-proto": "https",
"x-language": "en,en;q=0.9",
"x-nf-cdn-host": "proxy-5797688bf-5zl4l",
"x-nf-client-connection-ip": "67.180.63.210",
"x-nf-connection-proto": "https",
"x-nf-request-id": "01FJ70W9S8AS23JX8WM89MFEXE",
"host": "pokemon-rust.netlify.app",
},
query_string_parameters: {},
multi_value_query_string_parameters: {},
path_parameters: {},
stage_variables: {},
request_context: ApiGatewayProxyRequestContext {
account_id: None,
resource_id: None,
operation_name: None,
stage: None,
domain_name: None,
domain_prefix: None,
request_id: None,
protocol: None,
identity: ApiGatewayRequestIdentity {
cognito_identity_pool_id: None,
account_id: None,
cognito_identity_id: None,
caller: None,
api_key: None,
api_key_id: None,
access_key: None,
source_ip: None,
cognito_authentication_type: None,
cognito_authentication_provider: None,
user_arn: None,
user_agent: None,
user: None,
},
resource_path: None,
authorizer: {},
http_method: GET,
request_time: None,
request_time_epoch: 0,
apiid: None,
},
body: None,
is_base64_encoded: Some(false),
}
The LambdaEvent can contain additional data from Netlify Identity, such as the authenticated user. When we use them later, we'll split the LambdaEvent from the ApiGatewayProxyRequest.
We use println again to show when this handler is being run in the logs.
The ApiGatewayProxyResponse is a struct that helps us construct a valid function response. We could skip the struct and return JSON ourselves, similar to how a JavaScript function would work, but then we'd be responsible for remembering to make sure we specified a status code.
By using ApiGatewayProxyResponse we are ensuring that if any required values aren't filled out, our program won't compile.
let response = ApiGatewayProxyResponse {
status_code: 200,
headers: HeaderMap::new(),
multi_value_headers: HeaderMap::new(),
body: Some(Body::Text("Boop".to_string())),
is_base64_encoded: Some(false),
};
HeaderMap comes from the http crate and is like a JavaScript object specialized to storing headers and their values.
Body is an enum from the aws_lambda_events crate. It allows us to send Text or Binary data responses from our function, as well as Empty.
At this point the application should build.
cargo build -p pokemon-api