In this article, you will learn how to integrate Swagger UI, Redoc, and RapiDoc into a Rust API project. Yes, we will be generating three documentation UIs, but don’t worry, the process is straightforward, and we won’t need to write the OpenAPI YAML or JSON configurations manually.

Instead, we will utilize the utoipa crate, which provides simple macros for auto-generating the OpenAPI documentation. This means that utoipa will handle most of the heavy lifting for us, allowing us to concentrate on writing the actual API logic without having to worry about documentation.

Before we proceed, I would like to elaborate on these terms: the OpenAPI Specification and Swagger UI, ReDoc, or RapiDoc. Don’t worry; I won’t overwhelm you with excessive theory.

What do we mean by the OpenAPI Specification? The OpenAPI Specification, formerly known as Swagger, consists of a set of open standards designed for defining and documenting RESTful APIs. It offers a machine-readable format for describing endpoints, request and response data structures, authentication methods, and other API details. By default, it is named openapi.json.

Now, let’s explore Swagger UI, ReDoc, and RapiDoc. These are all tools designed to enhance the documentation and visualization of APIs based on the OpenAPI Specification.

Articles in This Series

  1. Building a Rust API with Unit Testing in Mind
  2. How to Add Swagger UI, Redoc and RapiDoc to a Rust API
  3. JWT Authentication and Authorization in a Rust API using Actix-Web
  4. How to Write Unit Tests for Your Rust API
  5. Dockerizing a Rust API Project: SQL Database and pgAdmin
  6. Deploy Rust App on VPS with GitHub Actions and Docker
How to Add Swagger UI, Redoc and RapiDoc to a Rust API

Running the Rust API Project in Docker

To run the Rust API project with Docker, follow these simple steps:

  1. Download or clone the project from its GitHub repository at https://github.com/wpcodevo/complete-restful-api-in-rust and open the source code in your preferred text editor.
  2. Explore the project files in your text editor to gain an understanding of its components, including database migrations, unit tests, SQLx models, and API endpoints.
  3. Open your code editor’s integrated terminal and execute the command docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d. This command will launch the Rust API, pgAdmin, and Postgres images within separate Docker containers.
  4. Access the Swagger documentation by visiting http://localhost:8000/. Alternatively, you can test endpoints using the Redoc UI at http://localhost:8000/redoc or the Rapidoc UI at http://localhost:8000/rapidoc. These user interfaces provide a convenient way to interact with the API endpoints.

Running the Rust API Project on Your Machine

To run the Rust project on your local machine and execute the accompanying unit tests, follow the steps outlined below:

  • Download or clone the project from its GitHub repository at https://github.com/wpcodevo/complete-restful-api-in-rust and open the source code in your preferred code editor.
  • Start the Postgres and pgAdmin Docker containers by running the command docker-compose -f docker-compose.no_api.yml up -d.
  • Apply the database migrations to the PostgreSQL database by running sqlx migrate run. If you don’t already have the SQLx-CLI installed on your computer, you can do so by running the command cargo install sqlx-cli --no-default-features --features postgres.
  • In the terminal of the root directory, run the command cargo test which will first build the project and run the 49 unit tests that come with the Rust project.
  • Once the tests have been completed successfully, run the command cargo run to start the Actix-web development server.
  • You can access the Swagger documentation by navigating to http://localhost:8000/. Additionally, you have the option to use the Redoc UI available at http://localhost:8000/redoc or the Rapidoc UI accessible through http://localhost:8000/rapidoc. These user interfaces offer a user-friendly approach to interact with the API endpoints and explore their functionality.

Installing the Utopia Swagger Ui Crates

This tutorial is a continuation of the previous article titled “Building a Rust API with Unit Testing in Mind“, where we built the API. To ensure we are all on the same page, please visit the project’s GitHub repository at https://github.com/wpcodevo/complete-restful-api-in-rust, and clone or download the project onto your machine. Open the source code in your preferred text editor and switch the Git branch to restful-api-in-rust.

Let’s kick things off by installing the necessary dependencies. At this stage, our primary focus will be on Utopia and Swagger UI. However, as we proceed, we’ll also integrate Redoc and RapiDoc. To begin, open your terminal and execute the following commands to install the crates:


cargo add utoipa -F "chrono actix_extras"
cargo add utoipa-swagger-ui -F actix-web

  • utoipa – This crate provides straightforward macros that we can use to annotate our code and document its components.
  • utoipa-swagger-ui – It’s a framework designed to serve the OpenAPI documentation, created using the utoipa library, within the Swagger UI.

Adding SwaggerUi to the Rust API

In this section, we will create a simple Actix-Web server with only one endpoint: a health checker endpoint. Throughout the process, we will use the macros provided by the utoipa library and the utoipa-swagger-ui library to serve the Swagger UI. This step is designed to help us become comfortable with the workflow and to understand each step before we dive into documenting our actual API.

It’s worth noting that we will write all the code in the src/main.rs file to keep things simple for now.

Registering the OpenAPI Schema

Now, we need to create the structure of the API response for the health checker endpoint. This requires implementing the Serialize trait provided by the serde library on the struct, enabling its instances to be transformed into the JSON format. Similarly, as we aim to add this struct to the OpenAPI schema, we need to use the ToSchema trait from the utoipa library. Utilizing the ToSchema trait on the struct will register it as an OpenAPI schema.

Below is the structure of the response:


use serde::{Deserialize, Serialize};
use utoipa::ToSchema;

#[derive(Serialize, Deserialize, ToSchema)]
pub struct Response {
    pub status: &'static str,
    pub message: String,
}

Registering the API Handler as OpenAPI Path

Moving forward, our next step is to create the Actix-Web route handler for the health checker endpoint and then annotate it with the path attribute macro provided by utoipa. This annotation allows us to register the handler as an OpenAPI path.

Here’s the code snippet that demonstrates this process:


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

#[utoipa::path(
    get,
    path = "/api/healthchecker",
    tag = "Health Checker Endpoint",
    responses(
        (status = 200, description= "Authenticated User", body = Response),       
    )
)]
#[get("/api/healthchecker")]
async fn health_checker_handler() -> impl Responder {
    HttpResponse::Ok().json(Response {
        status: "success",
        message: "Complete Restful API in Rust".to_string(),
    })
}

If you’re interested in learning more about the path macro and the various attributes it supports, simply hover your cursor over it. This action will trigger a popup displaying the relevant documentation for you to explore.

Generating the OpenApi Base Object

At this point, we have created the API route handler and the response struct, both of which have been annotated with their respective macros. The next step involves bringing these components together through the utilization of the OpenApi derive proc macro. This macro’s purpose is to facilitate the generation of the API documentation


use utoipa::OpenApi;

#[derive(OpenApi)]
#[openapi(
    paths(
        health_checker_handler
    ),
    components(
        schemas(Response)
    ),
    tags(
        (name = "Rust REST API", description = "Authentication in Rust Endpoints")
    ),
)]
struct ApiDoc;

Serving the Swagger Ui via a Web Server

Now it’s time to utilize the utoipa_swagger_ui crate to serve the OpenAPI documentation that was generated by utoipa via Swagger UI.


use utoipa_swagger_ui::SwaggerUi;

#[actix_web::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let openapi = ApiDoc::openapi();

    HttpServer::new(move || {
        App::new()
            .wrap(Logger::default())
            .service(health_checker_handler)
            .service(
                SwaggerUi::new("/swagger-ui/{_:.*}").url("/api-docs/openapi.json", openapi.clone()),
            )
    })
    .bind(("0.0.0.0", 8000))?
    .run()
    .await?;

    Ok(())
}

We use the ApiDoc::openapi() method to generate the OpenAPI documentation and then assign the result to the openapi variable. Subsequently, we register a new Actix-Web service using the SwaggerUi::new method. This method exposes the Swagger UI at the endpoint /swagger-ui/, and we call the .url() method on it to expose the derived OpenAPI documentation.

The Complete Code

Let’s take a look at the complete code for implementing the Swagger UI integration in your Actix-Web project. If you’ve followed the instructions provided earlier, your src/main.rs file should resemble the example below:

src/main.rs


use actix_web::{get, middleware::Logger, App, HttpResponse, HttpServer, Responder};

use serde::{Deserialize, Serialize};
use utoipa::{OpenApi, ToSchema};
use utoipa_swagger_ui::SwaggerUi;

#[derive(OpenApi)]
#[openapi(
    paths(
        health_checker_handler
    ),
    components(
        schemas(Response)
    ),
    tags(
        (name = "Rust REST API", description = "Authentication in Rust Endpoints")
    ),
)]
struct ApiDoc;

#[actix_web::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    if std::env::var_os("RUST_LOG").is_none() {
        std::env::set_var("RUST_LOG", "actix_web=info");
    }

    env_logger::init();

    println!(
        "{}",
        format!("Server is running on http://localhost:{}", 8000)
    );

    let openapi = ApiDoc::openapi();

    HttpServer::new(move || {
        App::new()
            .wrap(Logger::default())
            .service(health_checker_handler)
            .service(
                SwaggerUi::new("/swagger-ui/{_:.*}").url("/api-docs/openapi.json", openapi.clone()),
            )
    })
    .bind(("0.0.0.0", 8000))?
    .run()
    .await?;

    Ok(())
}

#[derive(Serialize, Deserialize, ToSchema)]
pub struct Response {
    pub status: &'static str,
    pub message: String,
}

#[utoipa::path(
    get,
    path = "/api/healthchecker",
    tag = "Health Checker Endpoint",
    responses(
        (status = 200, description= "Authenticated User", body = Response),       
    )
)]
#[get("/api/healthchecker")]
async fn health_checker_handler() -> impl Responder {
    HttpResponse::Ok().json(Response {
        status: "success",
        message: "Complete Restful API in Rust".to_string(),
    })
}

With the code in place, execute the command cargo run to start the Actix-Web HTTP server. Once the server is operational, access the Swagger Docs through the URL http://localhost:8000/swagger-ui/. Be sure to include the trailing slash in the URL.

Visit the Swagger Docs for the Actix-Web API with only the Health Checker Endpoint

If you successfully view the Swagger UI, it indicates that you’ve followed the instructions correctly. Within the Swagger UI, you can find the health checker endpoint, along with the Response struct included in the available schemas. To test the endpoint, expand the /api/healthchecker path, click the “Try it out” button, and then proceed to “Execute“. In just a matter of milliseconds, you’ll receive the API response.

Send a Reqeust to the Health Checker Endpoint From the Swagger Docs

Passing JWT Bearer Token in Swagger Ui

Let’s explore how to include a JWT Bearer Token in the Swagger UI when dealing with a protected endpoint, such as our health checker endpoint, which requires a valid JWT as a Bearer token in the Authorization header. This process is quite straightforward using the utoipa crate, which offers a Modify trait for runtime modifications to the OpenAPI specification. This allows us to add a custom JWT SecuritySchema to the OpenApi.

Here’s the code that demonstrates this process:


use utoipa::{
    openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme},
    Modify
};

#[derive(OpenApi)]
#[openapi(
    paths(
        health_checker_handler
    ),
    components(
        schemas(Response)
    ),
    tags(
        (name = "Rust REST API", description = "Authentication in Rust Endpoints")
    ),
    modifiers(&SecurityAddon)
)]
struct ApiDoc;

struct SecurityAddon;

impl Modify for SecurityAddon {
    fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
        let components = openapi.components.as_mut().unwrap();
        components.add_security_scheme(
            "token",
            SecurityScheme::Http(
                HttpBuilder::new()
                    .scheme(HttpAuthScheme::Bearer)
                    .bearer_format("JWT")
                    .build(),
            ),
        )
    }
}

With this addition, your src/main.rs file should now have the following code:

src/main.rs


use actix_web::{get, middleware::Logger, App, HttpResponse, HttpServer, Responder};

use serde::{Deserialize, Serialize};
use utoipa::{
    openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme},
    Modify, OpenApi, ToSchema,
};
use utoipa_swagger_ui::SwaggerUi;

#[derive(OpenApi)]
#[openapi(
    paths(
        health_checker_handler
    ),
    components(
        schemas(Response)
    ),
    tags(
        (name = "Rust REST API", description = "Authentication in Rust Endpoints")
    ),
    modifiers(&SecurityAddon)
)]
struct ApiDoc;

struct SecurityAddon;

impl Modify for SecurityAddon {
    fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
        let components = openapi.components.as_mut().unwrap();
        components.add_security_scheme(
            "token",
            SecurityScheme::Http(
                HttpBuilder::new()
                    .scheme(HttpAuthScheme::Bearer)
                    .bearer_format("JWT")
                    .build(),
            ),
        )
    }
}

#[actix_web::main]
async fn main() -> Result<(), Box> {
    if std::env::var_os("RUST_LOG").is_none() {
        std::env::set_var("RUST_LOG", "actix_web=info");
    }

    env_logger::init();

    println!(
        "{}",
        format!("Server is running on http://localhost:{}", 8000)
    );

    let openapi = ApiDoc::openapi();

    HttpServer::new(move || {
        App::new()
            .wrap(Logger::default())
            .service(health_checker_handler)
            .service(
                SwaggerUi::new("/swagger-ui/{_:.*}").url("/api-docs/openapi.json", openapi.clone()),
            )
    })
    .bind(("0.0.0.0", 8000))?
    .run()
    .await?;

    Ok(())
}

#[derive(Serialize, Deserialize, ToSchema)]
pub struct Response {
    pub status: &'static str,
    pub message: String,
}

#[utoipa::path(
    get,
    path = "/api/healthchecker",
    tag = "Health Checker Endpoint",
    responses(
        (status = 200, description= "Authenticated User", body = Response),       
    ),
    security(
       ("token" = [])
   )
)]
#[get("/api/healthchecker")]
async fn health_checker_handler() -> impl Responder {
    HttpResponse::Ok().json(Response {
        status: "success",
        message: "Complete Restful API in Rust".to_string(),
    })
}

By adding the security field to the utoipa::path macro, the Swagger UI will indicate that the endpoint is protected and requires a JWT named “token” to be included in the request. After running cargo run and refreshing the Swagger UI, you should see an “Authorize” button above the OpenAI tag on the right side. On the /api/healthchecker path, you’ll see a padlock icon at the right end.

Protecting the Health Checker Endpoint using JWT Authentication on the Swagger UI

Clicking the “Authorize” button opens a popup where you can input the JWT and authenticate. Note that there isn’t an endpoint yet to generate the JWT or a JWT middleware guard in this example. However, this demonstrates how you can include JWT from the Swagger UI.

Input the JWT Token as Bearer on the Swagger Ui

Registering All the DTOs as OpenAPI Schemas

With a solid grasp of how to generate and display OpenAPI documentation for a basic Actix-Web server using Swagger UI, let’s take things to the next level by documenting our actual API project. To kick off this process, our first step is to convert all of our data transfer objects (DTOs) into OpenAPI schemas.

To achieve this, open the src/dtos.rs file and incorporate the ToSchema macro provided by utoipa into the existing list of #[derive()] attributes associated with the DTOs. For the RequestQueryDto struct, we’ll use the IntoParams macro, which generates path parameters from the struct’s fields.

Here’s how the modified code should look:


use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use utoipa::{IntoParams, ToSchema};
use validator::Validate;

use crate::models::User;

#[derive(Validate, Debug, Default, Clone, Serialize, Deserialize, ToSchema)]
pub struct RegisterUserDto {}


#[derive(Validate, Debug, Default, Clone, Serialize, Deserialize, ToSchema)]
pub struct LoginUserDto {}


#[derive(Serialize, Deserialize, Validate, IntoParams)]
pub struct RequestQueryDto {}


#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct FilterUserDto {}


#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct UserData {}


#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct UserResponseDto {}


#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct UserListResponseDto {}


#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct UserLoginResponseDto {}


#[derive(Serialize, Deserialize, ToSchema)]
pub struct Response {}

Adding the ToSchema macro to the structs will enable them to be included in the list of OpenAPI schemas. It’s important to note that these modifications are necessary to prevent encountering errors when incorporating the structs into the list of OpenAPI schemas. Following these adjustments, your src/dtos.rs file should appear as follows:

src/dtos.rs


use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use utoipa::{IntoParams, ToSchema};
use validator::Validate;

use crate::models::User;

#[derive(Validate, Debug, Default, Clone, Serialize, Deserialize, ToSchema)]
pub struct RegisterUserDto {
    #[validate(length(min = 1, message = "Name is required"))]
    pub name: String,
    #[validate(
        length(min = 1, message = "Email is required"),
        email(message = "Email is invalid")
    )]
    pub email: String,
    #[validate(
        length(min = 1, message = "Password is required"),
        length(min = 6, message = "Password must be at least 6 characters")
    )]
    pub password: String,
    #[validate(
        length(min = 1, message = "Please confirm your password"),
        must_match(other = "password", message = "Passwords do not match")
    )]
    #[serde(rename = "passwordConfirm")]
    pub password_confirm: String,
}

#[derive(Validate, Debug, Default, Clone, Serialize, Deserialize, ToSchema)]
pub struct LoginUserDto {
    #[validate(
        length(min = 1, message = "Email is required"),
        email(message = "Email is invalid")
    )]
    pub email: String,
    #[validate(
        length(min = 1, message = "Password is required"),
        length(min = 6, message = "Password must be at least 6 characters")
    )]
    pub password: String,
}

#[derive(Serialize, Deserialize, Validate, IntoParams)]
pub struct RequestQueryDto {
    #[validate(range(min = 1))]
    pub page: Option<usize>,
    #[validate(range(min = 1, max = 50))]
    pub limit: Option<usize>,
}

#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct FilterUserDto {
    pub id: String,
    pub name: String,
    pub email: String,
    pub role: String,
    pub photo: String,
    pub verified: bool,
    #[serde(rename = "createdAt")]
    pub created_at: DateTime<Utc>,
    #[serde(rename = "updatedAt")]
    pub updated_at: DateTime<Utc>,
}

impl FilterUserDto {
    pub fn filter_user(user: &User) -> Self {
        FilterUserDto {
            id: user.id.to_string(),
            email: user.email.to_owned(),
            name: user.name.to_owned(),
            photo: user.photo.to_owned(),
            role: user.role.to_str().to_string(),
            verified: user.verified,
            created_at: user.created_at.unwrap(),
            updated_at: user.updated_at.unwrap(),
        }
    }

    pub fn filter_users(users: &[User]) -> Vec<FilterUserDto> {
        users.iter().map(FilterUserDto::filter_user).collect()
    }
}

#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct UserData {
    pub user: FilterUserDto,
}

#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct UserResponseDto {
    pub status: String,
    pub data: UserData,
}

#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct UserListResponseDto {
    pub status: String,
    pub users: Vec<FilterUserDto>,
    pub results: usize,
}

#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct UserLoginResponseDto {
    pub status: String,
    pub token: String,
}

#[derive(Serialize, Deserialize, ToSchema)]
pub struct Response {
    pub status: &'static str,
    pub message: String,
}

Registering the Authentication API Handlers as OpenAPI Paths

Let’s proceed to register all the authentication API handlers as OpenAPI paths. This involves editing the src/scopes/auth.rs file and utilizing the #[utoipa::path()] macro on the respective API handlers, providing the necessary arguments and attributes. Below are examples of code snippets that demonstrate how to register these handlers as OpenAPI paths:


#[utoipa::path(
    post,
    path = "/api/auth/register",
    tag = "Register Account Endpoint",
    request_body(content = RegisterUserDto, description = "Credentials to create account", example = json!({"email": "johndoe@example.com","name": "John Doe","password": "password123","passwordConfirm": "password123"})),
    responses(
        (status=201, description= "Account created successfully", body= UserResponseDto ),
        (status=400, description= "Validation Errors", body= Response),
        (status=409, description= "User with email already exists", body= Response),
        (status=500, description= "Internal Server Error", body= Response ),
    )
)]
pub async fn register(
    app_state: web::Data<AppState>,
    body: web::Json<RegisterUserDto>,
) -> Result<HttpResponse, HttpError> {}

#[utoipa::path(
    post,
    path = "/api/auth/login",
    tag = "Login Endpoint",
    request_body(content = LoginUserDto, description = "Credentials to log in to your account", example = json!({"email": "johndoe@example.com","password": "password123"})),
    responses(
        (status=200, description= "Login successfull", body= UserLoginResponseDto ),
        (status=400, description= "Validation Errors", body= Response ),
        (status=500, description= "Internal Server Error", body= Response ),
    )
)]
pub async fn login(
    app_state: web::Data<AppState>,
    body: web::Json<LoginUserDto>,
) -> Result<HttpResponse, HttpError> {}

#[utoipa::path(
    post,
    path = "/api/auth/logout",
    tag = "Logout Endpoint",
    responses(
        (status=200, description= "Logout successfull" ),
        (status=400, description= "Validation Errors", body= Response ),
        (status=401, description= "Unauthorize Error", body= Response),
        (status=500, description= "Internal Server Error", body= Response ),
    ),
    security(
       ("token" = [])
   )
)]
pub async fn logout() -> impl Responder {}

In these code examples, you’ll notice attributes being passed to the #[utoipa::path()] macro, including various responses, request body types, and in our case, the operation type is only the POST method. Notably, the security attribute is set on the logout handler to indicate that this route will be protected by a JWT middleware. After implementing these changes, your src/scopes/auth.rs file should be updated with the following code.

src/scopes/auth.rs


use actix_web::{
    cookie::time::Duration as ActixWebDuration, cookie::Cookie, web, HttpResponse, Responder, Scope,
};
use serde_json::json;
use validator::Validate;

use crate::{
    db::UserExt,
    dtos::{
        FilterUserDto, LoginUserDto, RegisterUserDto, UserData, UserLoginResponseDto,
        UserResponseDto,
    },
    error::{ErrorMessage, HttpError},
    extractors::auth::RequireAuth,
    utils::{password, token},
    AppState,
};

pub fn auth_scope() -> Scope {
    web::scope("/api/auth")
        .route("/register", web::post().to(register))
        .route("/login", web::post().to(login))
        .route("/logout", web::post().to(logout).wrap(RequireAuth))
}

#[utoipa::path(
    post,
    path = "/api/auth/register",
    tag = "Register Account Endpoint",
    request_body(content = RegisterUserDto, description = "Credentials to create account", example = json!({"email": "johndoe@example.com","name": "John Doe","password": "password123","passwordConfirm": "password123"})),
    responses(
        (status=201, description= "Account created successfully", body= UserResponseDto ),
        (status=400, description= "Validation Errors", body= Response),
        (status=409, description= "User with email already exists", body= Response),
        (status=500, description= "Internal Server Error", body= Response ),
    )
)]
pub async fn register(
    app_state: web::Data<AppState>,
    body: web::Json<RegisterUserDto>,
) -> Result<HttpResponse, HttpError> {
    body.validate()
        .map_err(|e| HttpError::bad_request(e.to_string()))?;

    let hashed_password =
        password::hash(&body.password).map_err(|e| HttpError::server_error(e.to_string()))?;

    let result = app_state
        .db_client
        .save_user(&body.name, &body.email, &hashed_password)
        .await;

    match result {
        Ok(user) => Ok(HttpResponse::Created().json(UserResponseDto {
            status: "success".to_string(),
            data: UserData {
                user: FilterUserDto::filter_user(&user),
            },
        })),
        Err(sqlx::Error::Database(db_err)) => {
            if db_err.is_unique_violation() {
                Err(HttpError::unique_constraint_voilation(
                    ErrorMessage::EmailExist,
                ))
            } else {
                Err(HttpError::server_error(db_err.to_string()))
            }
        }
        Err(e) => Err(HttpError::server_error(e.to_string())),
    }
}

#[utoipa::path(
    post,
    path = "/api/auth/login",
    tag = "Login Endpoint",
    request_body(content = LoginUserDto, description = "Credentials to log in to your account", example = json!({"email": "johndoe@example.com","password": "password123"})),
    responses(
        (status=200, description= "Login successfull", body= UserLoginResponseDto ),
        (status=400, description= "Validation Errors", body= Response ),
        (status=500, description= "Internal Server Error", body= Response ),
    )
)]
pub async fn login(
    app_state: web::Data<AppState>,
    body: web::Json<LoginUserDto>,
) -> Result<HttpResponse, HttpError> {
    body.validate()
        .map_err(|e| HttpError::bad_request(e.to_string()))?;

    let result = app_state
        .db_client
        .get_user(None, None, Some(&body.email))
        .await
        .map_err(|e| HttpError::server_error(e.to_string()))?;

    let user = result.ok_or(HttpError::unauthorized(ErrorMessage::WrongCredentials))?;

    let password_matches = password::compare(&body.password, &user.password)
        .map_err(|_| HttpError::unauthorized(ErrorMessage::WrongCredentials))?;

    if password_matches {
        let token = token::create_token(
            &user.id.to_string(),
            &app_state.env.jwt_secret.as_bytes(),
            app_state.env.jwt_maxage,
        )
        .map_err(|e| HttpError::server_error(e.to_string()))?;
        let cookie = Cookie::build("token", token.to_owned())
            .path("/")
            .max_age(ActixWebDuration::new(60 * &app_state.env.jwt_maxage, 0))
            .http_only(true)
            .finish();

        Ok(HttpResponse::Ok()
            .cookie(cookie)
            .json(UserLoginResponseDto {
                status: "success".to_string(),
                token,
            }))
    } else {
        Err(HttpError::unauthorized(ErrorMessage::WrongCredentials))
    }
}

#[utoipa::path(
    post,
    path = "/api/auth/logout",
    tag = "Logout Endpoint",
    responses(
        (status=200, description= "Logout successfull" ),
        (status=400, description= "Validation Errors", body= Response ),
        (status=401, description= "Unauthorize Error", body= Response),
        (status=500, description= "Internal Server Error", body= Response ),
    ),
    security(
       ("token" = [])
   )
)]
pub async fn logout() -> impl Responder {
    let cookie = Cookie::build("token", "")
        .path("/")
        .max_age(ActixWebDuration::new(-1, 0))
        .http_only(true)
        .finish();

    HttpResponse::Ok()
        .cookie(cookie)
        .json(json!({"status": "success"}))
}

Registering the User-Related Handlers as OpenAPI Paths

Next, let’s proceed to register the user-related route handlers as OpenAPI paths. Open the src/scopes/users.rs file and add the #[utoipa::path()] macro to the route handlers based on the examples provided below:


#[utoipa::path(
    get,
    path = "/api/users/me",
    tag = "Get Authenticated User Endpoint",
    responses(
        (status = 200, description= "Authenticated User", body = UserResponseDto),
        (status= 500, description= "Internal Server Error", body = Response )
       
    ),
    security(
       ("token" = [])
   )
)]
async fn get_me(
    req: HttpRequest,
    app_state: web::Data<AppState>,
) -> Result<HttpResponse, HttpError> {}

#[utoipa::path(
    get,
    path = "/api/users",
    tag = "Get All Users Endpoint",
    params(
        RequestQueryDto
    ),
    responses(
        (status = 200, description= "All Users", body = [UserResponseDto]),
        (status=401, description= "Authentication Error", body= Response),
        (status=403, description= "Permission Denied Error", body= Response),
        (status= 500, description= "Internal Server Error", body = Response )
       
    ),
    security(
       ("token" = [])
   )
)]
pub async fn get_users(
    query: web::Query<RequestQueryDto>,
    app_state: web::Data<AppState>,
) -> Result<HttpResponse, HttpError> {}

Observe that we have utilized the get method as the operation type for both route handlers. Since both handlers are protected by a JWT middleware, we’ve set the security attribute accordingly. For the get_users function, which accepts query parameters, we’ve included the RequestQueryDto struct within the params attribute. This struct’s details will be visible in the Swagger UI, providing insight into the parameters that the endpoint can accept.

After implementing these changes, your src/scopes/users.rs file should adopt the following format:

src/scopes/users.rs


use actix_web::{web, HttpMessage, HttpRequest, HttpResponse, Scope};
use validator::Validate;

use crate::{
    dtos::{FilterUserDto, UserData, UserResponseDto, RequestQueryDto, UserListResponseDto},
    error::HttpError,
    extractors::auth::{RequireAuth, RequireOnlyAdmin},
    AppState, db::UserExt,
};

pub fn users_scope() -> Scope {
    web::scope("/api/users")
    .route("", web::get().to(get_users).wrap(RequireOnlyAdmin))
    .route("/me", web::get().to(get_me).wrap(RequireAuth))
}

#[utoipa::path(
    get,
    path = "/api/users/me",
    tag = "Get Authenticated User Endpoint",
    responses(
        (status = 200, description= "Authenticated User", body = UserResponseDto),
        (status= 500, description= "Internal Server Error", body = Response )
       
    ),
    security(
       ("token" = [])
   )
)]
async fn get_me(
    req: HttpRequest,
    app_state: web::Data<AppState>,
) -> Result<HttpResponse, HttpError> {
    let ext = req.extensions();
    let user_id = ext.get::<uuid::Uuid>();

   let user = app_state.db_client.get_user(Some(*user_id.unwrap()), None, None).await.map_err(|e| HttpError::server_error(e.to_string()))?;

    Ok(HttpResponse::Ok().json(UserResponseDto {
        status: "success".to_string(),
        data: UserData {
            user: FilterUserDto::filter_user(&user.unwrap()),
        },
    }))
}

#[utoipa::path(
    get,
    path = "/api/users",
    tag = "Get All Users Endpoint",
    params(
        RequestQueryDto
    ),
    responses(
        (status = 200, description= "All Users", body = [UserResponseDto]),
        (status=401, description= "Authentication Error", body= Response),
        (status=403, description= "Permission Denied Error", body= Response),
        (status= 500, description= "Internal Server Error", body = Response )
       
    ),
    security(
       ("token" = [])
   )
)]
pub async fn get_users(
    query: web::Query<RequestQueryDto>,
    app_state: web::Data<AppState>,
) -> Result<HttpResponse, HttpError> {
    let query_params: RequestQueryDto = query.into_inner();

    query_params
        .validate()
        .map_err(|e| HttpError::bad_request(e.to_string()))?;

    let page = query_params.page.unwrap_or(1); 
    let limit = query_params.limit.unwrap_or(10);

    let users = app_state.db_client.get_users(page as u32, limit)
        .await.map_err(|e| HttpError::server_error(e.to_string()))?;


    Ok(HttpResponse::Ok().json(UserListResponseDto {
        status: "success".to_string(),
        users: FilterUserDto::filter_users(&users),
        results: users.len()
    }))
}

Generating the OpenAPI Object and Serving the Swagger Ui

Now, let’s proceed to add the DTOs to the schemas attribute of the #[openapi()] macro. This ensures they are registered as OpenAPI schemas. Additionally, we need to incorporate all the route handlers into the paths() attribute, enabling their registration as OpenAPI paths. Open the src/main.rs file and implement the necessary modifications according to the provided code snippets:

src/main.rs


mod config;
mod db;
mod dtos;
mod error;
mod extractors;
mod models;
mod scopes;
mod utils;

use actix_cors::Cors;
use actix_web::{
    get, http::header, middleware::Logger, web, App, HttpResponse, HttpServer, Responder,
};
use config::Config;
use db::DBClient;
use dotenv::dotenv;
use dtos::{
    FilterUserDto, LoginUserDto, RegisterUserDto, Response, UserData, UserListResponseDto,
    UserLoginResponseDto, UserResponseDto,
};
use models::UserRole;
use scopes::{auth, users};
use sqlx::postgres::PgPoolOptions;
use utoipa::{
    openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme},
    Modify, OpenApi,
};
use utoipa_swagger_ui::SwaggerUi;

#[derive(Debug, Clone)]
pub struct AppState {
    pub env: Config,
    pub db_client: DBClient,
}

#[derive(OpenApi)]
#[openapi(
    paths(
        auth::login,auth::logout,auth::register, users::get_me, users::get_users, health_checker_handler
    ),
    components(
        schemas(UserData,FilterUserDto,LoginUserDto,RegisterUserDto,UserResponseDto,UserLoginResponseDto,Response,UserListResponseDto,UserRole)
    ),
    tags(
        (name = "Rust REST API", description = "Authentication in Rust Endpoints")
    ),
    modifiers(&SecurityAddon)
)]
struct ApiDoc;

struct SecurityAddon;

impl Modify for SecurityAddon {
    fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
        let components = openapi.components.as_mut().unwrap();
        components.add_security_scheme(
            "token",
            SecurityScheme::Http(
                HttpBuilder::new()
                    .scheme(HttpAuthScheme::Bearer)
                    .bearer_format("JWT")
                    .build(),
            ),
        )
    }
}

#[actix_web::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    openssl_probe::init_ssl_cert_env_vars();
    if std::env::var_os("RUST_LOG").is_none() {
        std::env::set_var("RUST_LOG", "actix_web=info");
    }

    dotenv().ok();
    env_logger::init();

    let config = Config::init();

    let pool = PgPoolOptions::new()
        .max_connections(10)
        .connect(&config.database_url)
        .await?;

    match sqlx::migrate!("./migrations").run(&pool).await {
        Ok(_) => println!("Migrations executed successfully."),
        Err(e) => eprintln!("Error executing migrations: {}", e),
    };

    let db_client = DBClient::new(pool);
    let app_state: AppState = AppState {
        env: config.clone(),
        db_client,
    };

    println!(
        "{}",
        format!("Server is running on http://localhost:{}", config.port)
    );

    let openapi = ApiDoc::openapi();

    HttpServer::new(move || {
        let cors = Cors::default()
            .allowed_origin("http://localhost:3000")
            .allowed_origin("http://localhost:8000")
            .allowed_origin("https://rust.codevoweb.com")
            .allowed_methods(vec!["GET", "POST"])
            .allowed_headers(vec![
                header::CONTENT_TYPE,
                header::AUTHORIZATION,
                header::ACCEPT,
            ])
            .supports_credentials();

        App::new()
            .app_data(web::Data::new(app_state.clone()))
            .wrap(cors)
            .wrap(Logger::default())
            .service(scopes::auth::auth_scope())
            .service(scopes::users::users_scope())
            .service(health_checker_handler)
            .service(SwaggerUi::new("/{_:.*}").url("/api-docs/openapi.json", openapi.clone()))
    })
    .bind(("0.0.0.0", config.port))?
    .run()
    .await?;

    Ok(())
}

#[utoipa::path(
    get,
    path = "/api/healthchecker",
    tag = "Health Checker Endpoint",
    responses(
        (status = 200, description= "Authenticated User", body = Response),       
    )
)]
#[get("/api/healthchecker")]
async fn health_checker_handler() -> impl Responder {
    const MESSAGE: &str = "Complete Restful API in Rust";

    HttpResponse::Ok().json(serde_json::json!({"status": "success", "message": MESSAGE}))
}

An important alteration made in the main.rs file is making the Swagger UI accessible from the root URL. We’ve also allowed the root URL in the CORS configurations to ensure requests made from the Swagger UI aren’t blocked.

Following these changes, run cargo run to launch the Actix-Web HTTP server, and navigate to the root URL at http://localhost:8000 to explore the Swagger Docs for our API. There, test the different endpoints to verify their correct functionality and compare the responses against the ones specified in the OpenAPI schemas.

Swagger UI for the Rust API

Adding Redoc and Rapidoc to the API

Let’s wrap things up by integrating ReDoc and RapiDoc into our API. This is a straightforward process – we’ll just need to install their respective crates and use them to serve their user interfaces based on the OpenAPI docs generated by the utoipa crate. To get started, open your terminal and execute the following commands to install the crates:


cargo add utoipa-rapidoc -F actix-web
cargo add utoipa-redoc -F actix-web

  • utoipa-rapidoc – This crate acts as a bridge between utoipa and RapiDoc OpenAPI visualizer.
  • utoipa-redoc – This crate functions as a bridge between utoipa and Redoc OpenAPI visualizer.

Once the crates are installed, import them into the main.rs file and register them as Actix-Web services, as illustrated in the code snippet below:


use utoipa_rapidoc::RapiDoc;
use utoipa_redoc::{Redoc, Servable};

#[actix_web::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let openapi = ApiDoc::openapi();

    HttpServer::new(move || {
        App::new()
            .service(Redoc::with_url("/redoc", openapi.clone()))
            .service(RapiDoc::new("/api-docs/openapi.json").path("/rapidoc"))
    })
    .bind(("0.0.0.0", config.port))?
    .run()
    .await?;

    Ok(())
}

Note that these services should be placed before the Swagger UI service since the Swagger UI catches requests made from the root URL.

Once you’ve implemented these changes, your src/main.rs file should resemble the code provided below.

src/main.rs


mod config;
mod db;
mod dtos;
mod error;
mod extractors;
mod models;
mod scopes;
mod utils;

use actix_cors::Cors;
use actix_web::{
    get, http::header, middleware::Logger, web, App, HttpResponse, HttpServer, Responder,
};
use config::Config;
use db::DBClient;
use dotenv::dotenv;
use dtos::{
    FilterUserDto, LoginUserDto, RegisterUserDto, Response, UserData, UserListResponseDto,
    UserLoginResponseDto, UserResponseDto,
};
use models::UserRole;
use scopes::{auth, users};
use sqlx::postgres::PgPoolOptions;
use utoipa::{
    openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme},
    Modify, OpenApi,
};
use utoipa_rapidoc::RapiDoc;
use utoipa_redoc::{Redoc, Servable};
use utoipa_swagger_ui::SwaggerUi;

#[derive(Debug, Clone)]
pub struct AppState {
    pub env: Config,
    pub db_client: DBClient,
}

#[derive(OpenApi)]
#[openapi(
    paths(
        auth::login,auth::logout,auth::register, users::get_me, users::get_users, health_checker_handler
    ),
    components(
        schemas(UserData,FilterUserDto,LoginUserDto,RegisterUserDto,UserResponseDto,UserLoginResponseDto,Response,UserListResponseDto,UserRole)
    ),
    tags(
        (name = "Rust REST API", description = "Authentication in Rust Endpoints")
    ),
    modifiers(&SecurityAddon)
)]
struct ApiDoc;

struct SecurityAddon;

impl Modify for SecurityAddon {
    fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
        let components = openapi.components.as_mut().unwrap();
        components.add_security_scheme(
            "token",
            SecurityScheme::Http(
                HttpBuilder::new()
                    .scheme(HttpAuthScheme::Bearer)
                    .bearer_format("JWT")
                    .build(),
            ),
        )
    }
}

#[actix_web::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    openssl_probe::init_ssl_cert_env_vars();
    if std::env::var_os("RUST_LOG").is_none() {
        std::env::set_var("RUST_LOG", "actix_web=info");
    }

    dotenv().ok();
    env_logger::init();

    let config = Config::init();

    let pool = PgPoolOptions::new()
        .max_connections(10)
        .connect(&config.database_url)
        .await?;

    match sqlx::migrate!("./migrations").run(&pool).await {
        Ok(_) => println!("Migrations executed successfully."),
        Err(e) => eprintln!("Error executing migrations: {}", e),
    };

    let db_client = DBClient::new(pool);
    let app_state: AppState = AppState {
        env: config.clone(),
        db_client,
    };

    println!(
        "{}",
        format!("Server is running on http://localhost:{}", config.port)
    );

    let openapi = ApiDoc::openapi();

    HttpServer::new(move || {
        let cors = Cors::default()
            .allowed_origin("http://localhost:3000")
            .allowed_origin("http://localhost:8000")
            .allowed_origin("https://rust.codevoweb.com")
            .allowed_methods(vec!["GET", "POST"])
            .allowed_headers(vec![
                header::CONTENT_TYPE,
                header::AUTHORIZATION,
                header::ACCEPT,
            ])
            .supports_credentials();

        App::new()
            .app_data(web::Data::new(app_state.clone()))
            .wrap(cors)
            .wrap(Logger::default())
            .service(scopes::auth::auth_scope())
            .service(scopes::users::users_scope())
            .service(health_checker_handler)
            .service(Redoc::with_url("/redoc", openapi.clone()))
            .service(RapiDoc::new("/api-docs/openapi.json").path("/rapidoc"))
            .service(SwaggerUi::new("/{_:.*}").url("/api-docs/openapi.json", openapi.clone()))
    })
    .bind(("0.0.0.0", config.port))?
    .run()
    .await?;

    Ok(())
}

#[utoipa::path(
    get,
    path = "/api/healthchecker",
    tag = "Health Checker Endpoint",
    responses(
        (status = 200, description= "Authenticated User", body = Response),       
    )
)]
#[get("/api/healthchecker")]
async fn health_checker_handler() -> impl Responder {
    const MESSAGE: &str = "Complete Restful API in Rust";

    HttpResponse::Ok().json(serde_json::json!({"status": "success", "message": MESSAGE}))
}

#[cfg(test)]
mod tests {
    use super::*;
    use actix_web::{http::StatusCode, test, App};

    #[actix_web::test]
    async fn test_health_checker_handler() {
        let app = test::init_service(App::new().service(health_checker_handler)).await;

        let req = test::TestRequest::get()
            .uri("/api/healthchecker")
            .to_request();
        let resp = test::call_service(&app, req).await;

        assert_eq!(resp.status(), StatusCode::OK);

        let body = test::read_body(resp).await;
        let expected_json =
            serde_json::json!({"status": "success", "message": "Complete Restful API in Rust"});

        assert_eq!(body, serde_json::to_string(&expected_json).unwrap());
    }
}

With this setup in place, save your modifications and restart the Actix-Web HTTP server.

To view the ReDoc documentation for our API, open your browser and navigate to http://localhost:8000/redoc. This will display the ReDoc interface, where you can explore and interact with your API’s documentation.

View the ReDoc UI to Test the API Endpoints

Additionally, to access RapiDoc, simply visit http://localhost:8000/rapidoc.

View the RapiDoc UI to Test the API Endpoints

Conclusion

And there you have it! This in-depth tutorial has guided you through the process of documenting your Rust API by seamlessly integrating ReDoc, RapiDoc, and Swagger UI.

I trust that you found this guide both informative and engaging. Should you have any questions or feedback, please don’t hesitate to share your thoughts in the comments section below. I’m here to assist and will promptly respond. Thank you for dedicating your time to this tutorial – I truly appreciate it!