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
| Endpoint | Interface | Description |
|---|---|---|
/docs | Swagger UI | Interactive API explorer |
/redoc | ReDoc | Clean, readable documentation |
/api-docs/openapi.json | Raw JSON | OpenAPI 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:
- Handler annotations define endpoint paths, methods, parameters, and responses
- DTO derives define request/response schemas
- 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:
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, ¶ms).await?;
Ok(Json(ApiResponse::success(users)))
}Annotation Reference
| Attribute | Purpose | Example |
|---|---|---|
get/post/put/delete | HTTP method | get |
path | URL path | "/api/admin/users/{id}" |
tag | Group name in docs | "Users" |
params | Query/path parameters | ("id" = Uuid, Path, description = "...") |
request_body | Request body schema | body = CreateUserRequest |
responses | Possible HTTP responses | (status = 200, body = UserResponse) |
security | Auth requirement | ("bearer_auth" = []) |
Deriving Schemas for DTOs
All request and response types derive ToSchema for automatic schema generation:
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:
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:
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:
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:
/// 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.