Tutorial

Merancang API Warna: REST Endpoint untuk Data Warna

Baca 9 menit

API warna terdengar sangat spesifik hingga Anda memikirkan berapa banyak alat yang mengonsumsi data warna: alat desain yang membutuhkan konversi, pemeriksa aksesibilitas yang memverifikasi kontras, komponen pemilih warna yang memerlukan pencarian warna bernama, dan generator Palet yang menghasilkan harmoni. Jika Anda sedang membangun salah satu dari ini, atau menyediakan data warna kepada developer lain, Anda membutuhkan REST API yang dirancang dengan baik.

Panduan ini mencakup seluruh permukaan desain: keputusan skema, struktur endpoint, format respons, penanganan error, dan pembatasan laju — dari perspektif seseorang yang telah membangun endpoint yang mendukung Color Converter dan Contrast Checker di ColorFYI.


Desain Skema API untuk Data Warna

Objek Warna Utama

Sebuah warna memiliki beberapa representasi — hex, RGB, HSL, CMYK, OKLCH — dan beberapa bidang metadata. Objek respons kanonik harus mencakup semuanya, memungkinkan klien menggunakan format mana pun yang mereka butuhkan tanpa membuat permintaan tambahan:

{
  "hex": "#FF5733",
  "rgb": {
    "r": 255,
    "g": 87,
    "b": 51,
    "css": "rgb(255, 87, 51)"
  },
  "hsl": {
    "h": 11,
    "s": 100,
    "l": 60,
    "css": "hsl(11, 100%, 60%)"
  },
  "hsv": {
    "h": 11,
    "s": 80,
    "v": 100
  },
  "cmyk": {
    "c": 0,
    "m": 66,
    "y": 80,
    "k": 0
  },
  "oklch": {
    "l": 0.63,
    "c": 0.19,
    "h": 27.5,
    "css": "oklch(0.63 0.19 27.5)"
  },
  "luminance": 0.215,
  "is_dark": false,
  "name": "Coral Red",
  "name_source": "color_name_database",
  "name_distance": 0.04
}

Menyertakan is_dark (yang berasal dari luminansi) menghemat klien dari penerapan perhitungan luminansi WCAG sendiri. Menyertakan name dan name_distance memungkinkan klien menampilkan warna bernama terdekat tanpa panggilan API terpisah.

Konvensi Penamaan

Gunakan snake_case untuk kunci JSON (konsisten dengan sebagian besar REST API). Kelompokkan bidang terkait ke dalam objek (rgb, hsl, cmyk) daripada meratakannya (rgb_r, rgb_g, rgb_b). Objek bersarang lebih mudah dibaca, lebih mudah didestrukturisasi di JavaScript, dan dipetakan dengan bersih ke antarmuka bertipe:

interface ColorResponse {
  hex: string;
  rgb: { r: number; g: number; b: number; css: string };
  hsl: { h: number; s: number; l: number; css: string };
  oklch: { l: number; c: number; h: number; css: string };
  luminance: number;
  is_dark: boolean;
  name: string | null;
  name_source: string | null;
  name_distance: number | null;
}

Normalisasi Hex

Terima warna hex dalam berbagai format — dengan atau tanpa #, 3-digit atau 6-digit, huruf besar atau kecil — dan normalisasikan di lapisan API sebelum pemrosesan apapun:

# Contoh Python/Django
import re

def normalize_hex(raw: str) -> str:
    """Normalisasi input hex ke 6-digit huruf besar tanpa #."""
    clean = raw.strip().lstrip('#').upper()
    if not re.match(r'^[0-9A-F]{3}$|^[0-9A-F]{6}$', clean):
        raise ValueError(f"Warna hex tidak valid: {raw!r}")
    # Perluas 3-digit ke 6-digit
    if len(clean) == 3:
        clean = ''.join(c * 2 for c in clean)
    return clean

Kembalikan bentuk yang dinormalisasi dalam respons — ini menghilangkan ambiguitas dan membuat cache downstream lebih dapat diprediksi.


Endpoint Konversi Hex/RGB/HSL

GET /api/color/{hex}

Endpoint utama mengembalikan objek warna lengkap untuk kode hex yang diberikan. Inilah yang digunakan Color Converter untuk mengisi semua bidang format sekaligus:

GET /api/color/FF5733
GET /api/color/%23FF5733   (# yang diencode URL)
GET /api/color/f57          (3-digit, huruf kecil)

Ketiganya harus mengembalikan respons yang sama untuk #FF5733.

Respons:

{
  "hex": "#FF5733",
  "rgb": { "r": 255, "g": 87, "b": 51, "css": "rgb(255, 87, 51)" },
  "hsl": { "h": 11, "s": 100, "l": 60, "css": "hsl(11, 100%, 60%)" },
  "cmyk": { "c": 0, "m": 66, "y": 80, "k": 0 },
  "oklch": { "l": 0.63, "c": 0.19, "h": 27.5, "css": "oklch(0.63 0.19 27.5)" },
  "luminance": 0.215,
  "is_dark": false,
  "name": "Coral Red",
  "name_source": "css_extended",
  "name_distance": 0.08
}

POST /api/convert

Untuk mengonversi input arbitrer yang mungkin bukan hex — nama warna CSS, tripel RGB, nilai HSL:

POST /api/convert
Content-Type: application/json

{
  "input": "rgb(255, 87, 51)",
  "from": "rgb"
}

Respons: Objek warna lengkap yang sama.

Terima nilai from: hex, rgb, hsl, cmyk, oklch, name. Validasi format input sesuai dengan tipe from yang dinyatakan dan kembalikan 422 dengan pesan error yang jelas jika tidak cocok.

Contoh Django ViewSet

# apps/api/views.py
from django.http import JsonResponse
from django.views import View
from apps.colors.engine import ColorEngine

class ColorDetailView(View):
    def get(self, request, hex_code: str):
        try:
            normalized = normalize_hex(hex_code)
        except ValueError as e:
            return JsonResponse(
                {"error": "invalid_hex", "message": str(e)},
                status=422
            )

        engine = ColorEngine(normalized)
        data = {
            "hex": f"#{normalized}",
            "rgb": engine.to_rgb_dict(),
            "hsl": engine.to_hsl_dict(),
            "cmyk": engine.to_cmyk_dict(),
            "oklch": engine.to_oklch_dict(),
            "luminance": engine.relative_luminance(),
            "is_dark": engine.is_dark(),
            "name": engine.nearest_name(),
            "name_source": engine.nearest_name_source(),
            "name_distance": engine.nearest_name_distance(),
        }
        response = JsonResponse(data)
        response["Cache-Control"] = "public, max-age=86400, stale-while-revalidate=604800"
        return response

Cache data warna secara agresif — kode hex tertentu selalu menghasilkan output yang sama, sehingga cache bisa berumur sangat panjang.


API Pencarian dan Pelengkapan Otomatis Warna

GET /api/search

Pencarian warna bernama mendukung pelengkapan otomatis dalam pemilih warna dan kolom pencarian. Parameter kueri adalah nama warna parsial:

GET /api/search?q=coral
GET /api/search?q=teal&limit=5
GET /api/search?q=%230d&source=css   // Cari berdasarkan awalan hex

Respons:

{
  "results": [
    {
      "hex": "#FF7F50",
      "name": "Coral",
      "source": "css_named",
      "score": 1.0
    },
    {
      "hex": "#FF6B6B",
      "name": "Light Coral",
      "source": "css_extended",
      "score": 0.85
    },
    {
      "hex": "#E8735A",
      "name": "Terra Cotta",
      "source": "crayola",
      "score": 0.62
    }
  ],
  "total": 3,
  "query": "coral"
}

Pertimbangan Implementasi Pencarian

Untuk database warna bernama kecil (~2.000 entri), pencarian awalan dalam memori dengan pencocokan fuzzy sudah cukup cepat:

from difflib import SequenceMatcher

def search_colors(query: str, limit: int = 10) -> list[dict]:
    q = query.lower().strip()
    results = []

    for color in NAMED_COLORS:  # Daftar yang sudah dimuat
        name = color["name"].lower()
        if q in name:
            # Pencocokan substring tepat: skor berdasarkan posisi
            pos = name.index(q)
            score = 1.0 - (pos / len(name)) * 0.3
        else:
            # Pencocokan fuzzy
            score = SequenceMatcher(None, q, name).ratio()
            if score < 0.5:
                continue

        results.append({**color, "score": round(score, 3)})

    results.sort(key=lambda x: x["score"], reverse=True)
    return results[:limit]

Untuk database yang lebih dari 10.000 entri, gunakan ekstensi pg_trgm PostgreSQL dengan pengindeksan trigram untuk pencarian fuzzy sub-milidetik:

-- Aktifkan ekstensi
CREATE EXTENSION IF NOT EXISTS pg_trgm;

-- Tambah indeks
CREATE INDEX idx_color_name_trgm ON named_colors USING gin(name gin_trgm_ops);

-- Kueri
SELECT hex, name, source, similarity(name, 'coral') AS score
FROM named_colors
WHERE name % 'coral'
ORDER BY score DESC
LIMIT 10;

Optimisasi Pelengkapan Otomatis

Untuk pelengkapan otomatis pencarian langsung saat mengetik, optimalkan untuk latensi rendah:

  1. Debounce di klien — minimum 150ms sebelum mengirimkan permintaan.
  2. Tetapkan panjang kueri minimum — tolak kueri yang lebih pendek dari 2 karakter (kembalikan hasil kosong secara langsung).
  3. Cache hasil berdasarkan kueri — kueri yang sama dari pengguna berbeda menghasilkan hasil yang sama. Gunakan Cache-Control: public, max-age=3600.
  4. Respons cepat jika tidak ada hasil — respons segera dengan {"results": [], "total": 0} daripada menjalankan pemindaian fuzzy penuh.

Endpoint Pengecekan Kontras

GET /api/contrast

API kontras adalah mesin di balik Contrast Checker. Ia menerima dua kode hex dan mengembalikan rasio kontras WCAG dengan status lulus/gagal untuk setiap level:

GET /api/contrast?fg=FF5733&bg=FFFFFF
GET /api/contrast?fg=000000&bg=FFFFFF

Respons:

{
  "ratio": 3.02,
  "ratio_formatted": "3.02:1",
  "foreground": {
    "hex": "#FF5733",
    "luminance": 0.215
  },
  "background": {
    "hex": "#FFFFFF",
    "luminance": 1.0
  },
  "wcag": {
    "aa_normal": false,
    "aa_large": true,
    "aaa_normal": false,
    "aaa_large": false
  },
  "level": "AA Large",
  "passes": true
}

Bidang level mengembalikan level WCAG tertinggi yang dicapai pasangan tersebut ("AAA", "AA", "AA Large", atau "Fail"). Bidang passes adalah true jika level WCAG mana pun terpenuhi — berguna untuk indikator hijau/merah sederhana.

Perhitungan Kontras

def relative_luminance(hex_code: str) -> float:
    r, g, b = hex_to_rgb(hex_code)
    def linearize(v: int) -> float:
        s = v / 255
        return s / 12.92 if s <= 0.04045 else ((s + 0.055) / 1.055) ** 2.4
    return 0.2126 * linearize(r) + 0.7152 * linearize(g) + 0.0722 * linearize(b)

def contrast_ratio(hex1: str, hex2: str) -> float:
    L1 = relative_luminance(hex1)
    L2 = relative_luminance(hex2)
    lighter = max(L1, L2)
    darker = min(L1, L2)
    return round((lighter + 0.05) / (darker + 0.05), 2)

def wcag_result(ratio: float) -> dict:
    return {
        "aa_normal": ratio >= 4.5,
        "aa_large": ratio >= 3.0,
        "aaa_normal": ratio >= 7.0,
        "aaa_large": ratio >= 4.5,
    }

Menyarankan Alternatif Aksesibel

Ekstensi yang berguna: diberikan warna depan yang gagal kontras terhadap latar belakang, sarankan penyesuaian kecerahan minimum yang mencapai WCAG AA:

def suggest_accessible_foreground(fg_hex: str, bg_hex: str, target_ratio: float = 4.5) -> str:
    """Kembalikan versi fg_hex yang disesuaikan kecerahannya yang lulus target_ratio."""
    hsl = rgb_to_hsl(*hex_to_rgb(fg_hex))
    bg_lum = relative_luminance(bg_hex)
    is_dark_bg = bg_lum < 0.179

    # Coba sesuaikan kecerahan ke arah yang meningkatkan kontras
    step = -2 if is_dark_bg else 2  # Gelap pada bg terang, cerah pada bg gelap
    current_l = hsl[2]

    for _ in range(50):
        current_l = max(0, min(100, current_l + step))
        adjusted_hex = hsl_to_hex(hsl[0], hsl[1], current_l)
        ratio = contrast_ratio(adjusted_hex, bg_hex)
        if ratio >= target_ratio:
            return adjusted_hex

    # Kembalikan kontras maksimum jika target tidak dapat dicapai
    return '#000000' if not is_dark_bg else '#FFFFFF'

Pembatasan Laju untuk API Warna

Mengapa API Warna Memerlukan Pembatasan Laju

API warna mungkin tampak berisiko rendah, tetapi bisa menjadi target: - Serangan enumerasi: Mengulang semua 16,7 juta kode hex untuk memanen database nama warna lengkap. - Scraping: Menggunakan logika konversi Anda sebagai sumber daya komputasi gratis. - Penyalahgunaan: Permintaan volume tinggi dari satu klien yang menurunkan layanan bagi yang lain.

Strategi Implementasi

Untuk API Django, django-ratelimit menyediakan pembatasan laju berbasis dekorator dengan backend Redis:

# settings.py
RATELIMIT_USE_CACHE = 'default'  # Menggunakan backend cache Django (Redis direkomendasikan)

# views.py
from django_ratelimit.decorators import ratelimit

@ratelimit(key='ip', rate='100/m', method='GET', block=True)
def color_detail_view(request, hex_code: str):
    ...

Format Respons Pembatasan Laju

Saat batas laju terlampaui, kembalikan 429 Too Many Requests dengan isi respons yang jelas dan header Retry-After:

HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1708012345
Content-Type: application/json

{
  "error": "rate_limit_exceeded",
  "message": "Anda telah melampaui batas laju 100 permintaan per menit.",
  "retry_after": 60
}

Pembatasan Laju Berjenjang

Struktur berjenjang yang masuk akal:

Tingkat Batas Kunci
Anonim 60 permintaan/menit Alamat IP
Kunci API (gratis) 300 permintaan/menit Kunci API
Kunci API (berbayar) 3000 permintaan/menit Kunci API

Kunci API dikirim sebagai Authorization: Bearer {key} atau X-API-Key: {key}. Gunakan kunci API sebagai kunci pembatasan laju untuk permintaan terotentikasi:

@ratelimit(key='header:x-api-key', rate='300/m', method='GET')
@ratelimit(key='ip', rate='60/m', method='GET')
def color_detail_view(request, hex_code: str):
    # django-ratelimit memeriksa keduanya; batas yang lebih longgar menang untuk permintaan terotentikasi
    ...

Praktik Terbaik Format Respons

Skema Error yang Konsisten

Setiap respons error — kegagalan validasi, batas laju, tidak ditemukan — harus mengikuti skema yang sama:

{
  "error": "invalid_hex",
  "message": "Nilai 'gggggg' bukan warna hex yang valid. Diperlukan string hex 3 atau 6 digit.",
  "field": "hex_code",
  "value": "gggggg"
}
Bidang Tujuan
error Kode error yang dapat dibaca mesin (snake_case)
message Penjelasan yang dapat dibaca manusia
field Bidang input mana yang menyebabkan error (untuk error validasi)
value Nilai yang tidak valid (membantu debugging)

Paginasi untuk Endpoint Koleksi

Endpoint pencarian dengan limit dan offset:

{
  "results": [...],
  "pagination": {
    "total": 47,
    "limit": 10,
    "offset": 0,
    "next": "/api/search?q=blue&limit=10&offset=10",
    "prev": null
  }
}

Selalu sertakan total agar klien dapat merender kontrol paginasi tanpa mengetahui set hasil lengkap.

Strategi Versioning

Versi API Anda di jalur URL, bukan di header. Versioning URL bersifat eksplisit, dapat di-cache oleh CDN, dan tidak mengharuskan klien menetapkan header kustom:

/api/v1/color/FF5733    # Versi 1
/api/v2/color/FF5733    # Versi 2 (saat perubahan yang memutus diperlukan)

Pertahankan versi sebelumnya setidaknya satu tahun setelah rilis versi utama. Gunakan header deprecation pada versi lama:

Deprecation: Sun, 01 Jan 2026 00:00:00 GMT
Sunset: Sun, 01 Jan 2027 00:00:00 GMT
Link: </api/v2/color/FF5733>; rel="successor-version"

Konfigurasi CORS

Untuk API publik yang dikonsumsi oleh klien browser:

# settings.py (menggunakan django-cors-headers)
CORS_ALLOWED_ORIGINS = [
    "https://yoursite.com",
    "https://app.yoursite.com",
]

# Atau untuk API yang sepenuhnya publik:
CORS_ALLOW_ALL_ORIGINS = True

# Selalu batasi metode dan header
CORS_ALLOW_METHODS = ['GET', 'POST', 'OPTIONS']
CORS_ALLOW_HEADERS = ['Content-Type', 'Authorization', 'X-API-Key']

Arsitektur Caching

Data warna sangat mudah di-cache. Objek warna untuk #FF5733 tidak akan pernah berubah — matematika konversinya deterministik. Susun strategi cache Anda sesuai:

Endpoint Cache-Control CDN TTL
/api/color/{hex} public, max-age=86400, stale-while-revalidate=604800 1 hari
/api/search?q={q} public, max-age=3600 1 jam
/api/contrast?fg=X&bg=Y public, max-age=86400 1 hari
/api/palette/{hex} public, max-age=86400 1 hari

Tambahkan header ETag berdasarkan kode hex untuk permintaan kondisional:

response["ETag"] = f'"{hex_code}"'
response["Last-Modified"] = "Thu, 01 Jan 2026 00:00:00 GMT"  # Statis — konten tidak pernah berubah

Klien yang mengirim If-None-Match: "FF5733" mendapatkan respons 304 Not Modified tanpa isi — menghemat bandwidth pada permintaan berulang.


Poin Utama

  • Objek warna kanonik harus mencakup semua representasi format (hex, RGB, HSL, CMYK, OKLCH) dalam satu respons — jangan pernah membuat klien membuat beberapa permintaan untuk mendapatkan format yang berbeda.
  • Normalisasi input hex secara agresif: hapus #, perluas 3-digit, ubah ke huruf besar. Validasi dengan regex sebelum pemrosesan apapun.
  • Endpoint kontras adalah inti dari semua alat aksesibilitas — kembalikan rasio WCAG, lulus/gagal per level, dan level lulus tertinggi dalam satu respons.
  • Pencarian warna bernama bekerja andal dengan pencarian awalan dalam memori untuk database kecil; gunakan PostgreSQL pg_trgm untuk database besar.
  • Batasi laju berdasarkan IP untuk permintaan tidak terotentikasi (60/menit adalah nilai yang wajar); gunakan kunci API untuk tingkat yang lebih tinggi.
  • Setiap respons error harus mengikuti skema yang sama dengan error (dapat dibaca mesin) dan message (dapat dibaca manusia) minimal.
  • Data warna sangat mudah di-cache — cache CDN 24 jam sesuai untuk endpoint konversi; gunakan ETag dan stale-while-revalidate untuk invalidasi cache yang efisien.
  • Coba Color Converter dan Contrast Checker untuk melihat endpoint API ini beraksi.

Warna Terkait

Merek Terkait

Alat Terkait