API yang tidak terdokumentasi adalah API yang hanya bisa dipakai pembuatnya: calon pengguna tidak tahu endpoint apa yang tersedia, field apa yang wajib dikirim, atau bentuk error seperti apa yang harus diantisipasi. Pada Bagian 7 — bagian penutup proyek — kita menuangkan seluruh kontrak API ke dalam berkas openapi.yaml berstandar OpenAPI 3.1, lalu mengemas aplikasi ke dalam Dockerfile sehingga bisa dijalankan di mesin mana pun tanpa memasang PHP secara manual. Ditutup dengan pemeriksaan kesesuaian antara dokumen dan server yang benar-benar berjalan, catatan jujur tentang kematangan server bawaan PHP, dan rekap tujuh bagian.

Kontrak API dalam openapi.yaml #

OpenAPI adalah bahasa standar untuk mendeskripsikan REST API: satu berkas YAML berisi daftar endpoint, parameter, request body, response, skema data, hingga skema keamanan. Dokumen ini bukan sekadar bacaan manusia — alat seperti Swagger UI merendernya menjadi halaman interaktif, generator klien membangkitkan kode darinya, dan reviewer memakainya sebagai acuan resmi perilaku API. Bagian pembuka berkas kita:

yaml
openapi: 3.1.0
info:
  title: REST API Manajemen Tugas
  summary: API JSON untuk mengelola daftar tugas.
  description: |
    API manajemen tugas yang dibangun dengan PHP murni tanpa framework.
    Seluruh response dan request body berbentuk JSON. Endpoint /api/tugas
    terlindungi bearer token; minta token lewat POST /api/login, lalu
    kirim sebagai header "Authorization: Bearer <token>".
  version: 1.0.0
servers:
  - url: http://127.0.0.1:8097
    description: Server pengembangan lokal (php -S)
  - url: http://localhost:8080
    description: Container Docker

Blok info menjawab “API apa ini”, sedangkan servers mendaftarkan alamat dasar yang bisa dituju klien — dua lingkungan yang kita pakai: server pengembangan lokal dan container Docker yang akan dibangun sebentar lagi. Setiap operasi diberi tags (kesehatan, auth, tugas) yang kelak mengelompokkan endpoint pada tampilan Swagger UI.

Mendokumentasikan Route Terlindungi #

Delapan operasi terdokumentasi: GET /api/kesehatan, POST /api/login, POST /api/logout, serta CRUD lengkap /api/tugas dan /api/tugas/{id}. Operasi publik tidak memuat kunci security; operasi terlindungi memuatnya dan merujuk skema bearerAuth. Berikut operasi pembuatan tugas, lengkap dengan request body dan tiga kemungkinan response:

yaml
  /api/tugas:
    post:
      tags: [tugas]
      summary: Buat tugas baru
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TugasInput'
            example:
              judul: Belajar authentication
              status: belum
      responses:
        '201':
          description: Tugas tersimpan.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Tugas'
              example:
                id: 4
                judul: Belajar authentication
                status: belum
                dibuat_pada: '2026-10-13 22:20:56'
        '401':
          $ref: '#/components/responses/TidakSah'
        '422':
          $ref: '#/components/responses/ValidasiGagal'

Tiga hal pada potongan di atas adalah inti gaya dokumentasi ini. Pertama, security: [{bearerAuth: []}] menyatakan bahwa operasi menuntut token — tampilan Swagger UI akan otomatis menyediakan kolom token, dan klien generator tahu harus menyertakan header Authorization. Kedua, setiap response membawa example yang diambil dari transkrip nyata sesi Bagian 5-6, sehingga pembaca bisa membandingkan langsung. Ketiga, response 401 dan 422 tidak ditulis ulang di tiap operasi melainkan dirujuk lewat $ref ke komponen bersama — satu tempat mengubah, semua operasi ikut. Berikut komponen response untuk penolakan token:

yaml
  responses:
    TidakSah:
      description: Token tidak dikirim, tidak dikenal, atau kedaluwarsa.
      headers:
        WWW-Authenticate:
          description: Selalu berisi nilai Bearer.
          schema:
            type: string
          example: Bearer
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: UNAUTHORIZED
              message: "Kirim token lewat header Authorization: Bearer <token>."

Perhatikan bahwa header WWW-Authenticate ikut didokumentasikan — detail kecil yang kita bangun di TokenAuth::periksa() pada Bagian 5 kini menjadi janji tertulis. Komponen sejenis tersedia juga untuk TidakDitemukan (404), ValidasiGagal (422), dan MethodNotAllowed (405) beserta header Allow-nya.

Skema dan Contoh di Components #

Bagian components/schemas mendefinisikan bentuk data yang dipertukarkan. Skema Tugas menjadi rujukan semua response tugas; TugasInput dan TugasPerubahan menjadi rujukan request body:

yaml
  schemas:
    Tugas:
      type: object
      required: [id, judul, status, dibuat_pada]
      properties:
        id:
          type: integer
          example: 4
        judul:
          type: string
          example: Belajar SQLite
        status:
          type: string
          enum: [belum, proses, selesai]
          example: belum
        dibuat_pada:
          type: string
          description: Waktu pembuatan format YYYY-MM-DD HH:MM:SS (waktu server).
          example: '2026-01-15 08:00:00'
    # ... skema TugasInput, TugasPerubahan, LoginInput, Token, Error

Nilai required dan enum pada skema bukan hiasan: keduanya adalah duplikasi resmi dari aturan yang dijaga Validator (Bagian 4) dan constraint CHECK database (Bagian 3). Ketika tiga lapis itu menyebut nilai yang sama — validator, database, dokumen — kontrak terasa konsisten; ketika salah satunya melenceng, itulah bug dokumentasi yang paling sering mengecewakan pengguna. Skema Error sengaja hanya menuntut error.code dan error.message wajib, dengan details opsional — persis perilaku ErrorHandler yang menyertakan details hanya bila ada informasi tambahan.

Memeriksa Contoh JSON Secara Otomatis #

Bagaimana memastikan berkas openapi.yaml tidak berbohong? Lingkungan PHP kita tidak memuat ekstensi YAML, jadi kita tidak bisa mengurai berkasnya langsung dari PHP. Yang bisa dikerjakan adalah mengambil contoh-contoh JSON di dalamnya — yang memang paling sering disalin pengguna — dan memeriksa strukturnya terhadap aturan skema:

php
<?php

/**
 * Pemeriksa bentuk JSON untuk contoh-contoh di openapi.yaml.
 * PHP pada lingkungan ini tidak memuat ekstensi YAML, jadi contoh JSON
 * disalin dari berkas spesifikasi lalu strukturnya diperiksa satu per satu.
 */

/**
 * @param array<string, string>       $wajib kolom wajib => tipe yang diharapkan.
 * @param array<string, list<string>> $enum  kolom => nilai yang diizinkan.
 */
function periksaBentuk(string $label, string $json, array $wajib, array $enum = []): void
{
    if (!json_validate($json)) {
        printf("%-12s: GAGAL — bukan JSON valid\n", $label);
        return;
    }

    $data = json_decode($json, true);

    foreach ($wajib as $kolom => $tipe) {
        if (!array_key_exists($kolom, $data)) {
            printf("%-12s: GAGAL — kolom '%s' hilang\n", $label, $kolom);
            return;
        }
        if (gettype($data[$kolom]) !== $tipe) {
            printf(
                "%-12s: GAGAL — kolom '%s' bertipe %s, skema menuntut %s\n",
                $label,
                $kolom,
                gettype($data[$kolom]),
                $tipe,
            );
            return;
        }
    }

    foreach ($enum as $kolom => $daftar) {
        if (!in_array($data[$kolom], $daftar, true)) {
            printf("%-12s: GAGAL — nilai '%s' di luar %s\n", $label, $data[$kolom], implode('/', $daftar));
            return;
        }
    }

    printf("%-12s: cocok (%d kolom wajib)\n", $label, count($wajib));
}

// Contoh-contoh berikut disalin dari openapi.yaml.
$tugas = '{"id":4,"judul":"Belajar SQLite","status":"belum","dibuat_pada":"2026-01-15 08:00:00"}';
$login = '{"email":"[email protected]","sandi":"sandi-budi-2026"}';
$token = '{"token":"79a2e78d8883fe1c4626cb26074d8c6ec1f2c3671cd7b8b322f1368d16594396"}';
$error = '{"error":{"code":"NOT_FOUND","message":"Tugas dengan id 99 tidak ditemukan."}}';

$statusValid = ['belum', 'proses', 'selesai'];

$aturanTugas = ['id' => 'integer', 'judul' => 'string', 'status' => 'string', 'dibuat_pada' => 'string'];

periksaBentuk('Tugas', $tugas, $aturanTugas, ['status' => $statusValid]);
periksaBentuk('LoginInput', $login, ['email' => 'string', 'sandi' => 'string']);
periksaBentuk('Token', $token, ['token' => 'string']);
periksaBentuk('Error', $error, ['error' => 'array']);

// Satu contoh sengaja salah untuk membuktikan pemeriksa benar-benar bekerja.
$salah = '{"id":5,"judul":"Status ngaco","status":"batal","dibuat_pada":"2026-01-15 08:00:00"}';
periksaBentuk('Tugas-salah', $salah, $aturanTugas, ['status' => $statusValid]);

Output yang Dihasilkan:

text
Tugas       : cocok (4 kolom wajib)
LoginInput  : cocok (2 kolom wajib)
Token       : cocok (1 kolom wajib)
Error       : cocok (1 kolom wajib)
Tugas-salah : GAGAL — nilai 'batal' di luar belum/proses/selesai

Empat contoh sah lolos: json_validate() membuktikan string memang JSON, pemeriksaan kunci memastikan kolom wajib hadir, gettype() mencocokkan tipe, dan pemeriksaan enum menolak nilai di luar daftar. Baris terakhir sengaja mengirim status: batal dan ditolak — pemeriksa yang tidak pernah gagal bukan pemeriksa. Pada proyek nyata, lapisan berikutnya adalah validator OpenAPI eksternal (mis. Spectral atau editor daring Swagger) yang mengurai seluruh berkas YAML, bukan hanya contohnya; pendekatan PHP di atas adalah jaring pertama yang bisa berjalan tanpa alat tambahan.

Menguji Kesesuaian Dokumen dengan Server Nyata #

Pemeriksaan paling meyakinkan tetaplah server yang benar-benar berjalan. Dengan database segar, kita bandingkan response nyata dengan contoh yang tertulis di spesifikasi:

bash
# terminal 1 — server dengan database segar
$ rm -f /tmp/tasks.sqlite
$ TASK_API_DB=/tmp/tasks.sqlite php -S 127.0.0.1:8097 -t public

# terminal 2 — pengguna demo seperti pada Bagian 5, lalu dua pemeriksaan
$ curl -sS http://127.0.0.1:8097/api/kesehatan
text
{"status":"ok"}
bash
$ curl -sS -X POST -H 'Content-Type: application/json' \
    -d '{"email":"[email protected]","sandi":"sandi-budi-2026"}' \
    http://127.0.0.1:8097/api/login
text
{"token":"aad211d18f4edb8113e1c0dc8eb045c22f2f56aa64e97211daf0931dbe803be5"}
bash
$ TOKEN=aad211d18f4edb8113e1c0dc8eb045c22f2f56aa64e97211daf0931dbe803be5
$ curl -sS -i -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8097/api/tugas/1
text
HTTP/1.1 200 OK
Host: 127.0.0.1:8097
Date: Tue, 13 Oct 2026 22:33:54 GMT
Connection: close
X-Powered-By: PHP/8.4.23
Content-Type: application/json; charset=utf-8

{"id":1,"judul":"Belajar PHP dasar","status":"selesai","dibuat_pada":"2026-01-05 08:00:00"}

Body response terakhir identik dengan contoh pada operasi GET /api/tugas/{id} di openapi.yaml — kolom sama, tipe sama, nilai seed sama. Inilah arti “API sesuai dokumen”: pengguna yang memprogram berdasarkan spesifikasi tidak akan menabrak kejutan. Token pada transkrip tentu berbeda pada setiap login Anda, dan justru itu sesuai deskripsi skema Token yang menyatakan nilainya selalu baru.

Dockerfile: Mengemas API #

Dokumentasi menjangkau pembaca; container menjangkau mesin. Dockerfile berikut mengemas proyek menjadi image yang bisa dijalankan di mana Docker tersedia:

dockerfile
# Image dasar resmi PHP 8.4 CLI.
# Ekstensi pdo_sqlite sudah terpasang bawaan pada image resmi ini,
# jadi tidak ada pekerjaan instalasi ekstensi tambahan.
FROM php:8.4-cli

# Seluruh proyek (composer.json, public/, src/, database/) disalin ke /app.
COPY . /app
WORKDIR /app

# Lokasi berkas database SQLite di dalam container.
# Timpa dari luar tanpa membangun ulang image:
#   docker run -e TASK_API_DB=/data/tasks.sqlite ...
ENV TASK_API_DB=/tmp/tasks.sqlite

# Web server bawaan PHP bisa melayani beberapa request secara paralel
# lewat variabel ini (berlaku pada sistem mirip Unix seperti container).
ENV PHP_CLI_SERVER_WORKERS=4

# Port yang didengarkan php -S pada CMD di bawah.
EXPOSE 8080

# Satu proses PHP melayani seluruh request dari front controller
# di dalam direktori public/.
CMD ["php", "-S", "0.0.0.0:8080", "-t", "public"]

Mari kita bedah per instruksi. FROM php:8.4-cli memilih image resmi PHP 8.4 varian CLI — cukup untuk proyek kita karena kita tidak memakai Apache ataupun PHP-FPM, dan pdo_sqlite memang sudah terpasang di image resmi tersebut. COPY . /app lalu WORKDIR /app menyalin seluruh proyek dan menjadikannya direktori kerja. ENV TASK_API_DB=/tmp/tasks.sqlite memberi nilai bawaan variabel yang dibaca Database::buat() sejak Bagian 3 — satu-satunya titik konfigurasi yang dibutuhkan, dan bisa ditimpa lewat flag -e tanpa membangun ulang image. EXPOSE 8080 mendokumentasikan port yang dipakai, dan CMD menjalankan perintah yang sudah kita pakai sepanjang proyek — server web bawaan PHP, kali ini mendengarkan 0.0.0.0 (semua antarmuka jaringan container) alih-alih 127.0.0.1.

Catatan Kematangan: Server Bawaan vs PHP-FPM #

Kita perlu jujur tentang apa yang dikemas Dockerfile ini. Server web bawaan PHP (php -S) dirancang untuk pengembangan: secara bawaan ia memproses satu request pada satu waktu, sehingga request yang lambat membuat antrean. Variabel PHP_CLI_SERVER_WORKERS=4 pada Dockerfile mengatasinya sebagian dengan memproses beberapa request paralel, dan untuk API kecil internal itu sering sudah memadai. Namun untuk beban nyata, jalur produksi yang lazim adalah nginx atau Apache di depan PHP-FPM sebagai FastCGI process manager — pemisahan itu memberi manajemen proses worker, konfigurasi TLS, dan buffering yang matang. Detail kedua: berkas SQLite di /tmp akan hilang bersama container; untuk data yang menetap, arahkan TASK_API_DB ke path volume yang di-mount, misalnya docker run -v tasks-data:/data -e TASK_API_DB=/data/tasks.sqlite.

Apa yang Berubah #

Dua berkas baru menutup proyek ini, keduanya di luar kode PHP:

  • openapi.yaml — spesifikasi OpenAPI 3.1 untuk seluruh API: 8 operasi (2 publik, 6 terlindungi), skema Tugas, TugasInput, TugasPerubahan, LoginInput, Token, Error, skema keamanan bearerAuth, serta komponen response 401/404/405/422 yang dipakai ulang lewat $ref.
  • Dockerfile — image php:8.4-cli berisi seluruh proyek, variabel TASK_API_DB dan PHP_CLI_SERVER_WORKERS, port 8080, dan CMD php -S dari direktori public/.

Rekap Tujuh Bagian #

Tujuh bagian membentuk satu aplikasi utuh dari kosong. Tabel berikut memetakan perjalanan itu:

BagianBerkas kunciYang diberikan
1 — Fondasi APIcomposer.json, public/index.phpStruktur proyek, autoloading PSR-4, front controller, /api/kesehatan
2 — Routing dan Requestsrc/Router.php, Request.php, Response.phpRouter dengan {param}, 404/405 + Allow, objek request/response immutable
3 — Database dan Repositorysrc/Database.php, TaskRepository.php, database/schema.sqlPDO SQLite, prepared statements, CRUD tugas, seed deterministik
4 — Validation dan Errorsrc/Validator.php, ErrorHandler.phpAturan validasi berantai, bentuk error JSON tunggal, 500 tertangani
5 — Authenticationsrc/TokenAuth.php, UserRepository.phpLogin bcrypt, bearer token ter-hash, route terlindungi, logout
6 — API Testingtests/bootstrap.php, tests/ApiTest.php11 test end-to-end tanpa web server, isolasi :memory:
7 — Dokumentasi dan Deploymentopenapi.yaml, DockerfileKontrak tertulis untuk pengguna, kemasan container

Nilai terbesar proyek ini mungkin bukan aplikasinya — daemon tugas bertebaran di mana-mana — melainkan pola keputusannya: objek request/response yang bisa diuji tanpa HTTP (Bagian 2) dibayar penuh oleh test suite (Bagian 6); repository yang menyembunyikan SQL membuat handler tipis; bentuk error yang konsisten membuat dokumentasi bisa menjanjikan satu skema Error untuk semua endpoint. Pola-pola inilah yang Anda bawa ke proyek berikutnya, apa pun frameworknya.

Cara Menjalankan #

bash
# 1. Pemeriksa bentuk contoh JSON — salin blok "Memeriksa Contoh JSON"
#    ke berkas contoh-bentuk.php di dalam folder bagian ini, lalu jalankan
php contoh-bentuk.php

# 2. Uji kesesuaian dokumen vs server nyata (dari root proyek)
rm -f /tmp/tasks.sqlite
TASK_API_DB=/tmp/tasks.sqlite php -S 127.0.0.1:8097 -t public
curl -sS http://127.0.0.1:8097/api/kesehatan

# 3. Bangun dan jalankan container (butuh Docker di mesin Anda)
docker build -t manajemen-tugas-api .
docker run --rm -p 8080:8080 manajemen-tugas-api
curl -sS http://localhost:8080/api/kesehatan

Perintah Docker pada poin 3 tidak dijalankan di lingkungan penulisan halaman ini — Docker tidak tersedia di sana — sehingga tidak ada transkripnya; seluruh perilaku yang dikutip di halaman ini berasal dari sesi php -S dan PHPUnit yang nyata. Dua berkas pendukung halaman ini — openapi.yaml dan Dockerfile — tersedia di bagian bawah halaman. Untuk pendalaman, baca spesifikasi OpenAPI 3.1 , image resmi PHP di Docker Hub , dokumentasi server web bawaan PHP , serta panduan nginx + PHP-FPM . Selamat — proyek REST API manajemen tugas selesai, terdokumentasi, dan siap dikemas.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

openapi.yaml

Unduh berkas
yaml
openapi: 3.1.0
info:
  title: REST API Manajemen Tugas
  summary: API JSON untuk mengelola daftar tugas.
  description: |
    API manajemen tugas yang dibangun dengan PHP murni tanpa framework.
    Seluruh response dan request body berbentuk JSON. Endpoint /api/tugas
    terlindungi bearer token; minta token lewat POST /api/login, lalu
    kirim sebagai header "Authorization: Bearer <token>".
  version: 1.0.0
servers:
  - url: http://127.0.0.1:8097
    description: Server pengembangan lokal (php -S)
  - url: http://localhost:8080
    description: Container Docker
tags:
  - name: kesehatan
    description: Pemeriksaan status layanan.
  - name: auth
    description: Login dan logout bearer token.
  - name: tugas
    description: CRUD daftar tugas (membutuhkan token).
paths:
  /api/kesehatan:
    get:
      tags: [kesehatan]
      summary: Periksa status layanan
      responses:
        '200':
          description: Layanan hidup.
          content:
            application/json:
              schema:
                type: object
                required: [status]
                properties:
                  status:
                    type: string
                    const: ok
              example:
                status: ok
        '405':
          $ref: '#/components/responses/MethodNotAllowed'
  /api/login:
    post:
      tags: [auth]
      summary: Tukar email dan sandi dengan bearer token
      description: |
        Satu-satunya cara memperoleh token. Sandi diverifikasi terhadap
        hash bcrypt; kegagalan apa pun (email tak dikenal atau sandi salah)
        dijawab pesan yang sama.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginInput'
            example:
              email: [email protected]
              sandi: sandi-budi-2026
      responses:
        '200':
          description: Login berhasil; token berlaku 1 jam.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Token'
              example:
                token: 79a2e78d8883fe1c4626cb26074d8c6ec1f2c3671cd7b8b322f1368d16594396
        '401':
          description: Email atau sandi salah.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  code: UNAUTHORIZED
                  message: Email atau sandi salah.
  /api/logout:
    post:
      tags: [auth]
      summary: Cabut token yang dipakai request ini
      security:
        - bearerAuth: []
      responses:
        '204':
          description: Token dicabut; request berikutnya dengan token yang sama ditolak 401.
        '401':
          $ref: '#/components/responses/TidakSah'
  /api/tugas:
    get:
      tags: [tugas]
      summary: Daftar seluruh tugas
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Daftar tugas terurut menaik berdasarkan id.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Tugas'
              example:
                - id: 1
                  judul: Belajar PHP dasar
                  status: selesai
                  dibuat_pada: '2026-01-05 08:00:00'
                - id: 2
                  judul: Merancang REST API
                  status: proses
                  dibuat_pada: '2026-01-10 09:30:00'
                - id: 3
                  judul: Menulis dokumentasi API
                  status: belum
                  dibuat_pada: '2026-01-12 14:15:00'
        '401':
          $ref: '#/components/responses/TidakSah'
    post:
      tags: [tugas]
      summary: Buat tugas baru
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TugasInput'
            example:
              judul: Belajar authentication
              status: belum
      responses:
        '201':
          description: Tugas tersimpan.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Tugas'
              example:
                id: 4
                judul: Belajar authentication
                status: belum
                dibuat_pada: '2026-10-13 22:20:56'
        '401':
          $ref: '#/components/responses/TidakSah'
        '422':
          $ref: '#/components/responses/ValidasiGagal'
  /api/tugas/{id}:
    get:
      tags: [tugas]
      summary: Baca satu tugas
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/IdTugas'
      responses:
        '200':
          description: Detail tugas.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Tugas'
              example:
                id: 1
                judul: Belajar PHP dasar
                status: selesai
                dibuat_pada: '2026-01-05 08:00:00'
        '401':
          $ref: '#/components/responses/TidakSah'
        '404':
          $ref: '#/components/responses/TidakDitemukan'
    put:
      tags: [tugas]
      summary: Ubah tugas sebagian atau seluruhnya
      description: |
        Kolom yang tidak dikirim membiarkan nilai lama tetap; kolom yang
        dikirim divalidasi dengan aturan yang sama seperti pembuatan.
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/IdTugas'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TugasPerubahan'
            example:
              status: selesai
      responses:
        '200':
          description: Tugas terbarui; seluruh kolom dikirim balik.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Tugas'
              example:
                id: 4
                judul: Belajar authentication
                status: selesai
                dibuat_pada: '2026-10-13 22:20:56'
        '401':
          $ref: '#/components/responses/TidakSah'
        '404':
          $ref: '#/components/responses/TidakDitemukan'
        '422':
          $ref: '#/components/responses/ValidasiGagal'
    delete:
      tags: [tugas]
      summary: Hapus satu tugas
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/IdTugas'
      responses:
        '204':
          description: Tugas terhapus; tidak ada body.
        '401':
          $ref: '#/components/responses/TidakSah'
        '404':
          $ref: '#/components/responses/TidakDitemukan'
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: |
        Token 64 karakter heksadesimal dari POST /api/login. Kirim sebagai
        header "Authorization: Bearer <token>". Berlaku 1 jam dan bisa
        dicabut lewat POST /api/logout.
  parameters:
    IdTugas:
      name: id
      in: path
      required: true
      description: id tugas.
      schema:
        type: integer
        minimum: 1
  responses:
    TidakSah:
      description: Token tidak dikirim, tidak dikenal, atau kedaluwarsa.
      headers:
        WWW-Authenticate:
          description: Selalu berisi nilai Bearer.
          schema:
            type: string
          example: Bearer
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: UNAUTHORIZED
              message: "Kirim token lewat header Authorization: Bearer <token>."
    TidakDitemukan:
      description: Tugas dengan id tersebut tidak ada.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: NOT_FOUND
              message: Tugas dengan id 99 tidak ditemukan.
    ValidasiGagal:
      description: Body tidak lolos validasi; details berisi daftar pesan per kolom.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: VALIDATION_ERROR
              message: Data yang dikirim tidak valid.
              details:
                judul:
                  - "Kolom 'judul' minimal 3 karakter."
                status:
                  - "Kolom 'status' harus salah satu dari: belum, proses, selesai."
    MethodNotAllowed:
      description: Method tidak diizinkan untuk endpoint ini.
      headers:
        Allow:
          description: Daftar method yang diizinkan, dipisah koma.
          schema:
            type: string
          example: GET
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: METHOD_NOT_ALLOWED
              message: Method PUT tidak diizinkan untuk endpoint ini.
  schemas:
    Tugas:
      type: object
      required: [id, judul, status, dibuat_pada]
      properties:
        id:
          type: integer
          example: 4
        judul:
          type: string
          example: Belajar SQLite
        status:
          type: string
          enum: [belum, proses, selesai]
          example: belum
        dibuat_pada:
          type: string
          description: Waktu pembuatan format YYYY-MM-DD HH:MM:SS (waktu server).
          example: '2026-01-15 08:00:00'
    TugasInput:
      type: object
      required: [judul]
      properties:
        judul:
          type: string
          minLength: 3
          maxLength: 100
          example: Belajar authentication
        status:
          type: string
          enum: [belum, proses, selesai]
          default: belum
    TugasPerubahan:
      type: object
      description: Kolom yang dikirim divalidasi; kolom yang tidak dikirim membiarkan nilai lama.
      properties:
        judul:
          type: string
          minLength: 3
          maxLength: 100
        status:
          type: string
          enum: [belum, proses, selesai]
    LoginInput:
      type: object
      required: [email, sandi]
      properties:
        email:
          type: string
          format: email
          example: [email protected]
        sandi:
          type: string
          example: sandi-budi-2026
    Token:
      type: object
      required: [token]
      properties:
        token:
          type: string
          description: Token acak 64 karakter heksadesimal; nilai berbeda pada setiap login.
          example: 79a2e78d8883fe1c4626cb26074d8c6ec1f2c3671cd7b8b322f1368d16594396
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              description: Kode error yang stabil untuk diprogram klien.
              example: NOT_FOUND
            message:
              type: string
              description: Pesan yang ditujukan untuk manusia.
              example: Tugas dengan id 99 tidak ditemukan.
            details:
              type: object
              description: Informasi tambahan; hanya muncul bila relevan.