Hướng dẫn Thực hành

Thiết kế API Màu sắc: REST Endpoints cho Dữ liệu Màu

Đọc 12 phút

Một API màu sắc có vẻ hẹp cho đến khi bạn nghĩ đến bao nhiêu công cụ tiêu thụ dữ liệu màu: các công cụ thiết kế cần chuyển đổi, trình kiểm tra khả năng tiếp cận xác minh độ tương phản, các thành phần bộ chọn màu cần tìm kiếm màu có tên, và các trình tạo bảng màu tạo ra sự hài hòa. Nếu bạn đang xây dựng bất kỳ điều nào trong số này, hoặc cung cấp dữ liệu màu cho các nhà phát triển khác, bạn cần một REST API được thiết kế tốt.

Hướng dẫn này bao gồm toàn bộ bề mặt thiết kế: quyết định schema, cấu trúc endpoint, định dạng phản hồi, xử lý lỗi và giới hạn tốc độ — từ góc nhìn của người đã xây dựng các endpoints cung cấp sức mạnh cho Công cụ Chuyển đổi MàuCông cụ Kiểm tra Độ tương phản tại ColorFYI.


Thiết kế Schema API cho Dữ liệu Màu

Đối tượng Màu cốt lõi

Một màu sắc có nhiều cách biểu diễn — hex, RGB, HSL, CMYK, OKLCH — và nhiều trường metadata. Đối tượng phản hồi chuẩn nên bao gồm tất cả, cho phép client sử dụng bất kỳ định dạng nào họ cần mà không cần thêm request:

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

Bao gồm is_dark (được lấy từ luminance) giúp client không phải tự triển khai tính toán luminance WCAG. Bao gồm namename_distance cho phép client hiển thị màu có tên gần nhất mà không cần gọi API riêng.

Quy ước Đặt tên

Sử dụng snake_case cho các key JSON (nhất quán với hầu hết REST API). Lồng các trường liên quan vào các đối tượng (rgb, hsl, cmyk) thay vì làm phẳng chúng (rgb_r, rgb_g, rgb_b). Các đối tượng lồng nhau dễ đọc hơn, dễ destructure hơn trong JavaScript, và ánh xạ gọn gàng vào các interface có kiểu:

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

Chuẩn hóa Hex

Chấp nhận màu hex ở nhiều định dạng — có hoặc không có #, 3 chữ số hoặc 6 chữ số, chữ hoa hoặc chữ thường — và chuẩn hóa chúng ở tầng API trước khi xử lý:

# Ví dụ Python/Django
import re

def normalize_hex(raw: str) -> str:
    """Chuẩn hóa đầu vào hex thành 6 chữ số viết hoa không có #."""
    clean = raw.strip().lstrip('#').upper()
    if not re.match(r'^[0-9A-F]{3}$|^[0-9A-F]{6}$', clean):
        raise ValueError(f"Màu hex không hợp lệ: {raw!r}")
    # Mở rộng 3 chữ số thành 6 chữ số
    if len(clean) == 3:
        clean = ''.join(c * 2 for c in clean)
    return clean

Trả về dạng đã chuẩn hóa trong phản hồi — điều này loại bỏ sự mơ hồ và làm cho việc cache downstream dễ dự đoán hơn.


Endpoints Chuyển đổi Hex/RGB/HSL

GET /api/color/{hex}

Endpoint chính trả về đối tượng màu đầy đủ cho một mã hex. Đây là thứ Công cụ Chuyển đổi Màu sử dụng để điền vào tất cả các trường định dạng cùng một lúc:

GET /api/color/FF5733
GET /api/color/%23FF5733   (# được mã hóa URL)
GET /api/color/f57          (3 chữ số, chữ thường)

Cả ba đều nên trả về cùng một phản hồi cho #FF5733.

Phản hồi:

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

Để chuyển đổi các đầu vào tùy ý có thể không phải hex — tên màu CSS, bộ ba RGB, giá trị HSL:

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

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

Phản hồi: Cùng đối tượng màu đầy đủ.

Chấp nhận các giá trị from: hex, rgb, hsl, cmyk, oklch, name. Xác thực định dạng đầu vào khớp với kiểu from được khai báo và trả về 422 với thông báo lỗi rõ ràng khi không khớp.

Ví dụ Django ViewSet

# 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 dữ liệu màu một cách mạnh mẽ — một mã hex nhất định luôn tạo ra cùng một đầu ra, vì vậy cache có thể tồn tại rất lâu.


API Tìm kiếm và Gợi ý Tự động Màu

GET /api/search

Tìm kiếm màu có tên cung cấp sức mạnh cho tính năng gợi ý tự động trong bộ chọn màu và trường tìm kiếm. Tham số truy vấn là tên màu một phần:

GET /api/search?q=coral
GET /api/search?q=teal&limit=5
GET /api/search?q=%230d&source=css   // Tìm kiếm theo tiền tố hex

Phản hồi:

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

Cân nhắc về Triển khai Tìm kiếm

Đối với cơ sở dữ liệu màu có tên nhỏ (~2.000 mục), tìm kiếm tiền tố trong bộ nhớ với fuzzy matching đủ nhanh:

from difflib import SequenceMatcher

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

    for color in NAMED_COLORS:  # Danh sách được tải sẵn
        name = color["name"].lower()
        if q in name:
            # Khớp chuỗi con chính xác: điểm dựa trên vị trí
            pos = name.index(q)
            score = 1.0 - (pos / len(name)) * 0.3
        else:
            # Fuzzy matching
            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]

Đối với cơ sở dữ liệu lớn hơn 10.000 mục, sử dụng extension pg_trgm của PostgreSQL với trigram indexing để tìm kiếm fuzzy dưới mili giây:

-- Bật extension
CREATE EXTENSION IF NOT EXISTS pg_trgm;

-- Thêm index
CREATE INDEX idx_color_name_trgm ON named_colors USING gin(name gin_trgm_ops);

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

Tối ưu hóa Gợi ý Tự động

Để tìm kiếm tức thì khi gõ, tối ưu hóa cho độ trễ thấp:

  1. Debounce ở phía client — Tối thiểu 150ms trước khi gửi request.
  2. Đặt độ dài truy vấn tối thiểu — Từ chối các truy vấn ngắn hơn 2 ký tự (trả về kết quả trống ngay lập tức).
  3. Cache kết quả theo truy vấn — Cùng một truy vấn từ nhiều người dùng khác nhau trả về cùng một kết quả. Sử dụng Cache-Control: public, max-age=3600.
  4. Phản hồi nhanh khi không có kết quả — Phản hồi ngay lập tức với {"results": [], "total": 0} thay vì chạy quét fuzzy đầy đủ.

Endpoint Kiểm tra Độ tương phản

GET /api/contrast

API độ tương phản là động cơ phía sau Công cụ Kiểm tra Độ tương phản. Nó nhận hai mã hex và trả về tỷ lệ tương phản WCAG với trạng thái đạt/không đạt cho mỗi cấp độ:

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

Phản hồi:

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

Trường level trả về cấp độ WCAG cao nhất mà cặp màu đạt được ("AAA", "AA", "AA Large" hoặc "Fail"). Trường passestrue nếu bất kỳ cấp độ WCAG nào được đáp ứng — hữu ích cho chỉ báo xanh/đỏ đơn giản.

Tính toán Độ tương phản

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

Đề xuất các Lựa chọn thay thế Có thể Tiếp cận

Một tiện ích mở rộng hữu ích: khi màu chữ không đạt độ tương phản với nền, đề xuất điều chỉnh độ sáng tối thiểu để đạt WCAG AA:

def suggest_accessible_foreground(fg_hex: str, bg_hex: str, target_ratio: float = 4.5) -> str:
    """Trả về phiên bản fg_hex đã điều chỉnh độ sáng để đạt target_ratio."""
    hsl = rgb_to_hsl(*hex_to_rgb(fg_hex))
    bg_lum = relative_luminance(bg_hex)
    is_dark_bg = bg_lum < 0.179

    step = -2 if is_dark_bg else 2  # Tối hơn trên nền sáng, sáng hơn trên nền tối
    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

    return '#000000' if not is_dark_bg else '#FFFFFF'

Giới hạn Tốc độ cho API Màu

Tại sao API Màu cần Giới hạn Tốc độ

API màu có vẻ ít rủi ro, nhưng chúng có thể là mục tiêu của: - Tấn công liệt kê: Lặp qua tất cả 16,7 triệu mã hex để thu hoạch toàn bộ cơ sở dữ liệu tên màu. - Scraping: Sử dụng logic chuyển đổi của bạn như một tài nguyên tính toán miễn phí. - Lạm dụng: Các request khối lượng lớn từ một client duy nhất làm giảm chất lượng dịch vụ cho người khác.

Chiến lược Triển khai

Đối với Django API, django-ratelimit cung cấp giới hạn tốc độ dựa trên decorator với backend Redis:

# settings.py
RATELIMIT_USE_CACHE = 'default'  # Sử dụng backend cache của Django (Redis được khuyến nghị)

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

Định dạng Phản hồi Giới hạn Tốc độ

Khi giới hạn tốc độ bị vượt quá, trả về 429 Too Many Requests với nội dung phản hồi rõ ràng và header 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": "Bạn đã vượt quá giới hạn tốc độ 100 request mỗi phút.",
  "retry_after": 60
}

Giới hạn Tốc độ Theo tầng

Cấu trúc theo tầng hợp lý:

Tầng Giới hạn Key
Ẩn danh 60 request / phút Địa chỉ IP
API key (miễn phí) 300 request / phút API key
API key (trả phí) 3000 request / phút API key

API key được truyền qua Authorization: Bearer {key} hoặc X-API-Key: {key}. Sử dụng API key làm key giới hạn tốc độ cho các request đã xác thực:

@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 kiểm tra cả hai; giới hạn lỏng hơn thắng cho request đã xác thực
    ...

Thực hành Tốt nhất về Định dạng Phản hồi

Schema Lỗi Nhất quán

Mọi phản hồi lỗi — lỗi xác thực, giới hạn tốc độ, không tìm thấy — phải tuân theo cùng một schema:

{
  "error": "invalid_hex",
  "message": "Giá trị 'gggggg' không phải màu hex hợp lệ. Cần chuỗi hex 3 hoặc 6 chữ số.",
  "field": "hex_code",
  "value": "gggggg"
}
Trường Mục đích
error Mã lỗi có thể đọc được bởi máy (snake_case)
message Giải thích có thể đọc được bởi người
field Trường đầu vào nào gây ra lỗi (cho lỗi xác thực)
value Giá trị không hợp lệ (giúp debug)

Phân trang cho Endpoints Tập hợp

Endpoint tìm kiếm với limitoffset:

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

Luôn bao gồm total để client có thể hiển thị điều khiển phân trang mà không cần biết toàn bộ tập kết quả.

Chiến lược Versioning

Đánh phiên bản API của bạn trong đường dẫn URL, không phải trong header. Versioning theo URL là rõ ràng, có thể cache bởi CDN, và không yêu cầu client đặt header tùy chỉnh:

/api/v1/color/FF5733    # Phiên bản 1
/api/v2/color/FF5733    # Phiên bản 2 (khi cần thay đổi phá vỡ)

Duy trì các phiên bản trước ít nhất một năm sau khi phát hành phiên bản lớn. Sử dụng header deprecation trên các phiên bản cũ:

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"

Cấu hình CORS

Cho API công khai được sử dụng bởi client trình duyệt:

# settings.py (sử dụng django-cors-headers)
CORS_ALLOWED_ORIGINS = [
    "https://yoursite.com",
    "https://app.yoursite.com",
]

# Hoặc cho API hoàn toàn công khai:
CORS_ALLOW_ALL_ORIGINS = True

# Luôn giới hạn phương thức và header
CORS_ALLOW_METHODS = ['GET', 'POST', 'OPTIONS']
CORS_ALLOW_HEADERS = ['Content-Type', 'Authorization', 'X-API-Key']

Kiến trúc Cache

Dữ liệu màu có khả năng cache rất cao. Đối tượng màu cho #FF5733 sẽ không bao giờ thay đổi — toán học chuyển đổi là xác định. Cấu trúc chiến lược cache của bạn theo đó:

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

Thêm header ETag dựa trên mã hex cho các request có điều kiện:

response["ETag"] = f'"{hex_code}"'
response["Last-Modified"] = "Thu, 01 Jan 2026 00:00:00 GMT"  # Tĩnh — nội dung không bao giờ thay đổi

Client gửi If-None-Match: "FF5733" nhận phản hồi 304 Not Modified không có nội dung — tiết kiệm băng thông cho các request lặp lại.


Điểm chính cần ghi nhớ

  • Đối tượng màu chuẩn nên bao gồm tất cả các biểu diễn định dạng (hex, RGB, HSL, CMYK, OKLCH) trong một phản hồi duy nhất — không bao giờ để client phải thực hiện nhiều request để lấy các định dạng khác nhau.
  • Chuẩn hóa đầu vào hex một cách mạnh mẽ: bỏ #, mở rộng 3 chữ số, viết hoa. Xác thực bằng regex trước khi xử lý.
  • Endpoint độ tương phản là cốt lõi của bất kỳ công cụ tiếp cận nào — trả về tỷ lệ WCAG, kết quả đạt/không đạt theo từng cấp độ, và cấp độ cao nhất đạt trong một phản hồi duy nhất.
  • Tìm kiếm màu có tên hoạt động đáng tin cậy với tìm kiếm tiền tố trong bộ nhớ cho cơ sở dữ liệu nhỏ; sử dụng PostgreSQL pg_trgm cho cơ sở dữ liệu lớn.
  • Giới hạn tốc độ theo IP cho các request chưa xác thực (60/phút là hợp lý); sử dụng API key cho các tầng cao hơn.
  • Mọi phản hồi lỗi phải tuân theo cùng một schema với ít nhất error (có thể đọc bởi máy) và message (có thể đọc bởi người).
  • Dữ liệu màu có khả năng cache rất cao — cache CDN 24 giờ phù hợp cho các endpoint chuyển đổi; sử dụng ETagstale-while-revalidate để vô hiệu hóa cache hiệu quả.
  • Thử Công cụ Chuyển đổi MàuCông cụ Kiểm tra Độ tương phản để xem các API endpoints này hoạt động thực tế.

Màu sắc liên quan

Thương hiệu liên quan

Công cụ liên quan