Integrations

OpenAPI/Swagger

New to acton-service?

Start with the homepage to understand what acton-service is, then explore Core Concepts for foundational explanations. See the Glossary for technical term definitions.


Generate interactive API documentation with OpenAPI specifications and multiple UI options for comprehensive API exploration.


Overview

acton-service provides built-in OpenAPI/Swagger documentation support with automatic schema generation, multiple UI options, and first-class support for versioned APIs. Documentation is automatically generated from your route definitions and type annotations.

Installation

Enable the OpenAPI feature:

[dependencies]

Configuration

Configure OpenAPI documentation in your service configuration:

# ~/.config/acton-service/my-service/config.toml
[openapi]
enabled = true
title = "My Service API"
version = "1.0.0"
description = "Production API service"
contact_name = "API Team"
contact_email = "api@example.com"
license_name = "MIT"

# UI preferences
ui = "swagger"  # Options: swagger, rapidoc, redoc, all
serve_spec = true  # Serve OpenAPI JSON at /openapi.json

Environment Variables

ACTON_OPENAPI_ENABLED=true
ACTON_OPENAPI_UI=swagger

Basic Usage

Enable OpenAPI documentation in your service:

use acton_service::prelude::*;
use utoipa::{OpenApi, ToSchema};

#[derive(Serialize, Deserialize, ToSchema)]
struct User {
    id: i64,
    name: String,
    email: String,
}

#[derive(Serialize, Deserialize, ToSchema)]
struct CreateUserRequest {
    name: String,
    email: String,
}

/// List all users
#[utoipa::path(
    get,
    path = "/users",
    responses(
        (status = 200, description = "List of users", body = Vec<User>)
    ),
    tag = "users"
)]
async fn list_users(
    State(state): State<AppState>
) -> Result<Json<Vec<User>>> {
    let db = state.database()?;
    let users = sqlx::query_as!(User, "SELECT id, name, email FROM users")
        .fetch_all(db)
        .await?;
    Ok(Json(users))
}

/// Create a new user
#[utoipa::path(
    post,
    path = "/users",
    request_body = CreateUserRequest,
    responses(
        (status = 201, description = "User created", body = User),
        (status = 400, description = "Invalid request")
    ),
    tag = "users"
)]
async fn create_user(
    State(state): State<AppState>,
    Json(request): Json<CreateUserRequest>,
) -> Result<(StatusCode, Json<User>)> {
    let db = state.database()?;
    let user = sqlx::query_as!(
        User,
        "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *",
        request.name,
        request.email
    )
    .fetch_one(db)
    .await?;

    Ok((StatusCode::CREATED, Json(user)))
}

#[derive(OpenApi)]
#[openapi(
    paths(list_users, create_user),
    components(schemas(User, CreateUserRequest)),
    tags(
        (name = "users", description = "User management endpoints")
    )
)]
struct ApiDoc;

#[tokio::main]
async fn main() -> Result<()> {
    use acton_service::openapi::SwaggerUI;

    let routes = VersionedApiBuilder::new()
        .with_base_path("/api")
        .add_version(ApiVersion::V1, |router| {
            router
                .route("/users", get(list_users))
                .route("/users", post(create_user))
                .merge(SwaggerUI::with_spec("/swagger-ui", ApiDoc::openapi()))
        })
        .build_routes();

    ServiceBuilder::new()
        .with_routes(routes)
        .build()
        .serve()
        .await
}

Access the documentation:

# Swagger UI
http://localhost:8080/swagger-ui

# OpenAPI specification
http://localhost:8080/openapi.json

Note on OpenAPI Integration: OpenAPI/Swagger UI integration is handled via the Router::merge() method combined with SwaggerUI::with_spec(), not through ServiceBuilder methods. This allows you to flexibly place OpenAPI documentation routes within your versioned API structure. The SwaggerUI router can be merged into any Router instance during the version building phase.

Multiple UI Options

acton-service supports three popular OpenAPI UI frameworks:

Swagger UI

The traditional, feature-rich OpenAPI UI:

[openapi]
ui = "swagger"
http://localhost:8080/swagger-ui

Features:

  • Interactive API testing
  • Request/response examples
  • Authentication support
  • Model schema visualization

RapiDoc

Modern, customizable API documentation:

[openapi]
ui = "rapidoc"
http://localhost:8080/rapidoc

Features:

  • Responsive design
  • Three layout modes (column, row, focused)
  • Syntax highlighting
  • Code samples in multiple languages

ReDoc

Clean, three-panel documentation:

[openapi]
ui = "redoc"
http://localhost:8080/redoc

Features:

  • Responsive three-panel design
  • Search functionality
  • Downloadable OpenAPI spec
  • Menu/navigation sidebar

All UI Options

Serve all three UIs simultaneously:

[openapi]
ui = "all"
http://localhost:8080/swagger-ui
http://localhost:8080/rapidoc
http://localhost:8080/redoc

Multi-Version Documentation

Document multiple API versions in a single specification:

#[derive(OpenApi)]
#[openapi(
    info(
        title = "My Service API",
        version = "1.0.0",
    ),
    paths(
        v1::list_users,
        v1::create_user,
    ),
    components(schemas(v1::User, v1::CreateUserRequest)),
    tags(
        (name = "v1", description = "API Version 1 (deprecated)")
    )
)]
struct ApiDocV1;

#[derive(OpenApi)]
#[openapi(
    info(
        title = "My Service API",
        version = "2.0.0",
    ),
    paths(
        v2::list_users,
        v2::create_user,
        v2::update_user,
    ),
    components(schemas(v2::User, v2::CreateUserRequest, v2::UpdateUserRequest)),
    tags(
        (name = "v2", description = "API Version 2 (current)")
    )
)]
struct ApiDocV2;

#[tokio::main]
async fn main() -> Result<()> {
    use acton_service::openapi::SwaggerUI;

    let routes = VersionedApiBuilder::new()
        .with_base_path("/api")
        .add_version(ApiVersion::V1, |router| {
            router
                .route("/users", get(v1::list_users))
                .route("/users", post(v1::create_user))
                .merge(SwaggerUI::with_spec("/swagger-ui/v1", ApiDocV1::openapi()))
        })
        .add_version(ApiVersion::V2, |router| {
            router
                .route("/users", get(v2::list_users))
                .route("/users", post(v2::create_user))
                .route("/users/:id", put(v2::update_user))
                .merge(SwaggerUI::with_spec("/swagger-ui/v2", ApiDocV2::openapi()))
        })
        .build_routes();

    ServiceBuilder::new()
        .with_routes(routes)
        .build()
        .serve()
        .await
}

Access version-specific documentation:

# V1 documentation
http://localhost:8080/swagger-ui/v1

# V2 documentation
http://localhost:8080/swagger-ui/v2

# Version-specific specs
http://localhost:8080/openapi/v1.json
http://localhost:8080/openapi/v2.json

Authentication Documentation

Document authentication schemes:

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

#[derive(OpenApi)]
#[openapi(
    paths(list_users, create_user),
    components(schemas(User)),
    modifiers(&SecurityAddon)
)]
struct ApiDoc;

struct SecurityAddon;

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

/// List users (requires authentication)
#[utoipa::path(
    get,
    path = "/users",
    responses(
        (status = 200, description = "List of users", body = Vec<User>),
        (status = 401, description = "Unauthorized")
    ),
    security(
        ("bearer_auth" = [])
    ),
    tag = "users"
)]
async fn list_users(
    claims: JwtClaims,
    State(state): State<AppState>
) -> Result<Json<Vec<User>>> {
    // ...
}

Advanced Schema Annotations

Response Examples

/// Get user by ID
#[utoipa::path(
    get,
    path = "/users/{id}",
    params(
        ("id" = i64, Path, description = "User ID")
    ),
    responses(
        (status = 200, description = "User found", body = User,
            example = json!({
                "id": 1,
                "name": "John Doe",
                "email": "john@example.com"
            })
        ),
        (status = 404, description = "User not found")
    )
)]
async fn get_user(
    Path(id): Path<i64>,
    State(state): State<AppState>
) -> Result<Json<User>> {
    // ...
}

Complex Schema Types

#[derive(Serialize, Deserialize, ToSchema)]
struct PaginatedResponse<T> {
    #[schema(example = json!([]))]
    data: Vec<T>,

    #[schema(example = 1)]
    page: u32,

    #[schema(example = 10)]
    per_page: u32,

    #[schema(example = 100)]
    total: u64,
}

#[utoipa::path(
    get,
    path = "/users",
    params(
        ("page" = Option<u32>, Query, description = "Page number"),
        ("per_page" = Option<u32>, Query, description = "Items per page")
    ),
    responses(
        (status = 200, description = "Paginated users", body = PaginatedResponse<User>)
    )
)]
async fn list_users_paginated(
    Query(params): Query<PaginationParams>,
    State(state): State<AppState>
) -> Result<Json<PaginatedResponse<User>>> {
    // ...
}

Validation Constraints

use validator::Validate;

#[derive(Serialize, Deserialize, ToSchema, Validate)]
struct CreateUserRequest {
    #[validate(length(min = 1, max = 100))]
    #[schema(example = "John Doe", min_length = 1, max_length = 100)]
    name: String,

    #[validate(email)]
    #[schema(example = "john@example.com", format = "email")]
    email: String,

    #[validate(range(min = 18, max = 120))]
    #[schema(example = 25, minimum = 18, maximum = 120)]
    age: u8,
}

Production Configuration

Disable in Production

Disable OpenAPI documentation in production environments:

[openapi]
enabled = false  # Disable docs in production

Or use environment-based configuration:

# Development
ACTON_OPENAPI_ENABLED=true cargo run

# Production
ACTON_OPENAPI_ENABLED=false cargo run

Custom Documentation URL

Serve documentation at a custom path:

use acton_service::openapi::SwaggerUI;

let routes = VersionedApiBuilder::new()
    .add_version(ApiVersion::V1, |router| {
        router
            .route("/users", get(list_users))
            .merge(SwaggerUI::with_spec("/docs", ApiDoc::openapi()))
    })
    .build_routes();

ServiceBuilder::new()
    .with_routes(routes)
    .build()
    .serve()
    .await

External Specification

Load OpenAPI spec from external file:

use acton_service::openapi::SwaggerUI;

let spec = std::fs::read_to_string("openapi.yaml")?;
let openapi: utoipa::openapi::OpenApi = serde_yaml::from_str(&spec)?;

let routes = VersionedApiBuilder::new()
    .add_version(ApiVersion::V1, |router| {
        router
            .route("/users", get(list_users))
            .merge(SwaggerUI::with_spec("/swagger-ui", openapi))
    })
    .build_routes();

ServiceBuilder::new()
    .with_routes(routes)
    .build()
    .serve()
    .await

Best Practices

Document All Endpoints

// ✅ Good - comprehensive documentation
#[utoipa::path(
    post,
    path = "/users",
    request_body = CreateUserRequest,
    responses(
        (status = 201, description = "User created", body = User),
        (status = 400, description = "Invalid request", body = ErrorResponse),
        (status = 401, description = "Unauthorized"),
        (status = 409, description = "User already exists")
    ),
    tag = "users"
)]

Use Descriptive Examples

#[derive(ToSchema)]
struct User {
    #[schema(example = 42)]
    id: i64,

    #[schema(example = "John Doe")]
    name: String,

    #[schema(example = "john.doe@example.com", format = "email")]
    email: String,
}
#[openapi(
    paths(
        users::list_users,
        users::create_user,
        users::get_user,
        users::update_user,
        users::delete_user,
    ),
    tags(
        (name = "users", description = "User management operations"),
        (name = "auth", description = "Authentication endpoints")
    )
)]

Version Your Schemas

// ✅ Good - versioned schemas
mod v1 {
    #[derive(ToSchema)]
    struct User {
        id: i64,
        name: String,
    }
}

mod v2 {
    #[derive(ToSchema)]
    struct User {
        id: i64,
        name: String,
        email: String,  // New field in V2
    }
}
Previous
Events (NATS)