How to build Web based API with Rust
Rust

How to build Web based API with Rust (part 2)

A getting started guide to developing web based API with rust and actix-web.

Gary Woodfine

Gary Woodfine

25 Sept 2024

In Part 1 of this guide we went through the process of starting and creating a new Rust project and configured it to make use of the Actix-web crate, to enable creating a Web-Based API Project. The article provided a solid foundation to build upon, and this part of the guide we are going to further explore Actix-Web crate to gain a deeper understanding. We will also learn more about some additional crates we can use to add more functionality to our API.

What is a REST API

A flexible, lightweight approach to connecting and integrating applications

I will be the first to admit that, the first part of this article series, may be rather contrived, but I would just like to readers to keep in mind that, there is still a lot going on, and if you're new to Rust, there is still quite a bit to get familiar with.

Rust programming language has proven to be an efficient and reliable tool. Its distinction from other languages sets it apart, making it the most admired language eight times in a row. However, Rust does come with a steep learning curve compared to other programming languages, and often these things only become noticeable when you start scraping the tip of the iceberg.

In the previous article we created a really simple GET endpoint to start with to start along our path of creating a REST based API. The API itself provided very little functionality. In this article, we're going to dive a little deeper into Actix-web and even start using some additional crates that are available to add additional functionality to our API.

In our first example, we're going to add an endpoint to our greetings.rs we created in the first article which will take some query parameters to display the name of the person querying the API. It is still a rather contrived example, but nevertheless, it introduces a number of topics that are worth discussing and provides further examples of developing in rust.

Adding A GET request that take query string parameters

We are going to add an endpoint which will take two parameters, namely FirstName and Lastname to provide a personalized greeting to our API.

Our first step is add the new crate to our project, the serde crate, which is a framework for serializing and deserializing Rust data structures efficiently and generically.

The Serde ecosystem consists of data structures that know how to serialize and deserialize themselves along with data formats that know how to serialize and deserialize other things. Serde provides the layer by which these two groups interact with each other, allowing any supported data structure to be serialized and deserialized using any supported data format.

serde.rs

We can add serde to our project using the cargo cli, while we do this we also want to add a specific feature in serde, the derive feature, which provides a derive macro to generate implementations of the Serialize and Deserialize traits for data structures defined in your crate, allowing them to be represented conveniently in all of Serde's data formats.

To do this, we simply use the following command:

cargo add serde --features derive

With the crate added, we can now go to our greetings.rs file and start implementing our additional endpoint, the first step is to import another object from the Actix-web into our logic, the web object.

  • web: is a submodule or item within the actix_web crate. It is typically used to manage web-specific types and
  • functions, such as handling routes, extracting data from requests, and more.

To do this we'll simply add it as an additional import parameter in our use statement

use actix_web::{web, get, HttpResponse, Responder};

We now also want to declare that we are going to use serde library in our file

use serde::Deserialize;

Deserialize is a trait provided by the serde crate. In Rust, a trait is kind of like an interface in other languages; it defines a set of methods that must be implemented for a type.

The Deserialize trait allows a type to define how it can be deserialized from a given format (e.g., JSON, TOML, YAML).

We are now going to create a Person object, which will have properties that we want to collect with a get endpoint, which will be used to create the personalised greeting.

#[derive(Deserialize)]
struct Person {
    first_name: String,
    last_name: String
}
  • Struct Definition: defines a struct named Person. In Rust, a struct is a complex data type that groups multiple related values. Structs are used to create custom types that package together different pieces of data.
  • first_name: String: This field is a String type that will hold the first name of the person.
  • last_name: String: This field is a String type that will hold the last name of the person.

We can now add our endpoint logic which will utilise everything we just created to provide the additional functionality we wanted to achieve. This will be a really contrived and simple, but yet still has a lot going on.

#[get("/greet")]
async fn greet(person: web::Query<Person>) -> impl Responder {
    let greeting =  format!("Hello {} {} !", person.first_name, person.last_name);
    HttpResponse::Ok().body(greeting)
}

#[get("/greet")] is a route attribute, an attribute in rust is denoted by #..., are metadata applied to some module, crate, or item. They can be used for various purposes, including conditional compilation, linting, and more.

"/greet": specifies the path that will trigger the route. In this case, it matches when the path /greet is accessed.

async fn greet(person: web::Query<Person>) -> impl Responder {
  • The async keyword indicates that this function is asynchronous, meaning it can perform non-blocking I/O operations. fn greet declares the function name as greet.
  • The function takes a single argument, person, which is of type web::Query<Person>. This means we expect the person to be extracted from the query parameters of the request, and it will be deserialized into a Person struct. *The return type impl Responder indicates that this function will return a type that implements the Responder trait.

Inside the function

let greeting = format!("Hello {} {} !", person.first_name, person.last_name);

This line creates a formatted string greeting using the format! macro. It includes the first_name and last_name fields from the person query parameter.

HttpResponse::Ok().body(greeting)
  • HttpResponse::Ok() creates an HTTP 200 OK response.
  • .body(greeting) sets the body of the HTTP response to the greeting string.
  • The response is returned, fulfilling the function's promise.

This function greet is designed to handle an HTTP request asynchronously. It takes a query parameter parsed into a Person struct, constructs a personalized greeting message, and returns an HTTP 200 OK response containing that greeting.

With this logic in place we can simply add this new Route a.k.a service to our existing web server implementation in our main.rs file

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(routes::greetings::hello_world)
            .service(routes::greetings::greet)
    }).bind(("127.0.0.1", 8080))?
        .run()
        .await
}

We can now simply test this endpoint by using

cargo run

Navigating to your browser and using the following query string in the address bar, or as in my case, make use of http requests, like the one built into Rust Rover

< {%
    request.variables.set("firstName", "Gary");
    request.variables.set("lastName", "Woodfine");
%}
GET {{host}}/greet?first_name={{firstName}}&last_name={{lastName}}

### 
GET http://localhost:8080/greet?first_name={{firstName}}&last_name={{lastName}}

HTTP/1.1 200 OK
content-length: 21
date: Tue, 01 Oct 2024 19:50:54 GMT

Hello Gary Woodfine !

Response code: 200 (OK); Time: 3ms (3 ms); Content length: 21 bytes (21 B)

Conclusion

In this continuation of our series on building a web API with Rust, we delved deeper into using the Actix-web crate and explored adding functionalities through additional crates like Serde. By creating a new endpoint that personalizes greetings, we demonstrated how to handle query parameters and serialize/deserialize data in Rust.

This part solidifies our understanding of building RESTful APIs in Rust, focusing on practical examples and real-world scenarios. We saw firsthand how Rust's features, such as traits and async functions, contribute to efficient and safe web development.

While the examples may seem simple, they lay a crucial foundation for more complex applications. As you continue experimenting with Rust and Actix-web, you'll find numerous opportunities to optimize and expand your APIs.

Stay tuned for the next part of this series, where we will explore more advanced topics and further enhance our building Web API's with rust capabilities.

Gary Woodfine
Gary Woodfine

Back-end software engineer

Experienced software developer, specialising in API Development, API Design API Strategy and Web Application Development. Helping companies thrive in the API economy by offering a range of consultancy services, training and mentoring.

Need help starting your API project?

We'll help you with your API First strategy, Design & Development