TutorialbeginnerPart 13 of 18

Handling POST Requests

Learn how to receive and parse JSON data from your clients.

April 24, 20263 min read

What you'll learn

  • Defining an input struct for JSON data
  • Parsing the request body safely
  • Updating template.yaml for POST methods
Prerequisites:Previous post completed (Shared Code with lib.rs)

Listen, we've only handled GET requests so far. That's fine for reading data, but what if you want to send something to your API? Whether it's a new user registration or a contact form, you'll need to handle POST requests with a JSON body.

In this lesson, we'll learn how to parse that JSON body in Rust.

Input struct

Just like we use a struct to send JSON, we use a struct to receive it. Let's create a struct that represents the data we expect from the client.

Add this to a new file src/bin/hello_post.rs:

src/bin/hello_post.rs
rust
use serde::Deserialize;
 
#[derive(Deserialize)]
struct GreetingInput {
    name: String,
    greeting: Option<String>,
}

We use #[derive(Deserialize)] because we are turning a JSON string into a Rust struct.

Parsing the Body

The ApiGatewayProxyRequest has a body field, but it's just an Option<String>. We need to use serde_json to parse it into our GreetingInput struct.

Let's create a helper in src/util.rs to make this easy:

src/util.rs
rust
pub fn parse_body<T: serde::de::DeserializeOwned>(request: &ApiGatewayProxyRequest) -> Result<T, String> {
    let body_str = request.body.as_deref().unwrap_or("{}");
    serde_json::from_str::<T>(body_str).map_err(|e| format!("Invalid JSON: {}", e))
}

Parsing the JSON body

Now we can use it in our new handler:

src/bin/hello_post.rs
rust
use aws_lambda_events::apigw::{ApiGatewayProxyRequest, ApiGatewayProxyResponse};
use lambda_runtime::{run, service_fn, Error, LambdaEvent};
use rust_serverless_api::util::{create_api_response, parse_body};
use serde::Deserialize;
 
#[derive(Deserialize)]
struct GreetingInput {
    name: String,
    greeting: Option<String>,
}
 
async fn handler(event: LambdaEvent<ApiGatewayProxyRequest>) -> Result<ApiGatewayProxyResponse, Error> {
    // 1. Parse the JSON body
    let input: GreetingInput = match parse_body(&event.payload) {
        Ok(data) => data,
        Err(e) => return Ok(create_api_response::<()>(400, &e, None)),
    };
 
    // 2. Use the data
    let greeting = input.greeting.unwrap_or_else(|| "Hello".to_string());
    let message = format!("{}, {}!", greeting, input.name);
 
    Ok(create_api_response(200, &message, Some("Success")))
}
 
#[tokio::main]
async fn main() -> Result<(), Error> {
    run(service_fn(handler)).await
}

Updating Configuration

Don't forget to register the new binary in Cargo.toml:

Cargo.toml
toml
[[bin]]
name = "hello_post"
path = "src/bin/hello_post.rs"

And update template.yaml to add the POST method:

template.yaml
yaml
  PostHelloMethod:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref RustExample
      ResourceId: !Ref HelloResource
      AuthorizationType: NONE
      HttpMethod: POST
      Integration:
        Type: AWS_PROXY
        IntegrationHttpMethod: POST
        Uri: !Sub "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HelloPostFunction.Arn}/invocations"

(You would also need to define HelloPostFunction and its permissions, similar to how we did for HelloFunction.)

Testing the POST request

After building and deploying (sam build && sam deploy), you can test it with curl:

Terminal
bash
curl -X POST https://your-api.com/dev/hello \
     -H "Content-Type: application/json" \
     -d '{"name": "Hazeezet", "greeting": "Welcome"}'

Response:

json
Source File
{
  "statusCode": 200,
  "message": "Welcome, Hazeezet!",
  "data": "Success"
}

Warning

If the client sends invalid JSON (like a missing comma), our parse_body helper will catch the error and return a 400 Bad Request. This prevents your Lambda from crashing on bad input.

We now have three separate Lambda functions for three different endpoints. This is starting to feel like a lot of boilerplate in template.yaml. In the next post, we'll learn a pro tip: how to use one Lambda function to handle multiple endpoints.