Tutoriais

Projetando uma API de Cores: Endpoints REST para Dados de Cores

10 min de leitura

Uma API de cores parece nichada até você pensar em quantas ferramentas consomem dados de cores: ferramentas de design que precisam de conversão, verificadores de acessibilidade que verificam contraste, componentes de seletor de cores que precisam de pesquisa de cores nomeadas e geradores de paletas que produzem harmonias. Se você está construindo qualquer uma dessas, ou fornecendo dados de cores para outros desenvolvedores, precisa de uma API REST bem projetada.

Este guia cobre toda a superfície de design: decisões de esquema, estrutura de endpoint, formatos de resposta, tratamento de erros e limitação de taxa — da perspectiva de alguém que construiu os endpoints que alimentam o Conversor de Cores e o Verificador de Contraste no ColorFYI.


Design de Esquema da API para Dados de Cores

O Objeto Core de Cor

Uma cor tem múltiplas representações — hex, RGB, HSL, CMYK, OKLCH — e múltiplos campos de metadados. O objeto de resposta canônico deve incluir todos eles, permitindo que os clientes usem qualquer formato de que precisam sem fazer solicitações adicionais:

{
  "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 luminância) poupa os clientes de implementar o cálculo de luminância WCAG por conta própria. Incluir name e name_distance permite que os clientes mostrem a cor nomeada mais próxima sem uma chamada de API separada.

Convenções de Nomenclatura

Use snake_case para chaves JSON (consistente com a maioria das APIs REST). Aninhe campos relacionados em objetos (rgb, hsl, cmyk) em vez de achatá-los (rgb_r, rgb_g, rgb_b). Objetos aninhados são mais legíveis, mais fáceis de desestruturar em JavaScript e mapeiam bem para 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;
}

Normalização de Hex

Aceite cores hex em múltiplos formatos — com ou sem #, 3 ou 6 dígitos, maiúsculas ou minúsculas — e normalize-as na camada da API antes de qualquer processamento:

# Exemplo Python/Django
import re

def normalize_hex(raw: str) -> str:
    """Normaliza entrada hex para 6 dígitos maiúsculos sem #."""
    clean = raw.strip().lstrip('#').upper()
    if not re.match(r'^[0-9A-F]{3}$|^[0-9A-F]{6}$', clean):
        raise ValueError(f"Cor hex inválida: {raw!r}")
    # Expande 3 dígitos para 6 dígitos
    if len(clean) == 3:
        clean = ''.join(c * 2 for c in clean)
    return clean

Retorne a forma normalizada na resposta — isso elimina ambiguidade e torna o cache downstream mais previsível.


Endpoints de Conversão Hex/RGB/HSL

GET /api/color/{hex}

O endpoint principal retorna o objeto de cor completo para um código hex fornecido. É isso que o Conversor de Cores usa para preencher todos os campos de formato simultaneamente:

GET /api/color/FF5733
GET /api/color/%23FF5733   (# com codificação URL)
GET /api/color/f57          (3 dígitos, minúsculas)

Os três devem retornar a mesma resposta para #FF5733.

Resposta:

{
  "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 converter entradas arbitrárias que podem não ser hex — nomes de cores CSS, triplas RGB, valores HSL:

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

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

Resposta: Mesmo objeto de cor completo.

Aceite valores from de: hex, rgb, hsl, cmyk, oklch, name. Valide se o formato da entrada corresponde ao tipo from declarado e retorne 422 com uma mensagem de erro clara em caso de incompatibilidade.

Exemplo 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

Cache os dados de cor agressivamente — um determinado código hex sempre produz o mesmo resultado, então o cache pode ter vida muito longa.


API de Busca de Cores e Autocomplete

GET /api/search

A busca de cores nomeadas alimenta o autocomplete em seletores de cores e campos de pesquisa. O parâmetro de consulta é um nome de cor parcial:

GET /api/search?q=coral
GET /api/search?q=teal&limit=5
GET /api/search?q=%230d&source=css   // Busca por prefixo hex

Resposta:

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

Considerações de Implementação de Busca

Para um banco de dados pequeno de cores nomeadas (~2.000 entradas), uma busca de prefixo em memória com correspondência fuzzy é 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 pré-carregada
        name = color["name"].lower()
        if q in name:
            # Correspondência exata de substring: pontuação baseada na posição
            pos = name.index(q)
            score = 1.0 - (pos / len(name)) * 0.3
        else:
            # Correspondência 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]

Para bancos de dados maiores que 10.000 entradas, use a extensão pg_trgm do PostgreSQL com indexação de trigrama para busca fuzzy em sub-milissegundos:

-- Habilitar extensão
CREATE EXTENSION IF NOT EXISTS pg_trgm;

-- Adicionar í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;

Otimização de Autocomplete

Para um autocomplete de busca em tempo real enquanto digita, otimize para baixa latência:

  1. Debounce no cliente — mínimo de 150ms antes de disparar a solicitação.
  2. Defina um comprimento mínimo de consulta — rejeite consultas com menos de 2 caracteres (retorne resultados vazios imediatamente).
  3. Resultados de cache por consulta — a mesma consulta de diferentes usuários retorna o mesmo resultado. Use Cache-Control: public, max-age=3600.
  4. Retorne rapidamente em caso de sem resultados — responda imediatamente com {"results": [], "total": 0} em vez de executar uma varredura fuzzy completa.

Endpoint de Verificação de Contraste

GET /api/contrast

A API de contraste é o motor por trás do Verificador de Contraste. Ela recebe dois códigos hex e retorna a taxa de contraste WCAG com status de aprovação/reprovação para cada nível:

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

Resposta:

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

O campo level retorna o nível WCAG mais alto que o par atinge ("AAA", "AA", "AA Large" ou "Fail"). O campo passes é true se qualquer nível WCAG for atendido — útil para um indicador simples verde/vermelho.

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

Sugerindo Alternativas Acessíveis

Uma extensão útil: dado uma cor de primeiro plano que falha no contraste em relação ao fundo, sugira o ajuste mínimo de luminosidade que alcança o WCAG AA:

def suggest_accessible_foreground(fg_hex: str, bg_hex: str, target_ratio: float = 4.5) -> str:
    """Retorna uma versão ajustada de luminosidade de fg_hex que passa na target_ratio."""
    hsl = rgb_to_hsl(*hex_to_rgb(fg_hex))
    bg_lum = relative_luminance(bg_hex)
    is_dark_bg = bg_lum < 0.179

    # Tenta ajustar luminosidade na direção que aumenta o contraste
    step = -2 if is_dark_bg else 2  # Escurece em fundo claro, clareia em fundo escuro
    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

    # Retorna contraste máximo se o alvo for inatingível
    return '#000000' if not is_dark_bg else '#FFFFFF'

Limitação de Taxa para APIs de Cores

Por Que APIs de Cores Precisam de Limitação de Taxa

APIs de cores podem parecer de baixo risco, mas podem ser alvos de: - Ataques de enumeração: Iterar por todos os 16,7 milhões de códigos hex para coletar o banco de dados completo de nomes de cores. - Scraping: Usar sua lógica de conversão como um recurso computacional gratuito. - Abuso: Solicitações de alto volume de um único cliente que degradam o serviço para outros.

Estratégia de Implementação

Para uma API Django, o django-ratelimit fornece limitação de taxa baseada em decorator com backend Redis:

# settings.py
RATELIMIT_USE_CACHE = 'default'  # Usa o backend de cache do 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 Resposta de Limite de Taxa

Quando um limite de taxa é excedido, retorne 429 Too Many Requests com um corpo de resposta claro e um cabeçalho 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": "Você excedeu o limite de taxa de 100 solicitações por minuto.",
  "retry_after": 60
}

Limites de Taxa em Camadas

Uma estrutura em camadas sensata:

Camada Limite Chave
Anônimo 60 solicitações / minuto Endereço IP
Chave de API (gratuita) 300 solicitações / minuto Chave de API
Chave de API (paga) 3000 solicitações / minuto Chave de API

Chaves de API são passadas como Authorization: Bearer {chave} ou X-API-Key: {chave}. Use a chave de API como chave de limite de taxa para solicitações 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; o limite mais frouxo vence para solicitações autenticadas
    ...

Boas Práticas de Formato de Resposta

Esquema de Erro Consistente

Cada resposta de erro — falha de validação, limite de taxa, não encontrado — deve seguir o mesmo esquema:

{
  "error": "invalid_hex",
  "message": "O valor 'gggggg' não é uma cor hex válida. Era esperada uma string hex de 3 ou 6 dígitos.",
  "field": "hex_code",
  "value": "gggggg"
}
Campo Finalidade
error Código de erro legível por máquina (snake_case)
message Explicação legível por humano
field Qual campo de entrada causou o erro (para erros de validação)
value O valor inválido (ajuda na depuração)

Paginação para Endpoints de Coleção

O endpoint de busca com limit e offset:

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

Sempre inclua total para que os clientes possam renderizar controles de paginação sem conhecer o conjunto completo de resultados.

Estratégia de Versionamento

Versione sua API no caminho da URL, não em um cabeçalho. O versionamento de URL é explícito, cacheável por CDNs e não requer que os clientes definam cabeçalhos personalizados:

/api/v1/color/FF5733    # Versão 1
/api/v2/color/FF5733    # Versão 2 (quando mudanças disruptivas forem necessárias)

Mantenha versões anteriores por pelo menos um ano após um lançamento de versão principal. Use cabeçalhos de depreciação em versões antigas:

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"

Configuração CORS

Para uma API pública consumida por clientes de navegador:

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

# Ou para uma API totalmente pública:
CORS_ALLOW_ALL_ORIGINS = True

# Sempre restrinja métodos e cabeçalhos
CORS_ALLOW_METHODS = ['GET', 'POST', 'OPTIONS']
CORS_ALLOW_HEADERS = ['Content-Type', 'Authorization', 'X-API-Key']

Arquitetura de Cache

Os dados de cores são altamente cacheáveis. O objeto de cor para #FF5733 nunca mudará — a matemática de conversão é determinística. Estruture sua estratégia de cache de acordo:

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

Adicione um cabeçalho ETag baseado no código hex para solicitações condicionais:

response["ETag"] = f'"{hex_code}"'
response["Last-Modified"] = "Thu, 01 Jan 2026 00:00:00 GMT"  # Estático — o conteúdo nunca muda

Clientes que enviam If-None-Match: "FF5733" recebem uma resposta 304 Not Modified sem corpo — economizando largura de banda em solicitações repetidas.


Conclusões Principais

  • O objeto de cor canônico deve incluir todas as representações de formato (hex, RGB, HSL, CMYK, OKLCH) em uma única resposta — nunca faça os clientes fazerem múltiplas solicitações para obter formatos diferentes.
  • Normalize a entrada hex agressivamente: remova #, expanda para 3 dígitos, converta para maiúsculas. Valide com regex antes de qualquer processamento.
  • O endpoint de contraste é o núcleo de qualquer ferramental de acessibilidade — retorne taxas WCAG, aprovação/reprovação por nível e o nível de aprovação mais alto em uma única resposta.
  • A busca de cores nomeadas funciona de forma confiável com busca de prefixo em memória para bancos de dados pequenos; use pg_trgm do PostgreSQL para os maiores.
  • Limite a taxa por IP para solicitações não autenticadas (60/min é razoável); use chaves de API para camadas mais altas.
  • Cada resposta de erro deve seguir o mesmo esquema com error (legível por máquina) e message (legível por humano) no mínimo.
  • Os dados de cores são altamente cacheáveis — o cache CDN de 24 horas é apropriado para endpoints de conversão; use ETag e stale-while-revalidate para invalidação eficiente de cache.
  • Experimente o Conversor de Cores e o Verificador de Contraste para ver esses endpoints de API em ação.

Cores relacionadas

Marcas relacionadas

Ferramentas relacionadas