Tutorials

Entwurf einer Farb-API: REST-Endpunkte für Farbdaten

8 Min. Lesezeit

Eine Farb-API klingt nach einer Nischenanwendung – bis man überlegt, wie viele Tools Farbdaten verbrauchen: Design-Tools, die Konvertierungen benötigen, Barrierefreiheitsprüfer, die Kontrast verifizieren, Farbwähler-Komponenten, die benannte Farbsuche brauchen, und Palettengeneratoren, die Harmonien erzeugen. Wer eines dieser Tools baut oder anderen Entwicklern Farbdaten bereitstellt, benötigt eine gut gestaltete REST-API.

Dieser Leitfaden deckt die gesamte Designoberfläche ab: Schema-Entscheidungen, Endpunktstruktur, Antwortformate, Fehlerbehandlung und Rate-Limiting – aus der Perspektive von jemandem, der die Endpunkte gebaut hat, die den Farbkonverter und den Kontrastprüfer bei ColorFYI antreiben.


API-Schema-Design für Farbdaten

Das zentrale Farbobjekt

Eine Farbe hat mehrere Darstellungen – Hex, RGB, HSL, CMYK, OKLCH – und mehrere Metadatenfelder. Das kanonische Antwortobjekt sollte alle einschließen, sodass Clients das gewünschte Format verwenden können, ohne zusätzliche Anfragen zu stellen:

{
  "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
}

is_dark (abgeleitet aus der Leuchtdichte) einzuschließen erspart Clients die Implementierung der WCAG-Leuchtdichteberechnung. name und name_distance einzuschließen ermöglicht Clients, die nächste benannte Farbe anzuzeigen, ohne einen separaten API-Aufruf.

Namenskonventionen

snake_case für JSON-Schlüssel verwenden (konsistent mit den meisten REST-APIs). Verwandte Felder in Objekte verschachteln (rgb, hsl, cmyk) statt sie zu flachen (rgb_r, rgb_g, rgb_b). Verschachtelte Objekte sind lesbarer, einfacher in JavaScript zu destrukturieren und lassen sich sauber auf typisierte Interfaces abbilden:

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;
}

Hex-Normalisierung

Hex-Farben in mehreren Formaten akzeptieren – mit oder ohne #, 3-stellig oder 6-stellig, Groß- oder Kleinbuchstaben – und sie in der API-Schicht vor jeder Verarbeitung normalisieren:

# Python/Django-Beispiel
import re

def normalize_hex(raw: str) -> str:
    """Hex-Eingabe zu 6-stellig, Großbuchstaben, ohne # normalisieren."""
    clean = raw.strip().lstrip('#').upper()
    if not re.match(r'^[0-9A-F]{3}$|^[0-9A-F]{6}$', clean):
        raise ValueError(f"Ungültige Hex-Farbe: {raw!r}")
    # 3-stellig auf 6-stellig erweitern
    if len(clean) == 3:
        clean = ''.join(c * 2 for c in clean)
    return clean

Die normalisierte Form in der Antwort zurückgeben – dies beseitigt Mehrdeutigkeit und macht das nachgelagerte Caching vorhersehbarer.


Hex/RGB/HSL-Konvertierungsendpunkte

GET /api/color/{hex}

Der primäre Endpunkt gibt das vollständige Farbobjekt für einen gegebenen Hex-Code zurück. Dies ist es, was der Farbkonverter verwendet, um alle Formatfelder gleichzeitig zu befüllen:

GET /api/color/FF5733
GET /api/color/%23FF5733   (URL-kodiertes #)
GET /api/color/f57          (3-stellig, Kleinbuchstaben)

Alle drei sollten dieselbe Antwort für #FF5733 zurückgeben.

Antwort:

{
  "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

Für die Konvertierung beliebiger Eingaben, die möglicherweise kein Hex sind – CSS-Farbnamen, RGB-Tripel, HSL-Werte:

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

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

Antwort: Dasselbe vollständige Farbobjekt.

from-Werte akzeptieren: hex, rgb, hsl, cmyk, oklch, name. Das Eingabeformat gegen den deklarierten from-Typ validieren und bei Nichtübereinstimmung eine 422 mit einer klaren Fehlermeldung zurückgeben.

Django ViewSet-Beispiel

# 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

Farbdaten aggressiv cachen – ein gegebener Hex-Code erzeugt immer dieselbe Ausgabe, daher kann der Cache sehr langlebig sein.


Farbsuche und Autocomplete-API

GET /api/search

Die benannte Farbsuche treibt Autocomplete in Farbwählern und Suchfeldern an. Der Query-Parameter ist ein teilweiser Farbname:

GET /api/search?q=coral
GET /api/search?q=teal&limit=5
GET /api/search?q=%230d&source=css   // Suche nach Hex-Präfix

Antwort:

{
  "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"
}

Überlegungen zur Suchimplementierung

Für eine kleine benannte Farbdatenbank (~2.000 Einträge) ist eine In-Memory-Präfixsuche mit Fuzzy-Matching schnell genug:

from difflib import SequenceMatcher

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

    for color in NAMED_COLORS:  # Vorab geladene Liste
        name = color["name"].lower()
        if q in name:
            # Exakter Teilstring-Treffer: Score basierend auf Position
            pos = name.index(q)
            score = 1.0 - (pos / len(name)) * 0.3
        else:
            # Fuzzy-Match
            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]

Für Datenbanken mit mehr als 10.000 Einträgen die PostgreSQL-Erweiterung pg_trgm mit Trigram-Indexierung für Sub-Millisekunden-Fuzzy-Suche verwenden:

-- Erweiterung aktivieren
CREATE EXTENSION IF NOT EXISTS pg_trgm;

-- Index hinzufügen
CREATE INDEX idx_color_name_trgm ON named_colors USING gin(name gin_trgm_ops);

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

Autocomplete-Optimierung

Für ein live search-as-you-type Autocomplete auf niedrige Latenz optimieren:

  1. Client-seitiges Debouncing – Minimum 150 ms vor dem Abfeuern der Anfrage.
  2. Mindest-Abfragelänge festlegen – Abfragen kürzer als 2 Zeichen ablehnen (sofort leere Ergebnisse zurückgeben).
  3. Ergebnisse nach Abfrage cachen – Dieselbe Abfrage von verschiedenen Nutzern gibt dasselbe Ergebnis zurück. Cache-Control: public, max-age=3600 verwenden.
  4. Schnell bei keinen Ergebnissen antworten – Sofort mit {"results": [], "total": 0} antworten, statt eine vollständige Fuzzy-Suche durchzuführen.

Kontrastprüfungs-Endpunkt

GET /api/contrast

Die Kontrast-API ist das Herzstück des Kontrastprüfers. Sie nimmt zwei Hex-Codes entgegen und gibt das WCAG-Kontrastverhältnis mit Bestanden/Nicht-bestanden-Status für jede Stufe zurück:

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

Antwort:

{
  "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
}

Das level-Feld gibt die höchste WCAG-Stufe zurück, die das Paar erreicht ("AAA", "AA", "AA Large" oder "Fail"). Das passes-Feld ist true, wenn eine beliebige WCAG-Stufe erfüllt wird – nützlich für einen einfachen grün/roten Indikator.

Kontrastberechnung

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,
    }

Barrierefreie Alternativen vorschlagen

Eine nützliche Erweiterung: Für eine Vordergrundfarbe, die gegen den Hintergrund den Kontrast nicht besteht, die minimale Helligkeitsanpassung vorschlagen, die WCAG AA erreicht:

def suggest_accessible_foreground(fg_hex: str, bg_hex: str, target_ratio: float = 4.5) -> str:
    """Eine helligkeitsangepasste Version von fg_hex zurückgeben, die target_ratio besteht."""
    hsl = rgb_to_hsl(*hex_to_rgb(fg_hex))
    bg_lum = relative_luminance(bg_hex)
    is_dark_bg = bg_lum < 0.179

    # Helligkeit in die Richtung anpassen, die Kontrast erhöht
    step = -2 if is_dark_bg else 2  # Auf hellem Hg abdunkeln, auf dunklem aufhellen
    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

    # Maximalen Kontrast zurückgeben, wenn Ziel unerreichbar
    return '#000000' if not is_dark_bg else '#FFFFFF'

Rate-Limiting für Farb-APIs

Warum Farb-APIs Rate-Limiting benötigen

Farb-APIs mögen risikoarm erscheinen, können aber Ziel sein von: - Enumerations-Angriffen: Durchiterieren aller 16,7 Millionen Hex-Codes, um die vollständige benannte Farbdatenbank zu sammeln. - Scraping: Die eigene Konvertierungslogik als kostenlose Rechenressource nutzen. - Missbrauch: Hochvolumige Anfragen von einem einzelnen Client, die den Dienst für andere verschlechtern.

Implementierungsstrategie

Für eine Django-API bietet django-ratelimit Decorator-basiertes Rate-Limiting mit Redis-Backend:

# settings.py
RATELIMIT_USE_CACHE = 'default'  # Verwendet Djangos Cache-Backend (Redis empfohlen)

# 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):
    ...

Rate-Limit-Antwortformat

Wenn ein Rate-Limit überschritten wird, 429 Too Many Requests mit einem klaren Antwortkörper und einem Retry-After-Header zurückgeben:

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": "Sie haben das Rate-Limit von 100 Anfragen pro Minute überschritten.",
  "retry_after": 60
}

Abgestufte Rate-Limits

Eine sinnvolle abgestufte Struktur:

Stufe Limit Schlüssel
Anonym 60 Anfragen / Minute IP-Adresse
API-Schlüssel (kostenlos) 300 Anfragen / Minute API-Schlüssel
API-Schlüssel (kostenpflichtig) 3000 Anfragen / Minute API-Schlüssel

API-Schlüssel werden als Authorization: Bearer {key} oder X-API-Key: {key} übergeben. Den API-Schlüssel als Rate-Limit-Schlüssel für authentifizierte Anfragen verwenden:

@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 prüft beide; das lockerere Limit gewinnt für authentifizierte Anfragen
    ...

Best Practices für Antwortformate

Konsistentes Fehlerschema

Jede Fehlerantwort – Validierungsfehler, Rate-Limit, nicht gefunden – sollte demselben Schema folgen:

{
  "error": "invalid_hex",
  "message": "Der Wert 'gggggg' ist keine gültige Hex-Farbe. Erwartet wird ein 3- oder 6-stelliger Hex-String.",
  "field": "hex_code",
  "value": "gggggg"
}
Feld Zweck
error Maschinenlesbarer Fehlercode (snake_case)
message Menschenlesbare Erklärung
field Welches Eingabefeld den Fehler verursacht hat (für Validierungsfehler)
value Der ungültige Wert (hilft beim Debuggen)

Paginierung für Kollektionsendpunkte

Der Suchendpunkt mit limit und offset:

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

total immer einschließen, damit Clients Paginierungssteuerungen rendern können, ohne den vollständigen Ergebnissatz zu kennen.

Versionierungsstrategie

Die API im URL-Pfad versionieren, nicht in einem Header. URL-Versionierung ist explizit, von CDNs cachebar und erfordert nicht, dass Clients benutzerdefinierte Header setzen:

/api/v1/color/FF5733    # Version 1
/api/v2/color/FF5733    # Version 2 (wenn brechende Änderungen erforderlich sind)

Vorherige Versionen mindestens ein Jahr nach einem Major-Version-Release pflegen. Deprecation-Header auf alten Versionen verwenden:

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"

CORS-Konfiguration

Für eine öffentliche API, die von Browser-Clients verwendet wird:

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

# Oder für eine vollständig öffentliche API:
CORS_ALLOW_ALL_ORIGINS = True

# Methoden und Header immer einschränken
CORS_ALLOW_METHODS = ['GET', 'POST', 'OPTIONS']
CORS_ALLOW_HEADERS = ['Content-Type', 'Authorization', 'X-API-Key']

Caching-Architektur

Farbdaten sind hochgradig cachebar. Das Farbobjekt für #FF5733 ändert sich nie – die Konvertierungsmathematik ist deterministisch. Die Cache-Strategie entsprechend strukturieren:

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

Einen ETag-Header basierend auf dem Hex-Code für bedingte Anfragen hinzufügen:

response["ETag"] = f'"{hex_code}"'
response["Last-Modified"] = "Thu, 01 Jan 2026 00:00:00 GMT"  # Statisch – Inhalt ändert sich nie

Clients, die If-None-Match: "FF5733" senden, erhalten eine 304 Not Modified-Antwort ohne Body – spart Bandbreite bei wiederholten Anfragen.


Wichtigste Erkenntnisse

  • Das kanonische Farbobjekt sollte alle Formatdarstellungen (Hex, RGB, HSL, CMYK, OKLCH) in einer einzigen Antwort einschließen – Clients nie zwingen, mehrere Anfragen für verschiedene Formate zu stellen.
  • Hex-Eingaben aggressiv normalisieren: # entfernen, 3-stellig erweitern, Großbuchstaben. Mit einem Regex vor jeder Verarbeitung validieren.
  • Der Kontrast-Endpunkt ist das Herzstück jedes Barrierefreiheits-Toolings – WCAG-Verhältnisse, Bestanden/Nicht-bestanden pro Stufe und die höchste bestandene Stufe in einer einzigen Antwort zurückgeben.
  • Benannte Farbsuche funktioniert zuverlässig mit In-Memory-Präfixsuche für kleine Datenbanken; PostgreSQL pg_trgm für große verwenden.
  • Nach IP für nicht-authentifizierte Anfragen rate-limiten (60/min ist vernünftig); API-Schlüssel für höhere Stufen verwenden.
  • Jede Fehlerantwort muss demselben Schema mit error (maschinenlesbar) und message (menschenlesbar) mindestens folgen.
  • Farbdaten sind hochgradig cachebar – 24-Stunden-CDN-Cache ist für Konvertierungsendpunkte angemessen; ETag und stale-while-revalidate für effiziente Cache-Invalidierung verwenden.
  • Den Farbkonverter und Kontrastprüfer ausprobieren, um diese API-Endpunkte in Aktion zu sehen.

Ähnliche Farben

Ähnliche Marken

Ähnliche Werkzeuge