Masai Mahapa

30 days of Rust - Day Twenty Seven - Integrating front and backend

Day 27 of Rust So now that we have seen how to create both the client and server side (front-end and back-end), we need a way to link them up. This is exactly what I learnt today.

If you just got here, please check out my previous posts, from where I started building the blog application from day 24.

Day 27 - Integrating front and backend

At the moment, we have 2 applications which do not even know of each other's existence. Our server can take requests from any type of client over the internet, being a web browser, mobile application or another server.

Making a request from the client

Dependencies

In order to make requests over the internet on the frontend, we need to add the following dependencies;

reqwasm = "0.5"
serde = { version = "1.0", features = ["derive"] }
wasm-bindgen-futures = "0.4"
  • The first request is a crate used to make requests in WASM (WebAssembly) applications. It's actually a wrapper to fetch in javascript.
  • Serde, we've already seen used in the backend to Serialize structs. This time we're going to use it to de-serialize.
  • wasm-bindgen-futures is a bridge between Rust futures and javascript Promises.

With the dependencies out of our way, we now can then proceed with making the requets. We will be expecting a Post struct when calling the endpoint http://localhost:8000/api/blog/{}.

use serde::Deserialize;
use reqwasm::http::Request;
 
#[derive(Deserialize, Clone)]
struct Post {
   title: String,
   content: String
}

We then create a state of blog_post, which will store the value once the request is successful. Initially it is set to Box(None). Remember that None is an Option, whose opposite is Some, when there is a value. use_state is a hook, which basically hooks into the internal state of Rust. It takes a closure as its argument.

   let blog_post = use_state(|| Box::new(None));
   let error = use_state(|| Box::new(None));
 
   {
       let blog_post = blog_post.clone();
       use_effect_with_deps(move |_| {
           let blog_post = blog_post.clone();
           wasm_bindgen_futures::spawn_local(async move {
               // let fetched_post = get_post(&post_id).await;
               let url = format!("http://localhost:8000/api/blog/{}", 1);
               let fetched_post  = Request::get(&url).send().await;
               println!("getting {}", url);
               match fetched_post {
                   Ok(response)=>{
                       let json: Result<Post, _> = response.json().await;
                       match json {
                           Ok(f) => {
                               blog_post.set(Box::new(Some(f)))
                           }
                           Err(e) => error.set(Box::new(Some(e.to_string()))),
                       }
                   }
               Err(e) => error.set(Box::new(Some(e.to_string()))),
               }
           });
           || ()
       }, ());
   }
 

The code above can make one dizzy. I think the most important part here is where we make the actual request. We make a get request to our endpoint and pass in an id. We then await a response.

let url = format!("http://localhost:8000/api/blog/{}", 1);
let fetched_post  = Request::get(&url).send().await;

If the response is OK and not an error, we then proceed to convert it to a json format.

let json: Result<Post, _> = response.json().await;
                       match json {
                           Ok(f) => {
                               blog_post.set(Box::new(Some(f)))
                           }
                           Err(e) => error.set(Box::new(Some(e.to_string()))),
                       }

If we are able to successfully make the conversion, we can then assign the result blog_post. If we get an error we then set error to the error message we get.

Displaying the blog post

Now that we finally have a blog_post, we can conditionally render the content our request was successful. If not we can just show Empty blog in place.

match (*blog_post).as_ref() {
       Some(post) => html! {
           <div id="home">
           <Layout >
           <Header />
           <h1>{"Blog"}</h1>
          <p> { post.title.clone() } </p>
          <p> { post.content.clone() } </p>
           </Layout>
           </div>
       },
       None => html! {
           <div id="home">
           <Layout >
           <Header />
           <h1>{"Empty Blog"}</h1>
       
           </Layout>
           </div>
       }
   }

CORS

Running the project, we now get an error message saying Empty Blog. How come? We did everything right though.

Well, when we take a closer look by going inside our console, we see the following error; cors The above error relates to Cross Origin Resource Sharing. It's basically a security feature by browsers, to not allow our code to make requests to anything outside the server it exists.

So while you're on travcar.co.za, the scripts on your browser can't just make requests to www.gmail.com, and possibly do malicious stuff there.

How do we solve it?

We need to make the server allow requests from outside scripts.

For this we need to add another dependency, called actix-cors.

actix-cors = "0.6.1"

We then need to add the following code inside the HTTP server. Basically it adds to its headers information which says where a request can come from by the allowed_origin method, as well as the methods that are allowed to be performed.

use actix_cors::Cors;
//other code seen previously
 
 
#[actix_web::main]
async fn main() -> std::io::Result<()> {
   HttpServer::new(|| {
 
       let cors = Cors::default()
           .allowed_origin(format!("http://localhost:8080").as_str())
           .allowed_methods(vec!["GET", "POST"])
           .allowed_headers(vec![http::header::AUTHORIZATION, http::header::ACCEPT])
           .allowed_header(http::header::CONTENT_TYPE)
           .max_age(3600);
 
           App::new()
           .wrap(cors)
           // attach out services

Now if we navigate to our browser, we see the following. successfully blog request A success.

Conclusion

We've successfully put together the front as well as backend. I can now say I'm a full stack developer. Talk to me nicely.

This has been a great learning curve.

Next up, we need to figure out where to store the data, possibly a local database. Thereafter I can consider deployment so you can access it on your browser.

Please let me know what you think of Rust as a full stack development language.

Thanks for spending some time learning Rust with me. Until next time. Peace ✌🏼 .

Share