Entwurf einer Farb-API: REST-Endpunkte für Farbdaten
Embed This Widget
Add the script tag and a data attribute to embed this widget.
Embed via iframe for maximum compatibility.
<iframe src="https://colorfyi.com/iframe/entity//" width="420" height="400" frameborder="0" style="border:0;border-radius:10px;max-width:100%" loading="lazy"></iframe>
Paste this URL in WordPress, Medium, or any oEmbed-compatible platform.
https://colorfyi.com/entity//
Add a dynamic SVG badge to your README or docs.
[](https://colorfyi.com/entity//)
Use the native HTML custom element.
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:
- Client-seitiges Debouncing – Minimum 150 ms vor dem Abfeuern der Anfrage.
- Mindest-Abfragelänge festlegen – Abfragen kürzer als 2 Zeichen ablehnen (sofort leere Ergebnisse zurückgeben).
- Ergebnisse nach Abfrage cachen – Dieselbe Abfrage von verschiedenen Nutzern gibt dasselbe Ergebnis zurück.
Cache-Control: public, max-age=3600verwenden. - 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_trgmfü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) undmessage(menschenlesbar) mindestens folgen. - Farbdaten sind hochgradig cachebar – 24-Stunden-CDN-Cache ist für Konvertierungsendpunkte angemessen;
ETagundstale-while-revalidatefür effiziente Cache-Invalidierung verwenden. - Den Farbkonverter und Kontrastprüfer ausprobieren, um diese API-Endpunkte in Aktion zu sehen.