DEV Community

Cover image for Creating Rest APIs with Rust
Daniel Santos
Daniel Santos

Posted on

Creating Rest APIs with Rust

In this series, I'll share the learnings I got developing another personal project, today I bring a brief introduction to building Rest APIs with Rust.

Choosing the framework

Rust has big names in the framework market for building REST APIs: Rocket, Axum, Warp, and Actix. I only tested the first two and ended up opting for Axum.

Hot reload

A feature that I look for whenever possible in my development environment is Hot Reload, with it, every time a file is changed and saved the application restarts, so the cycle of writing-evaluating-refactoring code becomes extremely fast, for Rust, we have cargo watch, I suggest taking a look at the documentation for more details.

Hello, Axum

We will start by starting a new project with Cargo

$ cargo new hello_world --bin
Enter fullscreen mode Exit fullscreen mode

Add the following dependencies to Cargo.toml :

[dependencies]
axum = "0.6"
tokio = { version = "1.22.0", features = ["full"] }
serde = { version = "1.0.149", features = ["derive"] }
Enter fullscreen mode Exit fullscreen mode

In addition to Axum, as already mentioned, we are adding Tokio as our async runtime, and Serde, to handle JSON serialization and deserialization.

I recommend the following posts if you don't know what an async runtime is and why we need one in this scenario

With the dependencies installed, we'll edit the src/main.rs file:

// src/main.rs

use std::{error::Error, net::SocketAddr};

use axum::{
    routing::get,
    Router,
};

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    let addr = SocketAddr::from(([127, 0, 0, 1], 8000));

    let app = Router::new().route("/", get(|| async { "Hello, Axum" }));

    println!("listening on {}", addr);

    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();

    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

Now we can run our application with cargo watch -x run, and it will respond to GET requests on the http://127.0.0.1:8000/ endpoint

curl -X 'GET' \
  'http://127.0.0.1:8000/'
Enter fullscreen mode Exit fullscreen mode
Hello, Axum
Enter fullscreen mode Exit fullscreen mode

On line x we register the GET / endpoint, Axum calls the functions that resolve endpoints as handlers, and handlers can return any structure that implements the IntoResponse trait. We can start the code split by moving the closure into a function:

// add use axum::http::StatusCode;

pub async fn hello_axum() -> impl IntoResponse {
    (StatusCode::OK, "Hello, Axum")
}
Enter fullscreen mode Exit fullscreen mode

On line x, we place our handler:

// add use axum::http::StatusCode;

    let app = Router::new().route("/", get(hello_axum));
Enter fullscreen mode Exit fullscreen mode

Documentation

With an endpoint defined, now it's a good time to set up the API documentation. At this point, I believe that a specification like OpenApi, together with Swagger for its visualization, is a good combo.

To implement them, we'll need some libraries, one responsible for the specification and the other for its presentation, so let's go back to the Cargo.toml file and add utoipa and utoipa-swagger-UI as dependencies:

[dependencies]
axum = "0.6"
tokio = { version = "1.22.0", features = ["full"] }
serde = { version = "1.0.149", features = ["derive"] }
utoipa = { features = ["axum_extras"], version = "2.4.2" }
utoipa-swagger-ui = { features = ["axum"], version = "3.0.1" }
Enter fullscreen mode Exit fullscreen mode

And then, we use Utoipa's path attribute in our handler:

#[utoipa::path(
    get,
    path = "/",
    responses(
        (status = 200, description = "Send a salute from Axum")
    )
)]
pub async fn hello_axum() -> impl IntoResponse {
    (StatusCode::OK, "Hello, Axum")
}
Enter fullscreen mode Exit fullscreen mode

A Rest API can contain many different endpoints, and we need a point where we can unite all these specifications, for that, Utoipa provides the derive macro OpenApi, and by adding it to a struct, we can store all the endpoints of our application through the paths parameter of the #[openapi] attribute:

// add use utoipa::OpenApi;

#[derive(OpenApi)]
#[openapi(paths(hello_axum))]
pub struct ApiDoc;
Enter fullscreen mode Exit fullscreen mode

Once that's done, we can expose the documentation on an endpoint with SwaggerUi, and attach it to our application, using the Router.merge method:

// add use utoipa_swagger_ui::SwaggerUi;

let app = Router::new()
        .route("/", get(hello_axum))
        .merge(SwaggerUi::new("/swagger-ui").url("/api-doc/openapi.json", ApiDoc::openapi()));
Enter fullscreen mode Exit fullscreen mode

And finally, if we go to http://127.0.0.1:8000/swagger-ui/, we'll see the Swagger interface:

Swagger interface, displaying our only endpoint

From Utoipa, we have a range of possibilities, we can define the body and header of the requests, the HTTP codes to be returned, the format of the responses, and so on, it is worth taking some time in the library documentation to extract the maximum of OpenApi.

The post ends here, but in the next one, we will have some improvements, such as connection to a database and integration tests.


Thank you for reading this far, any doubts or questions can be asked in the comments section, if you want to contact me more easily, you can send me a message on Twitter, I am always available there.

Latest comments (1)

Collapse
 
liftoffstudios profile image
Liftoff Studios

Nice Article! I believe actix is also a good web framework that you should try!