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.
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.
tutorials
All code samples, examples and projects for all Threenine articles and tutorials for the threenine.blog
threenine/tutorialsAdding 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.
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 typeimpl Responder
indicates that this function will return a type that implements theResponder
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.
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.