Tutoriels

Concevoir une API de couleurs : endpoints REST pour les données de couleur

11 min de lecture

Une API de couleurs semble être un sujet de niche jusqu'à ce que vous réfléchissiez au nombre d'outils qui consomment des données de couleur : des outils de conception qui ont besoin de conversion, des vérificateurs d'accessibilité qui vérifient le contraste, des composants de sélection de couleur qui ont besoin de recherche de couleurs nommées et des générateurs de palettes qui produisent des harmonies. Si vous construisez l'un de ces outils, ou si vous fournissez des données de couleur à d'autres développeurs, vous avez besoin d'une API REST bien conçue.

Ce guide couvre l'ensemble de la surface de conception : décisions de schéma, structure des endpoints, formats de réponse, gestion des erreurs et limitation de débit — du point de vue de quelqu'un qui a construit les endpoints qui alimentent le Convertisseur de couleurs et le Vérificateur de contraste chez ColorFYI.


Conception du schéma d'API pour les données de couleur

L'objet couleur de base

Une couleur a plusieurs représentations — hexadécimale, RGB, HSL, CMYK, OKLCH — et plusieurs champs de métadonnées. L'objet de réponse canonique doit les inclure tous, permettant aux clients d'utiliser le format dont ils ont besoin sans faire de requêtes supplémentaires :

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

Inclure is_dark (dérivé de la luminance) évite aux clients d'implémenter eux-mêmes le calcul de luminance WCAG. Inclure name et name_distance permet aux clients d'afficher la couleur nommée la plus proche sans un appel API séparé.

Conventions de nommage

Utilisez snake_case pour les clés JSON (cohérent avec la plupart des API REST). Imbriquez les champs liés dans des objets (rgb, hsl, cmyk) plutôt que de les aplatir (rgb_r, rgb_g, rgb_b). Les objets imbriqués sont plus lisibles, plus faciles à déstructurer en JavaScript et correspondent proprement aux interfaces typées :

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

Normalisation hexadécimale

Acceptez les couleurs hexadécimales dans plusieurs formats — avec ou sans #, à 3 ou 6 chiffres, en majuscules ou minuscules — et normalisez-les dans la couche API avant tout traitement :

# Exemple Python/Django
import re

def normalize_hex(raw: str) -> str:
    """Normalise l'entrée hexadécimale en 6 chiffres majuscules sans #."""
    clean = raw.strip().lstrip('#').upper()
    if not re.match(r'^[0-9A-F]{3}$|^[0-9A-F]{6}$', clean):
        raise ValueError(f"Invalid hex color: {raw!r}")
    # Développe 3 chiffres en 6 chiffres
    if len(clean) == 3:
        clean = ''.join(c * 2 for c in clean)
    return clean

Retournez le formulaire normalisé dans la réponse — cela élimine l'ambiguïté et rend la mise en cache en aval plus prévisible.


Endpoints de conversion Hex/RGB/HSL

GET /api/color/{hex}

L'endpoint principal retourne l'objet couleur complet pour un code hexadécimal donné. C'est ce que le Convertisseur de couleurs utilise pour remplir simultanément tous les champs de format :

GET /api/color/FF5733
GET /api/color/%23FF5733   (# encodé en URL)
GET /api/color/f57          (3 chiffres, minuscules)

Les trois doivent retourner la même réponse pour #FF5733.

Réponse :

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

Pour convertir des entrées arbitraires qui peuvent ne pas être hexadécimales — noms de couleurs CSS, triplets RGB, valeurs HSL :

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

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

Réponse : Même objet couleur complet.

Acceptez les valeurs from de : hex, rgb, hsl, cmyk, oklch, name. Validez que le format d'entrée correspond au type from déclaré et retournez un 422 avec un message d'erreur clair en cas de non-correspondance.

Exemple de ViewSet Django

# 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

Mettez en cache les données de couleur de manière agressive — un code hexadécimal donné produit toujours la même sortie, donc le cache peut être de très longue durée.


API de recherche de couleurs et d'autocomplétion

GET /api/search

La recherche de couleurs nommées alimente l'autocomplétion dans les sélecteurs de couleur et les champs de recherche. Le paramètre de requête est un nom de couleur partiel :

GET /api/search?q=coral
GET /api/search?q=teal&limit=5
GET /api/search?q=%230d&source=css   // Recherche par préfixe hexadécimal

Réponse :

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

Considérations d'implémentation de la recherche

Pour une petite base de données de couleurs nommées (~2 000 entrées), une recherche de préfixe en mémoire avec correspondance floue est suffisamment rapide :

from difflib import SequenceMatcher

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

    for color in NAMED_COLORS:  # Liste pré-chargée
        name = color["name"].lower()
        if q in name:
            # Correspondance de sous-chaîne exacte : score basé sur la position
            pos = name.index(q)
            score = 1.0 - (pos / len(name)) * 0.3
        else:
            # Correspondance floue
            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]

Pour les bases de données de plus de 10 000 entrées, utilisez l'extension pg_trgm de PostgreSQL avec indexation trigramme pour une recherche floue en sous-milliseconde :

-- Activer l'extension
CREATE EXTENSION IF NOT EXISTS pg_trgm;

-- Ajouter l'index
CREATE INDEX idx_color_name_trgm ON named_colors USING gin(name gin_trgm_ops);

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

Optimisation de l'autocomplétion

Pour une autocomplétion en direct lors de la saisie, optimisez pour une faible latence :

  1. Debounce côté client — 150 ms minimum avant de déclencher la requête.
  2. Définir une longueur minimale de requête — rejeter les requêtes de moins de 2 caractères (retourner immédiatement des résultats vides).
  3. Mettre en cache les résultats par requête — la même requête de différents utilisateurs retourne le même résultat. Utilisez Cache-Control: public, max-age=3600.
  4. Répondre rapidement en l'absence de résultats — répondre immédiatement avec {"results": [], "total": 0} plutôt que d'exécuter un scan flou complet.

Endpoint de vérification du contraste

GET /api/contrast

L'API de contraste est le moteur derrière le Vérificateur de contraste. Elle prend deux codes hexadécimaux et retourne le rapport de contraste WCAG avec le statut réussite/échec pour chaque niveau :

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

Réponse :

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

Le champ level retourne le niveau WCAG le plus élevé atteint par la paire ("AAA", "AA", "AA Large" ou "Fail"). Le champ passes est true si un niveau WCAG est atteint — utile pour un indicateur vert/rouge simple.

Calcul du contraste

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

Suggérer des alternatives accessibles

Une extension utile : étant donné une couleur de premier plan qui échoue au contraste par rapport au fond, suggérez l'ajustement de luminosité minimal qui atteint WCAG AA :

def suggest_accessible_foreground(fg_hex: str, bg_hex: str, target_ratio: float = 4.5) -> str:
    """Retourne une version de fg_hex ajustée en luminosité qui passe target_ratio."""
    hsl = rgb_to_hsl(*hex_to_rgb(fg_hex))
    bg_lum = relative_luminance(bg_hex)
    is_dark_bg = bg_lum < 0.179

    # Essayez d'ajuster la luminosité dans la direction qui augmente le contraste
    step = -2 if is_dark_bg else 2  # Assombrir sur fond clair, éclaircir sur fond sombre
    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

    # Retourner le contraste maximum si la cible est inaccessible
    return '#000000' if not is_dark_bg else '#FFFFFF'

Limitation de débit pour les API de couleurs

Pourquoi les API de couleurs ont besoin de limitation de débit

Les API de couleurs peuvent sembler à faible risque, mais elles peuvent être des cibles pour : - Attaques d'énumération : Itérer à travers les 16,7 millions de codes hexadécimaux pour collecter la base de données complète des couleurs nommées. - Scraping : Utiliser votre logique de conversion comme ressource computationnelle gratuite. - Abus : Requêtes à volume élevé d'un seul client qui dégradent le service pour les autres.

Stratégie d'implémentation

Pour une API Django, django-ratelimit fournit une limitation de débit basée sur des décorateurs avec un backend Redis :

# settings.py
RATELIMIT_USE_CACHE = 'default'  # Utilise le backend de cache de Django (Redis recommandé)

# 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 de réponse de limite de débit

Lorsqu'une limite de débit est dépassée, retournez 429 Too Many Requests avec un corps de réponse clair et un en-tête 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": "You have exceeded the rate limit of 100 requests per minute.",
  "retry_after": 60
}

Limites de débit à plusieurs niveaux

Une structure à plusieurs niveaux sensée :

Niveau Limite Clé
Anonyme 60 requêtes / minute Adresse IP
Clé API (gratuite) 300 requêtes / minute Clé API
Clé API (payante) 3 000 requêtes / minute Clé API

Les clés API sont transmises sous la forme Authorization: Bearer {clé} ou X-API-Key: {clé}. Utilisez la clé API comme clé de limite de débit pour les requêtes authentifiées :

@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 vérifie les deux ; la limite la plus souple gagne pour les requêtes authentifiées
    ...

Meilleures pratiques de format de réponse

Schéma d'erreur cohérent

Chaque réponse d'erreur — échec de validation, limite de débit, non trouvé — doit suivre le même schéma :

{
  "error": "invalid_hex",
  "message": "The value 'gggggg' is not a valid hex color. Expected a 3 or 6 digit hex string.",
  "field": "hex_code",
  "value": "gggggg"
}
Champ Objectif
error Code d'erreur lisible par la machine (snake_case)
message Explication lisible par l'humain
field Quel champ d'entrée a causé l'erreur (pour les erreurs de validation)
value La valeur invalide (aide au débogage)

Pagination pour les endpoints de collection

L'endpoint de recherche avec limit et offset :

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

Incluez toujours total pour que les clients puissent afficher les contrôles de pagination sans connaître l'ensemble complet de résultats.

Stratégie de versionnage

Versionnez votre API dans le chemin URL, pas dans un en-tête. Le versionnage par URL est explicite, mis en cache par les CDN et ne nécessite pas que les clients définissent des en-têtes personnalisés :

/api/v1/color/FF5733    # Version 1
/api/v2/color/FF5733    # Version 2 (quand des changements incompatibles sont nécessaires)

Maintenez les versions précédentes pendant au moins un an après une version majeure. Utilisez des en-têtes de dépréciation sur les anciennes versions :

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"

Configuration CORS

Pour une API publique consommée par des clients navigateur :

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

# Ou pour une API entièrement publique :
CORS_ALLOW_ALL_ORIGINS = True

# Restreignez toujours les méthodes et les en-têtes
CORS_ALLOW_METHODS = ['GET', 'POST', 'OPTIONS']
CORS_ALLOW_HEADERS = ['Content-Type', 'Authorization', 'X-API-Key']

Architecture de mise en cache

Les données de couleur sont hautement cachables. L'objet couleur pour #FF5733 ne changera jamais — les calculs de conversion sont déterministes. Structurez votre stratégie de cache en conséquence :

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

Ajoutez un en-tête ETag basé sur le code hexadécimal pour les requêtes conditionnelles :

response["ETag"] = f'"{hex_code}"'
response["Last-Modified"] = "Thu, 01 Jan 2026 00:00:00 GMT"  # Statique — le contenu ne change jamais

Les clients qui envoient If-None-Match: "FF5733" obtiennent une réponse 304 Not Modified sans corps — économisant de la bande passante sur les requêtes répétées.


Points clés

  • L'objet couleur canonique doit inclure toutes les représentations de format (hex, RGB, HSL, CMYK, OKLCH) dans une seule réponse — ne faites jamais faire plusieurs requêtes aux clients pour obtenir différents formats.
  • Normalisez l'entrée hexadécimale de manière agressive : supprimez #, développez 3 chiffres, mettez en majuscules. Validez avec une regex avant tout traitement.
  • L'endpoint de contraste est le cœur de tout outil d'accessibilité — retournez les rapports WCAG, le résultat réussite/échec par niveau et le niveau de passage le plus élevé dans une seule réponse.
  • La recherche de couleurs nommées fonctionne de manière fiable avec une recherche de préfixe en mémoire pour les petites bases de données ; utilisez pg_trgm de PostgreSQL pour les grandes.
  • Limitez le débit par IP pour les requêtes non authentifiées (60/min est raisonnable) ; utilisez des clés API pour des niveaux supérieurs.
  • Chaque réponse d'erreur doit suivre le même schéma avec error (lisible par la machine) et message (lisible par l'humain) au minimum.
  • Les données de couleur sont hautement cachables — un cache CDN de 24 heures est approprié pour les endpoints de conversion ; utilisez ETag et stale-while-revalidate pour une invalidation de cache efficace.
  • Essayez le Convertisseur de couleurs et le Vérificateur de contraste pour voir ces endpoints API en action.

Couleurs associées

Marques associées

Outils associés