Handling POST Requests
Learn how to receive and parse JSON data from your clients.
What you'll learn
- Defining an input struct for JSON data
- Parsing the request body safely
- Updating template.yaml for POST methods
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:
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:
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:
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:
[[bin]]
name = "hello_post"
path = "src/bin/hello_post.rs"And update template.yaml to add the POST method:
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:
curl -X POST https://your-api.com/dev/hello \
-H "Content-Type: application/json" \
-d '{"name": "Hazeezet", "greeting": "Welcome"}'Response:
{
"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.