How to Make an API Endpoint Accept POST Requests with an Optional JSON body in axum?

Photo by RealToughCandy.com on Pexels.com

What is axum?

axum is a very popular web application framework in the Rust world.

How does axum handle incoming requests?

Like many other web application frameworks, axum routes the incoming requests to handlers.

What is a handler?

In axum, a handler is an async function that accepts zero or more “extractors” as arguments and returns something that can be converted into a response.

What is an extractor?

In axum, an extractor is a type for extracting data from requests, which implements FromRequest or FromRequestParts.

For example, Json is an extractor that consumes the request body and deserializes it as JSON into some target type:

use axum::{
    Json,
    routing::post,
    Router,
};
use serde::Deserialize;

#[derive(Deserialize)]
struct CreateUser {
    email: String,
    password: String,
}

async fn create_user(Json(payload): Json<CreateUser>) {
    // ...
}

let app = Router::new().route("/users", post(create_user));

We can spot a few things in the above example:

  1. the async function create_user() is the handler, which is set up to handle POST requests against the /users endpoint
  2. the function argument Json(payload) is a Json extractor that consumes the JSON body and deserializes it as JSON into the target type CreateUser

How to make an API endpoint accept POST requests with an optional JSON body?

All extractors defined in axum will reject the request if it doesn’t match. If you wish to make an extractor optional, you can wrap it in Option:

use axum::{
    Json,
    routing::post,
    Router,
};
use serde_json::Value;

async fn create_user(payload: Option<Json<Value>>) {
    if let Some(payload) = payload {
        // We got a valid JSON payload
    } else {
        // Payload wasn't valid JSON
    }
}

let app = Router::new().route("/users", post(create_user));

How to Read a Text File Line by Line Efficiently in Rust?

Photo by Poppy Thomas Hill on Pexels.com

How to open a file for reading?

In Rust, a reference to an open file on the filesystem is represented by the struct std::fs::File. And we can use the File::open method to open an existing file for reading:

pub fn open<P: AsRef<Path>>(path: P) -> Result<File>

Which takes a path (anything it can borrow a &Path from i.e. AsRef<Path>, to be exact) as a parameter and returns an io::Result<File>.

How to read a file line by line efficiently?

For efficiency, readers can be buffered, which simply means they have a chunk of memory (a buffer) that holds some input data in memory. This saves on system calls. In Rust, a BufRead is a type of Read which has an internal buffer, allowing it to perform extra ways of reading. Note that File is not automatically buffered as File only implements Read but not BufRead. However, it’s easy to create a buffered reader for a File:

BufReader::new(file);

Finally, we can use the std::io::BufRead::lines() method to return an iterator over the lines of this buffered reader:

BufReader::new(file).lines();

Put everything together

Now we can easily write a function that reads a text file line by line efficiently:

use std::fs::File;
use std::io::{self, BufRead, BufReader, Lines};
use std::path::Path;

fn read_lines<P>(path: P) -> io::Reasult<Lines<BufReader<File>>>
where P: AsRef<Path>,
{
    let file = File::open(path)?;
    Ok(BufReader::new(file).lines())
}

Want to buy me a coffee? Do it here: https://www.buymeacoffee.com/j3rrywan9