DEV Community

Cover image for Learn Tauri by Doing - Part 2: First method and APIs call
Giuliano1993
Giuliano1993

Posted on

Learn Tauri by Doing - Part 2: First method and APIs call

Introduction

Here we are at the second part of our journey in Tauri. Last time we left after finishing analyzing a project structure, explaining also how to invoke a Rust function from the JS frontend. If you've lost the previous part, you can find it at this link.
Today we're going to learn how to call some APIs from Rust and return the result to the JS frontend.
So, since the first time it was more theory than code; let's lose no more time and get started!

The setup

There are two ways to proceed in setup part and, then, in the following ones. These two ways can be applied to what we'll see in this API part but also in the following ones.
In fact you can access most functionalities both from Rust and from JS, using Tauri provided APIs.
If you don't want to use Rust while developing in Tauri, you can easily rely on JS modules and go on with your work; that is an amazing feature that really helps anyone to get started with this tool.
So in this very first case, you can go in your tauri.conf.json
and add to your allowlist:

//tauri.conf.json
...
"tauri":{
    "allowlist":{
        "http":{
            "scope":[] //an array of allowed url / url pattern that can be called from the web view,
            "all": true // this is used to allow all http features
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Once you configured, you'll be able accessing all Tauri-provided HTTP APIs directly from your JS.
Since these articles will focus on Rust development, as previously said, i won't go deeper on this part.
You can find all the documentation about JS Tauri HTTP Api here

So, if we mean to follow the Rust way we won't need to edit the tauri.conf.json; but we will need three packages instead: serde, serde_json and reqwest. While the first two are already installed when we init our Tauri project, we will need to add reqwest with it's json feature. So enter the src-tauri folder and run

cargo add reqwest --features=json
Enter fullscreen mode Exit fullscreen mode

This will download the package and add it to your Cargo.toml file
We're going to use serde and serde_json packages to serialize and deserialize data, while reqwest will be used, as the name suggests, to create our requests.

The first method

So the first thing i wanted to build Rust-side when started developing my application was a call to GPT to quickly translate my Italian newsletter to its English version, so that I only had to check on that translation and correct it, if necessary, instead of writing it all over again.

So the first step is to create a function that is callable from the front-end; as we saw last time, this can be accomplished through the #[tauri::command] attribute.

// main.rs

#[tauri::command]
async fn translate(text: String) -> String {

}
Enter fullscreen mode Exit fullscreen mode

As you see, we're creating an async function, that receives a String as a parameter and returns a String ( GPT Answer ). Now first of all we need a body for our request. To be precise, we need a json body; to do that, we can use serde_json::json method.

//main.rs

use serde_json::json;

#[tauri::command]
async fn translate(text: String) -> String {
    let body = json!({
        "model":"gpt-3.5-turbo",
        "messages": [
          {
            "role": "system",
            "content": "Translate the text you receive from italian to english. Return only the translated text. Format it as unparsed markdown."
          },
          {
            "role":"user",
            "content": text
          }
        ]
   });
}

Enter fullscreen mode Exit fullscreen mode

Once we have the body for our request it's time to create it. We also need a token for GPT requests ( and for most APIs actually ). For the moment we'll hard code it into our function, next time we'll talk about file system and how to handle this part more securely.


//main.rs
//... 
let open_ai_token = "this_is_my_token";

let response = Client::new()
.post("https://api.openai.com/v1/chat/completions")
.bearer_auth(open_ai_token)
.json(&body)
.send().await.unwrap()
.text().await.unwrap();

return response
Enter fullscreen mode Exit fullscreen mode

Okay; now there is some stuff going on here so let's break it down.
First of all we define our token, fine.
After that we create a new reqwest::Client to make our request. We define it as a POST request thank to the post function and we specify out bearer token.
After that we add a json body to the request.
Once we do this, we send our request, wait for the result retrieve it with unwrap.

If you're not familiar with Rust, in cases where a function returns Result that can be either a value or an error, unwrap directly catches the value, if there is one, otherwise it panics. It is best used when it's probably there will be no actual error in the execution, otherwise Pattern Matching is a way better solution.

Once we have the result we want to transform it into a String we can return to our front-end, so once again, we use the .await.unwrap() structure.

Finally, we just return our response.

Apparently, the function is complete. In order to make this function available in the front-end we need to take one more step. In our main function, we need to add the translate function to the array of generated handlers; So let's add it like this

.invoke_handler(tauri::generate_handler![greet, translate])
Enter fullscreen mode Exit fullscreen mode

and then we're ready to move to our frontend.

The front-end

Now that we have our rust function ready, it's time to call it from the front-end. So let's make a new component. As I said, you can use whatever framework you prefer for this part; I'm using Vue.

<script setup lang="ts">
// Translate.vue
import { ref } from "vue";
import { invoke } from "@tauri-apps/api/tauri";

const text = ref("");
const translatedText = ref(""); 

async function translate() {
      await invoke("translate",{text: text.value}).then((res:string)=>{
      const parsed =  JSON.parse(res)
      translatedText.value = parsed.choices[0].message.content
  })
}
</script>



<template>
  <label>Text to translate</label>
  <input type="text" v-model="text">
  <button @click="translate">Translate</button>
  <div>
    {{ translatedText }}
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

Here's the component: as you can see it's nothing special: we just need to import the invoke function from Tauri and call it, passing our function name as first argument and an object with the arguments needed by the rust function as second one. Since we're returning a String we need to transform it into JSON before being able to get our result and showing our translation.

Improve the code a little

As we saw, we are returning a string and transforming it to Json in the front end. We could probably do better!

Let's get back to our main.rs file for a small improvement.
Since we know the structure of the response we're receiving, we can prepare some structs to cope with it and return a parsed already object to the frontend.
So let's add these three structures at the top of our main.rs


#[derive(Serialize, Deserialize)]
pub struct OpenAiMessage {
    role: String,
    pub content: String
}



#[derive(Serialize,Deserialize)]
pub struct OpenAiChoice {
    pub message: OpenAiMessage,
}



#[derive(Serialize,Deserialize)]
pub struct OpenAiCompletion {
    pub choices: Vec<OpenAiChoice>
}

Enter fullscreen mode Exit fullscreen mode

As you see, we're just working with one key from the actual response, which is choices. In more realistic cases you may want to add some more granular control but for the moment is fine like this.
As you can see we derive serde Serialize and Deserialize traits for all these structs. That's because, everything returned by a Tauri command must be Serializable, and so implement that trait; as for Deserialize, we need it to transform the response into our object.

Let's go to our translate function:

//main.rs
//...
async fn translate(text: String) -> OpenAiCompletion {
//...
let response = Client::new()
.post("https://api.openai.com/v1/chat/completions")
.bearer_auth(open_ai_key)
.json(&body)
.send().await.unwrap();

   let open_ai_response: OpenAiCompletion = response.json().await.unwrap();



   return open_ai_response

Enter fullscreen mode Exit fullscreen mode

As you can see the last part of the response chain was removed because we're not transforming it into a text anymore; instead we read the response as a json and assign that value to open_ai_response, infering it as a OpenAiCompletion. In order to return it we also changed the return type of the translate function.

Now we can get back to our frontend and update the function to receive this new value

// translate.vue
async function translate() {
  await invoke("translate",{text: text.value}).then((res:any)=>{
    translatedText.value = res.choices[0].message.content
  })
}

Enter fullscreen mode Exit fullscreen mode

Conclusion

This was our very first step getting our hands dirty with Tauri and Rust.
We could go further and deeper and we will, more and more; we will organize our commands in modules and will securely save our tokens; one step at a time we'll learn all we need to about this great tool!
For the moment I hope you enjoy the article and hopefully learned something.
I'm looking forward writing the next part.
Until then,
Happy Coding 0_1

Top comments (0)