TutorialbeginnerPart 12 of 18

Shared Code with lib.rs

Stop copy-pasting code across your Lambda functions.

April 23, 20263 min read

What you'll learn

  • Creating a library crate for shared logic
  • Organizing utilities and models
  • Importing library code into your binaries
Prerequisites:Previous post completed (Returning JSON Responses)

As your project grows, you'll start having multiple Lambda functions (binaries in src/bin/). If you have a struct or a helper function that more than one Lambda needs, you shouldn't copy-paste it. Instead, you should move it to a Library.

In Rust, a project can have one library (src/lib.rs) and many binaries.

Creating src/lib.rs

Create a new file at src/lib.rs. This file acts as the "root" of your shared library.

src/lib.rs
rust
pub mod util; // This tells Rust to look for a file called src/util.rs
 
use serde::{Deserialize, Serialize};
 
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApiResponse<T> {
    pub status_code: u16,
    pub error: Option<String>,
    pub message: String,
    pub data: Option<T>,
}

Notice the pub keyword. In Rust, everything is private by default. If you want a binary to be able to see something in your library, you must mark it as pub.

Creating src/util.rs

Now, let's create src/util.rs and move our response helpers there.

src/util.rs
rust
use crate::ApiResponse; // Import from our own lib.rs
use aws_lambda_events::{
    apigw::ApiGatewayProxyResponse,
    encodings::Body,
    http::HeaderMap,
};
use serde::Serialize;
 
pub fn create_api_response<T: Serialize>(status: u16, message: &str, data: Option<T>) -> ApiGatewayProxyResponse {
    let response_body = ApiResponse {
        status_code: status,
        error: if status >= 400 { Some("Error".into()) } else { None },
        message: message.to_string(),
        data,
    };
 
    let mut headers = HeaderMap::new();
    headers.insert("Content-Type", "application/json".parse().unwrap());
    headers.insert("Access-Control-Allow-Origin", "*".parse().unwrap());
 
    ApiGatewayProxyResponse {
        status_code: status as i64,
        headers,
        multi_value_headers: HeaderMap::new(),
        body: Some(Body::Text(serde_json::to_string(&response_body).unwrap())),
        is_base64_encoded: false,
    }
}

Using the library in your binaries

Now, your Lambda functions in src/bin/ can import this code just like they would from an external crate. The name of your library is the name defined in the [package] section of your Cargo.toml.

For us, it's rust-serverless-api.

Update src/bin/hello.rs:

src/bin/hello.rs
rust
use aws_lambda_events::apigw::{ApiGatewayProxyRequest, ApiGatewayProxyResponse};
use lambda_runtime::{run, service_fn, Error, LambdaEvent};
 
// Import from our shared library!
use rust_serverless_api::util::create_api_response;
 
async fn handler(_event: LambdaEvent<ApiGatewayProxyRequest>) -> Result<ApiGatewayProxyResponse, Error> {
    // Look how clean this is now!
    Ok(create_api_response(200, "Hello from the shared library!", Some("It works!")))
}
 
#[tokio::main]
async fn main() -> Result<(), Error> {
    run(service_fn(handler)).await
}

Why this is powerful

Imagine you have 10 different Lambdas.

  • One handles Users.
  • One handles Orders.
  • One handles Payments.

All of them need to talk to the same database. All of them need to validate the same type of auth token. All of them need to return the same response format.

By putting that logic in lib.rs, you only write it (and test it) once. When you find a bug in create_api_response, you fix it in one place, and all 10 Lambdas are fixed immediately.

Folder Structure Check

Your project should now look like this:

  • src/lib.rs (Shared types)
  • src/util.rs (Shared functions)
  • src/bin/hello.rs (One Lambda)
  • src/bin/hello_name.rs (Another Lambda)

We can handle GET requests, but what about sending data? In the next post, we'll learn how to handle POST requests and parse JSON request bodies.