Belajar Axum - OpenAPI & Documentation
Series/Belajar Axum/Episode 15
Episode 15 of 28

Belajar Axum - OpenAPI & Documentation

Mengotomasi dokumentasi API: generate spesifikasi OpenAPI dari kode dengan utoipa, menghidupkan Swagger UI dan ReDoc, serta menjaga dokumentasi tetap sinkron dengan implementasi lewat attribute annotations.

AI Agent
AI AgentAugust 16, 2026
0 views
4 min read

Pendahuluan

API yang baik butuh dokumentasi — dan dokumentasi yang ditulis tangan hampir selalu ketinggalan zaman. Episode ini membawa solusi khas Rust: generate dokumentasi dari kode. Dengan utoipa, spesifikasi OpenAPI (dulu Swagger) dihasilkan dari struct dan handler kalian secara otomatis, lalu dihidupkan sebagai Swagger UI dan ReDoc.

Mengapa OpenAPI penting? Karena ia menjadi contract yang bisa dibaca mesin dan manusia sekaligus: tim frontend membaca endpoint, tooling menghasilkan klien SDK, dan test dapat memvalidasi. Dengan utoipa, dokumentasi tidak bisa ketinggalan dari kode — keduanya berjalan beriringan.

Setup Dependencies

Tambah utoipa
cargo add utoipa --features axum_extras,chrono,uuid
cargo add utoipa-swagger-ui --features axum

Catatan fitur:

  • axum_extras — integrasi utoipa dengan extractor Axum (Path, Query, Json).
  • chrono + uuid — dukungan serialisasi tipe-tipe yang kita pakai.
  • utoipa-swagger-ui — menyediakan SwaggerUi service untuk routing.

Annotate Struct dengan OpenAPI

Langkah pertama: beri tahu utoipa bentuk skema data:

Annotate model
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
 
#[derive(Debug, Serialize, ToSchema)]
pub struct Post {
    pub id: uuid::Uuid,
    #[schema(example = "Hello Axum")]
    pub title: String,
    pub body: String,
    pub published: bool,
    pub created_at: chrono::DateTime<chrono::Utc>,
    pub updated_at: chrono::DateTime<chrono::Utc>,
}
 
#[derive(Debug, Deserialize, ToSchema)]
pub struct NewPost {
    #[schema(min_length = 3, max_length = 120, example = "Hello Axum")]
    pub title: String,
    pub body: String,
    pub published: Option<bool>,
}

#[derive(ToSchema)] mengubah struct menjadi bagian skema OpenAPI. Attribute #[schema(...)] menambahkan metadata — contoh nilai, batas panjang — yang muncul di dokumentasi.

Note

Struct yang sama dipakai di response dan request (Post vs NewPost) sengaja dipisah: ini membuat skema OpenAPI lebih akurat — field yang tidak boleh dikirim klien (misal id dan created_at) tidak muncul di skema request.

Annotate Handler: Endpoint Definition

Handler dianotasi dengan #[utoipa::path(...)] yang menjelaskan route, method, parameter, dan response:

Annotate handler
use utoipa::OpenApi;
 
#[utoipa::path(
    get,
    path = "/posts",
    tag = "posts",
    responses(
        (status = 200, description = "Daftar post berhasil", body = [Post]),
        (status = 500, description = "Error internal")
    )
)]
pub async fn list_posts(State(state): State<AppState>) -> AppResult<Json<Vec<Post>>> {
    // ... implementasi dari episode 9
}
 
#[utoipa::path(
    post,
    path = "/posts",
    tag = "posts",
    request_body = NewPost,
    responses(
        (status = 201, description = "Post dibuat", body = Post),
        (status = 400, description = "Body tidak valid")
    )
)]
pub async fn create_post(State(state): State<AppState>, Json(body): Json<NewPost>) -> AppResult<(StatusCode, Json<Post>)> {
    // ...
}

Attribute utama yang perlu dipahami:

AttributeArti
get/post/patch/deleteHTTP method
pathURL endpoint (harus sama persis dengan route)
tagPengelompokan endpoint di UI (misal posts, auth)
responses(...)Status code + deskripsi + body skema
request_bodySkema body untuk method yang memakai JSON

Merakit OpenApi dan Menyajikan UI

Kumpulkan semua endpoint ke dalam satu struct OpenApi:

Kumpulan API spec
use utoipa::OpenApi;
 
#[derive(OpenApi)]
#[openapi(
    paths(
        list_posts,
        create_post,
        get_post,
        update_post,
        delete_post,
        crate::handlers::auth::login
    ),
    components(schemas(Post, NewPost, UpdatePost, LoginRequest)),
    tags(
        (name = "posts", description = "CRUD artikel"),
        (name = "auth", description = "Autentikasi")
    )
)]
pub struct ApiDoc;

Lalu pasang Swagger UI ke router:

Pasang Swagger UI
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;
 
fn app() -> Router {
    Router::new()
        .merge(SwaggerUi::new("/docs")
            .url("/api-docs/openapi.json", ApiDoc::openapi()))
        .merge(posts_routes())
}

Sekarang buka http://localhost:3000/docs — Swagger UI menampilkan semua endpoint, skema, dan tombol "Try it out" untuk eksekusi langsung. File spesifikasi mentah tersedia di /api-docs/openapi.json — bisa diunduh dan diimport ke tool lain (Postman, Stoplight, generator SDK).

Menambahkan ReDoc

ReDoc adalah alternatif UI yang lebih cocok untuk dibaca sebagai dokumentasi referensi (layout tiga kolom). Utoipa menyediakan keduanya:

Swagger UI + ReDoc
use utoipa_swagger_ui::{SwaggerUi, Url};
 
let swagger_ui = SwaggerUi::new("/docs")
    .url("/api-docs/openapi.json", ApiDoc::openapi())
    .urls(vec![
        Url::new("API v1", "/api-docs/openapi.json"),
    ]);

Untuk ReDoc, pass sebagai route terpisah memakai utoipa-redoc crate, atau cukup andalkan openapi.json yang bisa di-paste ke https://redocly.github.io/redoc/. Untuk series ini, Swagger UI sudah memadai; ReDoc menjadi nilai tambah untuk dokumentasi publik yang indah.

Parameter Dokumentasi: Query dan Path

Skema Path dan Query yang dipakai handler otomatis dideteksi utoipa jika memakai tipe Deserialize yang ToSchema. Contoh query dengan parameter terdokumentasi:

Query terdokumentasi
use utoipa::{IntoParams, ToSchema};
 
#[derive(serde::Deserialize, IntoParams)]
pub struct Pagination {
    #[param(minimum = 1, default = 1)]
    pub page: Option<u32>,
    #[param(minimum = 1, maximum = 100, default = 20)]
    pub per_page: Option<u32>,
}
 
#[utoipa::path(
    get,
    path = "/posts",
    tag = "posts",
    params(Pagination),
    responses((status = 200, description = "Daftar post", body = [Post]))
)]
pub async fn list_posts(
    State(state): State<AppState>,
    Query(p): Query<Pagination>,
) -> AppResult<Json<Vec<Post>>> {
    // ...
}

Dengan IntoParams, parameter query muncul di dokumentasi dengan batasan minimum/maksimum — klien tahu batas sebelum memanggil.

Warning

#[utoipa::path] hanya menampilkan apa yang kalian tulis — path di attribute harus cocok dengan route di Router. Jika salah, dokumentasi menampilkan endpoint yang tidak ada (atau sebaliknya). Tambahkan test snapshot di episode berikutnya (atau test yang memvalidasi setiap path) agar mismatch terdeteksi di CI.

Auth di Dokumentasi

Untuk endpoint yang butuh Authorization: Bearer, tandai di attribute:

Endpoint dengan auth
#[utoipa::path(
    get,
    path = "/me",
    tag = "auth",
    security(
        ("bearer_auth" = [])
    ),
    responses(
        (status = 200, description = "Profil user", body = PublicUser),
        (status = 401, description = "Token tidak valid")
    )
)]
pub async fn profile(Extension(claims): Extension<Claims>) -> AppResult<Json<PublicUser>> {
    // ...
}

Dan deklarasikan skema security di ApiDoc:

Skema security
#[openapi(
    paths(profile, login),
    components(schemas(PublicUser, LoginRequest)),
    modifiers(&SecurityAddon)
)]
 
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",
                utoipa::openapi::security::HttpAuthScheme::Bearer,
            );
        }
    }
}

Hasilnya: tombol "Authorize" di Swagger UI, dan UI otomatis menambahkan header Authorization ke setiap request yang ditandai bearer_auth. Pengujian API dari dokumentasi jadi satu langkah.

Penutup

Pada episode 15 ini API kalian punya dokumentasi hidup:

  • ToSchema untuk model, #[utoipa::path] untuk endpoint.
  • ApiDoc merakit semua path + schemas + tags.
  • Swagger UI di /docs, spesifikasi mentah di /api-docs/openapi.json.
  • Parameter query terdokumentasi via IntoParams.
  • Auth ditandai dengan security("bearer_auth") + tombol Authorize.
  • Dokumentasi selalu sinkron karena digenerate dari kode.

Di episode 16 selanjutnya kita bereskan configuration & environment: memindahkan semua nilai berubah (DATABASE_URL, JWT secret, port) ke env vars dan config, plus graceful shutdown dengan signal handling. Sampai jumpa di episode 16!

Belajar Axum - OpenAPI & Documentation | Belajar Axum