Search⌘ K
AI Features

Create Endpoint

Explore how to create a web endpoint in Rust that accepts JSON parameters and calls a database handler function. Understand module use, cloning versus referencing for parameter handling, and implement tests to ensure endpoint reliability.

We'll cover the following...

Creates the handler

In this lesson, we need an endpoint that receives the parameters through the webserver.

We need to make our create_product function public. To do so, we add the pub keyword at the beginning to change the corresponding line.

Rust 1.40.0
pub fn create_product(new_product: NewCompleteProduct, conn: &SqliteConnection) -> Result<i32> {

We also need to add derive instructions to NewProduct and NewCompleteProduct.

Rust 1.40.0
#[derive(Clone, Serialize, Deserialize)]
pub struct NewCompleteProduct {
pub product: NewProduct,
pub variants: Vec<NewVariantValue>
}
#[derive(Insertable, Debug, AsChangeset, Serialize, Deserialize, Clone)]
#[table_name="products"]
pub struct NewProduct {
pub name: String,
pub cost: f64,
pub active: bool,
}

We’re now ready to add our new handler. This will capture a JSON form of a NewCompleteProduct object and add it as a parameter to create_product.

Rust 1.40.0
mod product;
use actix_web::{web, App, HttpServer, Responder, HttpResponse, Result};
use product::create_product;
use ::shoe_store::models::NewCompleteProduct;
use ::shoe_store::establish_connection;
// This endpoint will create a new product, we just call the
// create_product function and expect a response, if everything is ok we just
// return a 200 response like this: HttpResponse::Ok(), otherwise
// we can return a 500 internal server error.
async fn product_create(product: web::Json<NewCompleteProduct>) -> Result<impl Responder> {
let connection = establish_connection();
match create_product(product.clone(), &connection) {
Ok(_) => Ok(HttpResponse::Ok()),
Err(error) => Err(actix_web::error::ErrorInternalServerError(error))
}
}
// In the main function we can create the routes to our endpoints.
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route("products", web::post().to(product_create))
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}

Rust organizes code through modules, so we can move all our code to handle database operations to a file called product.rs. We can then use the mod keyword to load that file.

The create_product function requires a product object. However, we have a web::Json object.

In order to obtain our product, we can try to dereference it by using a * before product. However, it won’t work because a move occurs.

We have two alternatives. We can either transform the parameter to a reference in the create_product function or clone the product. It’s safe to clone the product in this case because the object won’t be that big. However, we can transform the parameter to a reference as an alternative to cloning, if we need a performance improvement.

Testing

Now, we need to test our endpoint.

Rust 1.40.0
#[cfg(test)]
mod tests {
use super::*;
use actix_web::{test, web, App};
use diesel::Connection;
use ::shoe_store::establish_connection_test;
use ::shoe_store::models::{NewCompleteProduct, NewProduct, NewVariantValue, NewVariant};
#[actix_rt::test]
async fn test_product_creation_is_ok() {
let connection = establish_connection_test();
connection.begin_test_transaction().unwrap();
// We can mock the server with init_service.
let mut app = test::init_service(
App::new()
.data(connection)
.route("products", web::post().to(product_create))
).await;
let body =
NewCompleteProduct {
product: NewProduct {
name: "boots".to_string(),
cost: 13.23,
active: true
},
variants: vec![
NewVariantValue {
variant: NewVariant {
name: "size".to_string()
},
values: vec![
Some(12.to_string()),
Some(14.to_string()),
Some(16.to_string()),
Some(18.to_string())
]
}
]
};
// We test our endpoint by sending the body through a post request.
let req = test::TestRequest::post().set_json(&body).uri("/products").to_request();
let resp = test::call_service(&mut app, req).await;
// Expect a 200 response.
assert!(resp.status().is_success());
}
}

In the above code, we use the function begin_test_transaction to make sure we didn’t commit to the database and to keep it clean.

Testing makes sure we obtain a successful status and the code works as expected.