Skip to content

API Documentation

FORGE generates interactive API documentation using the OpenAPI 3.0 specification. Documentation is auto-generated from your handler annotations and DTO type definitions, ensuring that your docs always stay in sync with your code. The generated project includes both Swagger UI and ReDoc interfaces out of the box.

Documentation Endpoints

EndpointInterfaceDescription
/docsSwagger UIInteractive API explorer
/redocReDocClean, readable documentation
/api-docs/openapi.jsonRaw JSONOpenAPI 3.0 specification file

Access Swagger UI in your browser at http://localhost:8080/docs after starting the development server.

TIP

Swagger UI allows you to execute API calls directly from the browser. Click "Authorize" and paste your JWT token to test authenticated endpoints without leaving the documentation page.

How It Works

FORGE uses Utoipa to generate OpenAPI specs from Rust code. Documentation is derived from three sources:

  1. Handler annotations define endpoint paths, methods, parameters, and responses
  2. DTO derives define request/response schemas
  3. Module tags organize endpoints into logical groups
┌──────────────┐     ┌──────────────┐     ┌────────────────┐
│  #[utoipa::  │     │  ToSchema    │     │   ApiDoc       │
│   path(...)] │────▶│  derives     │────▶│   struct       │
│  on handlers │     │  on DTOs     │     │   (collects    │
│              │     │              │     │    everything)  │
└──────────────┘     └──────────────┘     └────────────────┘


                                          ┌────────────────┐
                                          │  OpenAPI JSON   │
                                          │  /docs (Swagger)│
                                          │  /redoc         │
                                          └────────────────┘

Annotating Handlers

Add the #[utoipa::path] attribute to each handler to include it in the generated documentation:

rust
use utoipa;

/// List all users with pagination
#[utoipa::path(
    get,
    path = "/api/admin/users",
    tag = "Users",
    params(
        ("page" = Option<i64>, Query, description = "Page number"),
        ("per_page" = Option<i64>, Query, description = "Items per page"),
        ("search" = Option<String>, Query, description = "Search by name or email"),
    ),
    responses(
        (status = 200, description = "Paginated user list", body = PaginatedResponse<UserResponse>),
        (status = 401, description = "Unauthorized", body = ErrorResponse),
        (status = 403, description = "Forbidden", body = ErrorResponse),
    ),
    security(
        ("bearer_auth" = [])
    )
)]
pub async fn list_users(
    State(pool): State<PgPool>,
    Extension(user): Extension<AuthUser>,
    Query(params): Query<PaginatedRequest>,
) -> Result<impl IntoResponse, AppError> {
    let users = UserService::list(&pool, &params).await?;
    Ok(Json(ApiResponse::success(users)))
}

Annotation Reference

AttributePurposeExample
get/post/put/deleteHTTP methodget
pathURL path"/api/admin/users/{id}"
tagGroup name in docs"Users"
paramsQuery/path parameters("id" = Uuid, Path, description = "...")
request_bodyRequest body schemabody = CreateUserRequest
responsesPossible HTTP responses(status = 200, body = UserResponse)
securityAuth requirement("bearer_auth" = [])

Deriving Schemas for DTOs

All request and response types derive ToSchema for automatic schema generation:

rust
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use validator::Validate;

/// Request to create a new user
#[derive(Deserialize, Validate, ToSchema)]
pub struct CreateUserRequest {
    /// Full name of the user
    #[schema(example = "Jane Doe")]
    #[validate(length(min = 1, max = 255))]
    pub name: String,

    /// Email address (must be unique)
    #[schema(example = "jane@example.com")]
    #[validate(email)]
    pub email: String,

    /// Password (minimum 8 characters)
    #[schema(example = "securepassword123")]
    #[validate(length(min = 8))]
    pub password: String,

    /// Role IDs to assign
    #[schema(example = json!(["550e8400-e29b-41d4-a716-446655440000"]))]
    pub role_ids: Option<Vec<Uuid>>,
}

/// User response returned by the API
#[derive(Serialize, ToSchema)]
pub struct UserResponse {
    pub id: Uuid,
    pub name: String,
    pub email: String,
    pub is_active: bool,
    pub roles: Vec<RoleResponse>,
    pub created_at: chrono::DateTime<chrono::Utc>,
}

TIP

Use #[schema(example = ...)] to provide meaningful example values. These appear in Swagger UI's "Try it out" feature, making it easier for frontend developers to understand expected formats.

Tag Organization

Endpoints are grouped by tags, which correspond to modules:

rust
use utoipa::OpenApi;

#[derive(OpenApi)]
#[openapi(
    info(
        title = "FORGE API",
        version = "1.0.0",
        description = "Auto-generated API documentation"
    ),
    tags(
        (name = "Auth", description = "Authentication endpoints"),
        (name = "Users", description = "User management"),
        (name = "Roles", description = "Role management"),
        (name = "Permissions", description = "Permission management"),
        (name = "Contents", description = "Content management"),
        (name = "Settings", description = "Application settings"),
        (name = "Profile", description = "Current user profile"),
        (name = "Media", description = "File uploads and media"),
    ),
    paths(
        handlers::auth::login,
        handlers::auth::register,
        handlers::auth::refresh,
        handlers::auth::logout,
        handlers::admin::users::list_users,
        handlers::admin::users::create_user,
        handlers::admin::users::show_user,
        handlers::admin::users::update_user,
        handlers::admin::users::delete_user,
        // ... more handlers
    ),
    components(
        schemas(
            CreateUserRequest,
            UpdateUserRequest,
            UserResponse,
            LoginRequest,
            TokenResponse,
            ApiResponse<UserResponse>,
            PaginatedResponse<UserResponse>,
            ErrorResponse,
            // ... more schemas
        )
    ),
    modifiers(&SecurityAddon)
)]
pub struct ApiDoc;

Security Scheme

The JWT bearer token authentication is registered as a security scheme:

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

struct SecurityAddon;

impl 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(),
                ),
            );
        }
    }
}

Serving Documentation

The documentation routes are registered in main.rs:

rust
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;
use utoipa_redoc::{Redoc, Servable};

let app = Router::new()
    // API routes
    .nest("/api/admin", routes::admin::routes(pool.clone()))
    .nest("/api", routes::public::routes(pool.clone()))
    // Documentation
    .merge(SwaggerUi::new("/docs").url("/api-docs/openapi.json", ApiDoc::openapi()))
    .merge(Redoc::with_url("/redoc", ApiDoc::openapi()));

CLI Commands

API documentation is automatically generated from your handler annotations. The OpenAPI spec is served at https://api.<yourapp>.test/docs when running the development server with forge serve.

TIP

The API server exposes a built-in Swagger UI at the /docs endpoint. No separate CLI command is needed — just start the server and navigate to the docs URL.

Customizing Documentation

Adding Descriptions

Use Rust doc comments on handlers and DTOs. They automatically become OpenAPI descriptions:

rust
/// Create a new content entry
///
/// Creates a content entry with translations for all supported locales.
/// The slug must be unique across all content entries.
/// Requires the `contents.create` permission.
#[utoipa::path(/* ... */)]
pub async fn create_content(/* ... */) {
    // ...
}

Hiding Internal Endpoints

Omit internal endpoints from the paths list in the ApiDoc struct. Only handlers listed in the #[openapi(paths(...))] attribute appear in the documentation.

See Also

  • Handlers — Writing handlers with documentation annotations
  • DTOs — Request/response types with schema derivation
  • Routes — Route registration for documented endpoints

Released under the MIT License.