Back to writing

Actix Web Auto Docs with Utoipa & Swagger-UI

Archived from the SubCom engineering blog. Subconscious Compute closed its doors; the guide survives here. Original page. The original Swagger UI screenshots are recovered from that page.
SubCom logo with the label Actix Web · Utoipa on a dark canvas

Post card from the SubCom engineering blog.

Intro

So you’ve made your shiny new server with all these fancy Endpoints. Now comes the question, How do you tell someone what to access? You could write your own documentation, but that would take far too long, and you would be prone to making errors. A much better solution is to generate the docs from the code you’ve already written.

Such is the solution utoipa provides. It describes your endpoints in the OpenAPI standard in JSON, so it can be consumed by a suitable front-end. A front-end like SwaggerUI will display your endpoints so that it can consumed by a user with relative ease. We can bring SwaggerUI onto our server with the utoipa-swagger-ui crate, and it would look like this:

Swagger UI rendering the endpoint list
Swagger UI rendering the endpoint list — recovered from the original page.
Swagger UI executing a request
Swagger UI executing a request — recovered from the original page.

Limitations

The only limitation we’ve come across is that swagger cannot send GET requests with a request body. You’ll have to use query parameters or change it to a POST method.

Actix Web Setup

We’ll being using actix_web but you could just as easily use axum, warp, tide or rocket. The entire project we setup here is available at this Github Repo. This is the structure we’ll be following:

File structure for our actix server
src
├── auth.rs          # authentication extractors
├── handler.rs       # endpoints go here
├── main.rs          # server setup
├── swagger_docs.rs  # swagger setup
└── types.rs         # types w/ some swagger setup

We have the following endpoints setup:

  • GET /v1/ - hello world example
  • GET /v1/auth - authenticated hello world example
  • POST /v1/create - basic post request which returns request
  • DELETE /v1/delete/{email} - basic delete request which returns request

Utoipa Setup

we’ll be adding our 2 crates with actix integration features. Additionally we’ll also enable debug-embed in utoipa-swagger-ui so the docs endpoint is included in debug builds as well.

Cargo commands to add utoipa & utoipa-swagger-ui
$ cargo add utoipa -F "actix_web"
$ cargo add utoipa-swagger-ui -F "actix_web debug-embed"

Next in the swagger_docs.rs file we’ll add the documentation struct ApiDoc. This struct will derive OpenAPI proc macro, which will be used to create our openapi.json file which will be used by swagger.

ApiDoc struct
#[derive(OpenApi)]
#[openapi(
    paths(
        super::handler::index,
        super::handler::auth_index,
        super::handler::create_thing,
        super::handler::delete_thing,
    ),
    components(
        schemas(
            utoipa::TupleUnit,
            types::GenericPostRequest,
            types::GenericPostResponse,
            types::GenericStringResponse,
            types::PostRequest,
            types::PostResponse,
        )
    ),
    tags((name = "BasicAPI", description = "A very Basic API")),
    modifiers(&SecurityAddon)
)]
pub struct ApiDoc;

We’ve also added a couple of attributes as well:

  • paths - this is where we add our defined endpoints, so it shows up in the json.
  • components
    • schemas - where we add out type definitions. More on this later.
  • tags - information about the APIs
  • modifiers - We can pass in a struct that implements the Modify trait, which allows us to modify OpenAPI at runtime. We’ll be using this to add security setup, more on this later.

Auth Security

Our GET /v1/auth endpoint uses a bearer token. Utoipa allows us to define this which gives us a convenient way to set it for endpoints that require it. We’ll pass the SecurityAddon struct to modifiers attribute like so:

SecurityAddon Struct Implementing Modify Trait
struct SecurityAddon;
impl Modify for SecurityAddon {
    fn modify(&self, openapi: &mut openapi::OpenApi) {
        // NOTE: we can unwrap safely since there already is components registered.
        let components = openapi.components.as_mut().unwrap();
        components.add_security_scheme(
            "Token",
            SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer)),
        );
    }
}

We can add a security scheme called Token, and its type of HttpAuthScheme::Bearer. When documenting our endpoints, we can specify which security the endpoint requires using the name we provide (in this case “Token”).

Swagger UI with the Token security scheme
Swagger UI with the Token security scheme — recovered from the original page.

Types

Before we begin documenting our endpoints, we’ll define our types so it can be parsed and displayed on swagger.

Query

Query Parameters just require the IntoParams trait to be derived. We’ll also add doc comments to define so their descriptions get ported to into json as well.

Defining Query Parameters using a struct
#[derive(IntoParams, Serialize, Deserialize, Debug, PartialEq, Eq)]
pub struct DeleteRequest {
    /// Delete Permanently ?
    pub permanent: Option<bool>,

    /// when to delete ?
    pub when: Option<u64>,

    /// height of shamlessness (cuz its funny)
    pub height: u64,
}

Path

When defining our Endpoints we can pass the path variables & query parameters in the params attribute like so:

Defining params in utoipa::path proc macro
#[utoipa::path(
		...
    params(
        ("email" = String, Path, description = "Device UUID"),
        DeleteRequest,
    ),
)]
#[get("/user/{email}"]
async fn get_user(...) -> HttpResponse {...}
Swagger UI showing path and query parameters
Swagger UI showing path and query parameters — recovered from the original page.

Structs & Objects

All structs that are going to be used for request or response will require the ToSchema trait to be derived (including the structs used within your structs!). Additionally these structs should be included in schemas attribute in the swagger_docs.rs file.

Deriving ToSchema on structs
#[derive(ToSchema, Serialize, Deserialize, Debug, PartialEq, Eq)]
pub struct PostRequest {
    pub name: String,
}

#[derive(ToSchema, Serialize, Deserialize, Debug, PartialEq, Eq)]
pub struct PostResponse {
    pub status: String,
}

Generics

Suppose we have these Generic Structs:

GenericResponse & GenericRequest Structs
#[derive(ToSchema, Serialize, Deserialize, Debug, PartialEq, Eq)]
pub struct GenericResponse<U> {
    pub msg: String,
    pub data: U,
}

#[derive(ToSchema, Serialize, Deserialize, Debug, PartialEq, Eq)]
pub struct GenericRequest<U, V> {
    pub params: Option<U>,
    pub data: Option<V>,
}

utoipa can’t parse these without defining an alias. Therefore to define GenericResponse<PostResponse> you would have to add the aliases attribute:

Aliases for Generic Structs
#[derive(ToSchema, Serialize, Deserialize, Debug, PartialEq, Eq)]
#[aliases(
    GenericPostResponse = GenericResponse<PostResponse>,
    GenericStringResponse = GenericResponse<String>
)]
pub struct GenericResponse<U> {
    pub msg: String,
    pub data: U,
}

Just like with normal structs, you’ll have to import this alias type to schemas attribute, as well as the specified type (in this case PostResponse).

Sometimes you’ll want to pass in a void type (). This can be done using the utoipa::TupleUnit type. It’s just a type alias to (). We can use it like so:

Aliases for Generic Structs with empty Tuples
use utoipa::TupleUnit;

#[derive(ToSchema, Serialize, Deserialize, Debug, PartialEq, Eq)]
#[aliases(
    GenericPostRequest = GenericRequest<TupleUnit, PostRequest>,
    GenericDeleteRequest = GenericRequest<TupleUnit, DeleteRequest>,
)]
pub struct GenericRequest<U, V> {
    pub params: Option<U>,
    pub data: Option<V>,
}

Just as before you’ll have to include the types in schemas attribute, but this time you’ll have to include utoipa::TupleUnit as well.

Swagger UI with the generated schemas
Swagger UI with the generated schemas — recovered from the original page.

Documenting Endpoints

Now that we’ve defined our basic building blocks, we can start building our documentation for the individual endpoints. It’ll be in the following structure:

Format for utoipa::path proc macro
/// Endpoint Name
///
/// Description
#[utoipa::path(
    context_path = "/v1",
    responses(
        (status = 200, description = "Hello World!", body = GenericStringResponse),
        (status = 409, description = "Invalid Request Format")
    ),
		request_body = GenericPostRequest,
    params(
        ("email" = String, Path, description = "Device UUID"),
        DeleteRequest,
    ),
		security(
        ("Token" = []),
    )
)]

Since we’ve enabled the actix integration flags, utoipa can infer the request type & its path. Context path will be used if we’re scoping or handlers to a path (eg. /v1). In the responses attribute we specify the status code, description & optionally the response body if there is one. request_body, params & security are optional.

There are more options to configure (refer to the docs), and they’ll vary from the framework you’re using and what you want to display.

Showing of your endpoints

Finally its time to show off our fancy endpoints. We can attach a service to our Http Server App which will show SwaggerUI on the endpoint of our choice.

Adding SwaggerUI service to the server
HttpServer::new(|| {
      App::new()
          // enable logger
          .wrap(middleware::Logger::default())
          .configure(config)
          .service(
              SwaggerUi::new("/docs-v1/{_:.*}")
								.url("/api-docs/openapi.json", ApiDoc::openapi()),
          )
  })
  .bind(("127.0.0.1", 8080))?
  .run()
  .await

We specify the endpoint URL (in this case /docs-v1/). Then specify the location of where openapi.json is generated (in this case /api-docs/openapi.json) & the ApiDoc::openapi() object.

And when we run & open it on http://localhost:8080/docs-v1/ we’ll be presented with:

Swagger UI serving the finished docs
Swagger UI serving the finished docs — recovered from the original page.

References