Dokumentasi API yang tertinggal dari kode adalah dokumen yang berbohong. OpenAPI (sebelumnya Swagger) adalah standar industri untuk mendeskripsikan REST API secara mesin-readable. Library springdoc-openapi memindai @RestController Spring dan menghasilkan dokumen OpenAPI otomatis. Pada chapter ini kita akan memahami struktur OpenAPI dan memodelkan registry endpoint yang mencetak JSON OpenAPI.

OpenAPI 3.0 #

OpenAPI 3.0 adalah spesifikasi JSON/YAML untuk mendeskripsikan REST API. Struktur utamanya:

text Struktur OpenAPI
{
  "openapi": "3.0.3",
  "info": { "title": "...", "version": "..." },
  "tags": [ { "name": "...", "description": "..." } ],
  "paths": {
    "/users": {
      "get": { "summary": "...", "parameters": [...], "responses": {...} },
      "post": { ... }
    }
  }
}

Field paths memetakan URL ke operasi HTTP. Setiap operasi memiliki summary, parameters, dan responses. Klien dapat memparse dokumen ini untuk generate SDK, mock server, atau dokumentasi interaktif.

springdoc-openapi #

springdoc-openapi adalah library Spring Boot yang memindai classpath untuk @RestController, membaca anotasi @GetMapping/@PostMapping, dan menghasilkan JSON di endpoint /v3/api-docs. Library juga menyediakan Swagger UI di /swagger-ui.html — antarmuka interaktif untuk mencoba API.

kotlin Konfigurasi springdoc-openapi
// build.gradle.kts
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0")

// Controller dengan anotasi OpenAPI
@Tag(name = "users", description = "Manajemen pengguna")
@RestController
@RequestMapping("/users")
class UserController {
    @Operation(summary = "Daftar pengguna", description = "Mengembalikan seluruh pengguna")
    @ApiResponse(responseCode = "200", description = "OK")
    @GetMapping
    fun list(): List<User> = ...
}

Anotasi @Tag, @Operation, @Parameter, @ApiResponse berasal dari OpenAPI. Springdoc membaca anotasi ini dan menggabungkannya dengan refleksi Spring MVC untuk menghasilkan dokumen lengkap.

Anatomi Endpoint OpenAPI #

Setiap endpoint dijelaskan oleh beberapa field:

FieldPeran
summaryJudul singkat operasi (1 baris)
descriptionPenjelasan detail (paragraf)
parametersDaftar parameter (path, query, header)
responsesMap status code ke deskripsi
tagsPengelompokan logis
deprecatedTandai operasi sebagai deprecated

Parameter memiliki in (path, query, header, cookie), required, description, dan schema. Responses memiliki code (status HTTP) dan description.

Demo OpenAPI Registry #

Karena springdoc-openapi nyata membutuhkan Spring Boot runtime, kita akan memodelkan registry endpoint sebagai class biasa. Program ini mendaftar lima endpoint, menghasilkan JSON OpenAPI 3.0, dan mensimulasikan Swagger UI route.

kotlin
// api_documentation.kt
// Mendemonstrasikan auto-generate dokumentasi API OpenAPI/Swagger.
// Pada kode produksi, springdoc-openapi memindai @RestController dan
// menghasilkan /v3/api-docs serta UI Swagger. Di sini kita memodelkannya
// sebagai registry yang mengumpulkan endpoint metadata dan mencetak
// OpenAPI JSON.

// Mock anotasi OpenAPI
@Target(AnnotationTarget.CLASS) annotation class Tag(val name: String, val description: String = "")
@Target(AnnotationTarget.FUNCTION) annotation class Operation(
    val summary: String = "",
    val description: String = "",
    val deprecated: Boolean = false
)
@Target(AnnotationTarget.VALUE_PARAMETER) annotation class Parameter(
    val name: String,
    val `in`: String = "path",
    val required: Boolean = true,
    val description: String = ""
)
@Target(AnnotationTarget.VALUE_PARAMETER) annotation class ApiResponse(
    val code: Int,
    val description: String
)

data class EndpointInfo(
    val method: String,
    val path: String,
    val summary: String,
    val description: String,
    val deprecated: Boolean,
    val tags: List<String>,
    val parameters: List<ParamInfo>,
    val responses: Map<Int, String>
)

data class ParamInfo(val name: String, val `in`: String, val required: Boolean, val description: String)

// OpenAPI Registry: mengumpulkan endpoint yang dipindai dari controller
class OpenApiRegistry {
    private val endpoints = mutableListOf<EndpointInfo>()
    private val tags = mutableMapOf<String, String>()

    fun addTag(name: String, description: String) { tags[name] = description }
    fun addEndpoint(info: EndpointInfo) { endpoints.add(info) }

    fun endpoints(): List<EndpointInfo> = endpoints.toList()

    // Generate JSON OpenAPI 3.0
    fun toJson(): String {
        val sb = StringBuilder()
        sb.append("{\n")
        sb.append("""  "openapi": "3.0.3",""").append("\n")
        sb.append("""  "info": {"title": "Demo API", "version": "1.0.0"},""").append("\n")
        val tagsJson = tags.entries.joinToString(",") { (n, d) ->
            """{"name":"$n","description":"$d"}"""
        }
        sb.append("""  "tags": [$tagsJson],""").append("\n")
        sb.append("""  "paths": {""")
        val paths = endpoints.groupBy { it.path }
        val pathStrs = paths.map { (path, infos) ->
            val methods = infos.joinToString(",") { e ->
                val m = e.method.lowercase()
                val params = e.parameters.joinToString(",") { p ->
                    """{"name":"${p.name}","in":"${p.`in`}","required":${p.required},"description":"${p.description}","schema":{"type":"string"}}"""
                }
                val responses = e.responses.entries.joinToString(",") { (code, desc) ->
                    """"$code":{"description":"$desc"}"""
                }
                val deprecation = if (e.deprecated) """"deprecated": true,""" else ""
                val tagsArr = e.tags.joinToString(",") { """"$it"""" }
                """"$m":{"summary":"${e.summary}","description":"${e.description}",$deprecation"parameters":[$params],"responses":{$responses},"tags":[$tagsArr]}"""
            }
            """"$path":{$methods}"""
        }
        sb.append(pathStrs.joinToString(","))
        sb.append("}\n")
        sb.append("}")
        return sb.toString()
    }
}

// Mock controller yang akan dipindai
@Tag(name = "users", description = "Manajemen pengguna")
class UserController

@Tag(name = "health", description = "Health check")
class HealthController

fun registerEndpoints(reg: OpenApiRegistry) {
    reg.addEndpoint(EndpointInfo(
        method = "GET",
        path = "/users",
        summary = "Daftar semua pengguna",
        description = "Mengembalikan array berisi seluruh pengguna terdaftar.",
        deprecated = false,
        tags = listOf("users"),
        parameters = emptyList(),
        responses = mapOf(200 to "OK", 500 to "Server error")
    ))
    reg.addEndpoint(EndpointInfo(
        method = "GET",
        path = "/users/{id}",
        summary = "Detail pengguna",
        description = "Mengembalikan satu pengguna berdasarkan id.",
        deprecated = false,
        tags = listOf("users"),
        parameters = listOf(ParamInfo("id", "path", true, "ID pengguna")),
        responses = mapOf(200 to "OK", 404 to "Pengguna tidak ditemukan")
    ))
    reg.addEndpoint(EndpointInfo(
        method = "POST",
        path = "/users",
        summary = "Buat pengguna baru",
        description = "Membuat pengguna baru. Body berisi nama dan email.",
        deprecated = false,
        tags = listOf("users"),
        parameters = listOf(ParamInfo("body", "body", true, "Data pengguna baru")),
        responses = mapOf(201 to "Created", 400 to "Input invalid")
    ))
    reg.addEndpoint(EndpointInfo(
        method = "DELETE",
        path = "/users/{id}",
        summary = "Hapus pengguna (deprecated)",
        description = "Penghapusan di-deprecate; gunakan soft delete via PATCH.",
        deprecated = true,
        tags = listOf("users"),
        parameters = listOf(ParamInfo("id", "path", true, "ID pengguna")),
        responses = mapOf(204 to "No Content", 404 to "Not found")
    ))
    reg.addEndpoint(EndpointInfo(
        method = "GET",
        path = "/health",
        summary = "Status server",
        description = "Endpoint publik untuk health check.",
        deprecated = false,
        tags = listOf("health"),
        parameters = emptyList(),
        responses = mapOf(200 to "OK")
    ))
}

fun main() {
    val reg = OpenApiRegistry()
    registerEndpoints(reg)

    println("=== Endpoint terdaftar (${reg.endpoints().size}) ===")
    reg.endpoints().forEach { e ->
        val deprec = if (e.deprecated) " [DEPRECATED]" else ""
        println("  ${e.method.padEnd(7)} ${e.path}$deprec")
        println("           summary: ${e.summary}")
        println("           tags: ${e.tags}, params: ${e.parameters.size}, responses: ${e.responses.keys}")
    }

    println("\n=== OpenAPI 3.0 JSON (ringkas) ===")
    println(reg.toJson())

    println("\n=== Swagger UI route (di Spring Boot otomatis tersedia di /swagger-ui.html) ===")
    println("  GET /swagger-ui.html  -> UI interaktif")
    println("  GET /v3/api-docs       -> JSON di atas")
}

Output yang Dihasilkan:

text
=== Endpoint terdaftar (5) ===
  GET     /users
           summary: Daftar semua pengguna
           tags: [users], params: 0, responses: [200, 500]
  GET     /users/{id}
           summary: Detail pengguna
           tags: [users], params: 1, responses: [200, 404]
  POST    /users
           summary: Buat pengguna baru
           tags: [users], params: 1, responses: [201, 400]
  DELETE  /users/{id} [DEPRECATED]
           summary: Hapus pengguna (deprecated)
           tags: [users], params: 1, responses: [204, 404]
  GET     /health
           summary: Status server
           tags: [health], params: 0, responses: [200]

=== OpenAPI 3.0 JSON (ringkas) ===
{
  "openapi": "3.0.3",
  "info": {"title": "Demo API", "version": "1.0.0"},
  "tags": [],
  "paths": {"/users":{"get":{"summary":"Daftar semua pengguna","description":"Mengembalikan array berisi seluruh pengguna terdaftar.","parameters":[],"responses":{"200":{"description":"OK"},"500":{"description":"Server error"}},"tags":["users"]},"post":{"summary":"Buat pengguna baru","description":"Membuat pengguna baru. Body berisi nama dan email.","parameters":[{"name":"body","in":"body","required":true,"description":"Data pengguna baru","schema":{"type":"string"}}],"responses":{"201":{"description":"Created"},"400":{"description":"Input invalid"}},"tags":["users"]}},"/users/{id}":{"get":{"summary":"Detail pengguna","description":"Mengembalikan satu pengguna berdasarkan id.","parameters":[{"name":"id","in":"path","required":true,"description":"ID pengguna","schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"404":{"description":"Pengguna tidak ditemukan"}},"tags":["users"]},"delete":{"summary":"Hapus pengguna (deprecated)","description":"Penghapusan di-deprecate; gunakan soft delete via PATCH.","deprecated": true,"parameters":[{"name":"id","in":"path","required":true,"description":"ID pengguna","schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"404":{"description":"Not found"}},"tags":["users"]}},"/health":{"get":{"summary":"Status server","description":"Endpoint publik untuk health check.","parameters":[],"responses":{"200":{"description":"OK"}},"tags":["health"]}}}
}

=== Swagger UI route (di Spring Boot otomatis tersedia di /swagger-ui.html) ===
  GET /swagger-ui.html  -> UI interaktif
  GET /v3/api-docs       -> JSON di atas

Mari kita bedah bagian penting kode di atas:

  1. data class EndpointInfo(val method, val path, val summary, ...): model satu endpoint OpenAPI. Field deprecated menandai endpoint yang akan dihapus; parameter dan responses dijelaskan per endpoint.
  2. class OpenApiRegistry { private val endpoints = mutableListOf<EndpointInfo>() }: registry yang menyimpan endpoint. Pada produksi, springdoc mengisi registry otomatis via refleksi — kita tidak perlu memanggil addEndpoint manual.
  3. val paths = endpoints.groupBy { it.path }: kelompokkan endpoint berdasarkan path. OpenAPI menyatakan bahwa satu path dapat memiliki multiple method (GET, POST, DELETE di URL yang sama).
  4. val deprecation = if (e.deprecated) """"deprecated": true,""" else "": bila endpoint deprecated, sertakan field deprecated: true di JSON. Swagger UI akan menandai endpoint ini dengan label deprecated.
  5. fun toJson(): String: generator JSON manual. Pada produksi, library seperti kotlinx.serialization atau Jackson menangani ini — tetapi struktur output sama.

Swagger UI #

Swagger UI adalah antarmuka HTML yang membaca JSON OpenAPI dan menampilkan dokumentasi interaktif. Setiap endpoint dapat dieksekusi langsung dari browser — pengguna mengisi parameter, klik “Try it out”, dan melihat respons nyata. Swagger UI sangat berguna untuk onboarding klien API baru.

Referensi Lanjutan #

  • Ktor routing — panduan routing Ktor sebagai sumber kebenaran struktur endpoint yang akan didokumentasikan
  • Spring guides — kumpulan panduan Spring untuk mendokumentasikan REST API

Selanjutnya #

Setelah dokumentasi siap, langkah berikutnya adalah tugas latar belakang. Chapter Background Jobs akan membahas @Scheduled, scheduler Quartz, dan cron expression untuk tugas yang berjalan teratur.