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