Diseño de una API de color: Endpoints REST para datos de color
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.
Una API de color parece un nicho hasta que piensas en cuántas herramientas consumen datos de color: herramientas de diseño que necesitan conversión, verificadores de accesibilidad que validan el contraste, componentes de selector de color que necesitan búsqueda de colores con nombre, y generadores de paletas que producen armonías. Si estás construyendo alguna de estas, o proporcionando datos de color a otros desarrolladores, necesitas una API REST bien diseñada.
Esta guía cubre toda la superficie de diseño: decisiones de esquema, estructura de endpoints, formatos de respuesta, manejo de errores y limitación de velocidad — desde la perspectiva de alguien que ha construido los endpoints que impulsan el Convertidor de Color y el Verificador de Contraste en ColorFYI.
Diseño del esquema API para datos de color
El objeto de color principal
Un color tiene múltiples representaciones — hex, RGB, HSL, CMYK, OKLCH — y múltiples campos de metadatos. El objeto de respuesta canónico debe incluirlos todos, permitiendo a los clientes usar el formato que necesiten sin realizar solicitudes adicionales:
{
"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
}
Incluir is_dark (derivado de la luminancia) evita que los clientes implementen el cálculo de luminancia WCAG por su cuenta. Incluir name y name_distance permite a los clientes mostrar el color con nombre más cercano sin una llamada API adicional.
Convenciones de nomenclatura
Usa snake_case para las claves JSON (consistente con la mayoría de las APIs REST). Anida los campos relacionados en objetos (rgb, hsl, cmyk) en lugar de aplanarlos (rgb_r, rgb_g, rgb_b). Los objetos anidados son más legibles, más fáciles de desestructurar en JavaScript y se mapean limpiamente a interfaces tipadas:
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;
}
Normalización de hex
Acepta colores hex en múltiples formatos — con o sin #, de 3 o 6 dígitos, en mayúsculas o minúsculas — y normalízalos en la capa de la API antes de cualquier procesamiento:
# Ejemplo Python/Django
import re
def normalize_hex(raw: str) -> str:
"""Normaliza la entrada hex a 6 dígitos en mayúsculas sin #."""
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}")
# Expande de 3 dígitos a 6 dígitos
if len(clean) == 3:
clean = ''.join(c * 2 for c in clean)
return clean
Devuelve la forma normalizada en la respuesta — esto elimina la ambigüedad y hace el almacenamiento en caché posterior más predecible.
Endpoints de conversión Hex/RGB/HSL
GET /api/color/{hex}
El endpoint primario devuelve el objeto de color completo para un código hex dado. Esto es lo que usa el Convertidor de Color para poblar todos los campos de formato simultáneamente:
GET /api/color/FF5733
GET /api/color/%23FF5733 (# codificado en URL)
GET /api/color/f57 (3 dígitos, minúsculas)
Los tres deberían devolver la misma respuesta para #FF5733.
Respuesta:
{
"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
Para convertir entradas arbitrarias que pueden no ser hex — nombres de color CSS, triples RGB, valores HSL:
POST /api/convert
Content-Type: application/json
{
"input": "rgb(255, 87, 51)",
"from": "rgb"
}
Respuesta: El mismo objeto de color completo.
Acepta valores from de: hex, rgb, hsl, cmyk, oklch, name. Valida que el formato de entrada coincida con el tipo from declarado y devuelve un error 422 con un mensaje claro en caso de discrepancia.
Ejemplo de ViewSet en 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
Almacena en caché los datos de color de forma agresiva — un código hex dado siempre produce el mismo resultado, por lo que la caché puede tener una vida muy larga.
API de búsqueda y autocompletado de color
GET /api/search
La búsqueda de color con nombre impulsa el autocompletado en selectores de color y campos de búsqueda. El parámetro de consulta es un nombre de color parcial:
GET /api/search?q=coral
GET /api/search?q=teal&limit=5
GET /api/search?q=%230d&source=css // Buscar por prefijo hex
Respuesta:
{
"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"
}
Consideraciones de implementación de búsqueda
Para una base de datos de colores con nombre pequeña (~2,000 entradas), una búsqueda de prefijos en memoria con coincidencia difusa es suficientemente rápida:
from difflib import SequenceMatcher
def search_colors(query: str, limit: int = 10) -> list[dict]:
q = query.lower().strip()
results = []
for color in NAMED_COLORS: # Lista precargada
name = color["name"].lower()
if q in name:
# Coincidencia exacta de subcadena: puntaje basado en posición
pos = name.index(q)
score = 1.0 - (pos / len(name)) * 0.3
else:
# Coincidencia difusa
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]
Para bases de datos de más de 10,000 entradas, usa la extensión pg_trgm de PostgreSQL con indexación de trigramas para búsqueda difusa de menos de un milisegundo:
-- Habilitar extensión
CREATE EXTENSION IF NOT EXISTS pg_trgm;
-- Agregar índice
CREATE INDEX idx_color_name_trgm ON named_colors USING gin(name gin_trgm_ops);
-- Consulta
SELECT hex, name, source, similarity(name, 'coral') AS score
FROM named_colors
WHERE name % 'coral'
ORDER BY score DESC
LIMIT 10;
Optimización del autocompletado
Para un autocompletado de búsqueda en tiempo real mientras se escribe, optimiza para baja latencia:
- Debounce en el cliente — mínimo 150ms antes de disparar la solicitud.
- Establece una longitud mínima de consulta — rechaza consultas de menos de 2 caracteres (devuelve resultados vacíos inmediatamente).
- Almacena en caché los resultados por consulta — la misma consulta de diferentes usuarios devuelve el mismo resultado. Usa
Cache-Control: public, max-age=3600. - Responde rápidamente cuando no hay resultados — responde inmediatamente con
{"results": [], "total": 0}en lugar de ejecutar una búsqueda difusa completa.
Endpoint de verificación de contraste
GET /api/contrast
La API de contraste es el motor detrás del Verificador de Contraste. Toma dos códigos hex y devuelve la relación de contraste WCAG con el estado de aprobación/fallo para cada nivel:
GET /api/contrast?fg=FF5733&bg=FFFFFF
GET /api/contrast?fg=000000&bg=FFFFFF
Respuesta:
{
"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
}
El campo level devuelve el nivel WCAG más alto que logra el par ("AAA", "AA", "AA Large" o "Fail"). El campo passes es true si se cumple cualquier nivel WCAG — útil para un indicador verde/rojo simple.
Cálculo de 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,
}
Sugerencia de alternativas accesibles
Una extensión útil: dado un color de primer plano que falla el contraste contra el fondo, sugiere el ajuste mínimo de luminosidad que logra WCAG AA:
def suggest_accessible_foreground(fg_hex: str, bg_hex: str, target_ratio: float = 4.5) -> str:
"""Devuelve una versión con luminosidad ajustada de fg_hex que pasa target_ratio."""
hsl = rgb_to_hsl(*hex_to_rgb(fg_hex))
bg_lum = relative_luminance(bg_hex)
is_dark_bg = bg_lum < 0.179
# Intenta ajustar la luminosidad en la dirección que aumenta el contraste
step = -2 if is_dark_bg else 2 # Oscurecer en fondo claro, aclarar en fondo oscuro
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
# Devuelve el máximo contraste si el objetivo no es alcanzable
return '#000000' if not is_dark_bg else '#FFFFFF'
Limitación de velocidad para APIs de color
Por qué las APIs de color necesitan limitación de velocidad
Las APIs de color pueden parecer de bajo riesgo, pero pueden ser objetivo de: - Ataques de enumeración: Iterar a través de los 16.7 millones de códigos hex para extraer la base de datos completa de nombres de color. - Scraping: Usar tu lógica de conversión como un recurso computacional gratuito. - Abuso: Solicitudes de alto volumen desde un único cliente que degradan el servicio para otros.
Estrategia de implementación
Para una API de Django, django-ratelimit proporciona limitación de velocidad basada en decoradores con backend de Redis:
# settings.py
RATELIMIT_USE_CACHE = 'default' # Usa el backend de caché de Django (Redis recomendado)
# 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):
...
Formato de respuesta de límite de velocidad
Cuando se excede un límite de velocidad, devuelve 429 Too Many Requests con un cuerpo de respuesta claro y un encabezado 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": "Has superado el límite de velocidad de 100 solicitudes por minuto.",
"retry_after": 60
}
Límites de velocidad escalonados
Una estructura escalonada razonable:
| Nivel | Límite | Clave |
|---|---|---|
| Anónimo | 60 solicitudes / minuto | Dirección IP |
| Clave API (gratis) | 300 solicitudes / minuto | Clave API |
| Clave API (pago) | 3000 solicitudes / minuto | Clave API |
Las claves API se pasan como Authorization: Bearer {clave} o X-API-Key: {clave}. Usa la clave API como clave de límite de velocidad para solicitudes autenticadas:
@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 verifica ambos; el límite más flexible gana para solicitudes autenticadas
...
Mejores prácticas para formatos de respuesta
Esquema de error consistente
Cada respuesta de error — fallo de validación, límite de velocidad, no encontrado — debe seguir el mismo esquema:
{
"error": "invalid_hex",
"message": "El valor 'gggggg' no es un color hex válido. Se esperaba una cadena hex de 3 o 6 dígitos.",
"field": "hex_code",
"value": "gggggg"
}
| Campo | Propósito |
|---|---|
error |
Código de error legible por máquina (snake_case) |
message |
Explicación legible por humanos |
field |
Qué campo de entrada causó el error (para errores de validación) |
value |
El valor inválido (ayuda en la depuración) |
Paginación para endpoints de colección
El endpoint de búsqueda con limit y offset:
{
"results": [...],
"pagination": {
"total": 47,
"limit": 10,
"offset": 0,
"next": "/api/search?q=blue&limit=10&offset=10",
"prev": null
}
}
Incluye siempre total para que los clientes puedan renderizar controles de paginación sin conocer el conjunto de resultados completo.
Estrategia de versiones
Versiona tu API en la ruta de URL, no en un encabezado. El versionado de URL es explícito, almacenable en caché por CDN y no requiere que los clientes establezcan encabezados personalizados:
/api/v1/color/FF5733 # Versión 1
/api/v2/color/FF5733 # Versión 2 (cuando se necesitan cambios que rompen la compatibilidad)
Mantén las versiones anteriores durante al menos un año después de un lanzamiento de versión principal. Usa encabezados de deprecación en versiones antiguas:
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"
Configuración de CORS
Para una API pública consumida por clientes de navegador:
# settings.py (usando django-cors-headers)
CORS_ALLOWED_ORIGINS = [
"https://yoursite.com",
"https://app.yoursite.com",
]
# O para una API completamente pública:
CORS_ALLOW_ALL_ORIGINS = True
# Siempre restringe métodos y encabezados
CORS_ALLOW_METHODS = ['GET', 'POST', 'OPTIONS']
CORS_ALLOW_HEADERS = ['Content-Type', 'Authorization', 'X-API-Key']
Arquitectura de caché
Los datos de color son altamente almacenables en caché. El objeto de color para #FF5733 nunca cambiará — las matemáticas de conversión son deterministas. Estructura tu estrategia de caché en consecuencia:
| Endpoint | Cache-Control | TTL de CDN |
|---|---|---|
/api/color/{hex} |
public, max-age=86400, stale-while-revalidate=604800 |
1 día |
/api/search?q={q} |
public, max-age=3600 |
1 hora |
/api/contrast?fg=X&bg=Y |
public, max-age=86400 |
1 día |
/api/palette/{hex} |
public, max-age=86400 |
1 día |
Agrega un encabezado ETag basado en el código hex para solicitudes condicionales:
response["ETag"] = f'"{hex_code}"'
response["Last-Modified"] = "Thu, 01 Jan 2026 00:00:00 GMT" # Estático — el contenido nunca cambia
Los clientes que envíen If-None-Match: "FF5733" reciben una respuesta 304 Not Modified sin cuerpo — ahorrando ancho de banda en solicitudes repetidas.
Puntos clave
- El objeto de color canónico debe incluir todas las representaciones de formato (hex, RGB, HSL, CMYK, OKLCH) en una sola respuesta — nunca hagas que los clientes realicen múltiples solicitudes para obtener diferentes formatos.
- Normaliza la entrada hex de forma agresiva: elimina
#, expande los de 3 dígitos, convierte a mayúsculas. Valida con una expresión regular antes de cualquier procesamiento. - El endpoint de contraste es el núcleo de cualquier herramienta de accesibilidad — devuelve relaciones WCAG, aprobación/fallo por nivel y el nivel de aprobación más alto en una sola respuesta.
- La búsqueda de color con nombre funciona de forma confiable con búsqueda de prefijos en memoria para bases de datos pequeñas; usa PostgreSQL
pg_trgmpara las grandes. - Limita la velocidad por IP para solicitudes no autenticadas (60/min es razonable); usa claves API para niveles más altos.
- Cada respuesta de error debe seguir el mismo esquema con
error(legible por máquina) ymessage(legible por humanos) como mínimo. - Los datos de color son altamente almacenables en caché — la caché de CDN de 24 horas es apropiada para los endpoints de conversión; usa
ETagystale-while-revalidatepara una invalidación de caché eficiente. - Prueba el Convertidor de Color y el Verificador de Contraste para ver estos endpoints de API en acción.
Colores relacionados
Marcas relacionadas
Herramientas relacionadas
Convertidor de color
Convierte entre los formatos de color HEX, RGB, HSL, HSV, CMYK y OKLCH al instante.
Verificador de contraste
Verifica las relaciones de contraste de color según las directrices WCAG 2.1. Prueba el cumplimiento AA y AAA para texto normal y grande.