บทเรียนแนะนำ

การออกแบบ Color API: REST Endpoint สำหรับข้อมูลสี

อ่าน 7 นาที

Color API อาจดูเฉพาะทางมาก จนกว่าคุณจะนึกถึงว่ามีเครื่องมือกี่ชิ้นที่ใช้ข้อมูลสี: เครื่องมือออกแบบที่ต้องการการแปลง ตัวตรวจสอบการเข้าถึงที่ตรวจสอบคอนทราสต์ คอมโพเนนต์ตัวเลือกสีที่ต้องการการค้นหาสีที่มีชื่อ และตัวสร้างจานสีที่ผลิตความสอดคล้องกัน หากคุณกำลังสร้างสิ่งใดสิ่งหนึ่งเหล่านี้ หรือจัดหาข้อมูลสีให้กับนักพัฒนาคนอื่น คุณต้องการ REST API ที่ออกแบบมาอย่างดี

คู่มือนี้ครอบคลุมพื้นที่การออกแบบทั้งหมด: การตัดสินใจเกี่ยวกับ schema โครงสร้าง endpoint รูปแบบการตอบสนอง การจัดการข้อผิดพลาด และการจำกัดอัตรา — จากมุมมองของผู้ที่สร้าง endpoint ที่ขับเคลื่อน Color Converter และ Contrast Checker ที่ ColorFYI


การออกแบบ Schema API สำหรับข้อมูลสี

Object สีหลัก

สีมีหลายการแสดงผล — hex, RGB, HSL, CMYK, OKLCH — และหลายฟิลด์ metadata Object การตอบสนองแบบ canonical ควรรวมทั้งหมด ช่วยให้ client ใช้รูปแบบใดก็ได้ที่ต้องการโดยไม่ต้องส่งคำขอเพิ่มเติม:

{
  "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 (ที่มาจากความสว่าง) ช่วยให้ client ไม่ต้องนำการคำนวณความสว่าง WCAG มาใช้เอง การรวม name และ name_distance ช่วยให้ client แสดงสีที่มีชื่อใกล้เคียงที่สุดโดยไม่ต้องเรียก API แยกต่างหาก

แบบแผนการตั้งชื่อ

ใช้ snake_case สำหรับ JSON key (สอดคล้องกับ REST API ส่วนใหญ่) จัดกลุ่มฟิลด์ที่เกี่ยวข้องเป็น object (rgb, hsl, cmyk) แทนที่จะแผ่ออก (rgb_r, rgb_g, rgb_b) Object ที่ซ้อนกันอ่านง่ายกว่า แยกโครงสร้างใน JavaScript ได้ง่ายกว่า และ mapping ไปยัง typed interface ได้อย่างชัดเจน:

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

การ Normalize Hex

รับสีใน hex หลายรูปแบบ — มีหรือไม่มี #, 3-หลักหรือ 6-หลัก, ตัวพิมพ์ใหญ่หรือเล็ก — และ normalize ใน API layer ก่อนการประมวลผลใดๆ:

# ตัวอย่าง Python/Django
import re

def normalize_hex(raw: str) -> str:
    """Normalize อินพุต hex เป็น 6-หลักตัวพิมพ์ใหญ่โดยไม่มี #"""
    clean = raw.strip().lstrip('#').upper()
    if not re.match(r'^[0-9A-F]{3}$|^[0-9A-F]{6}$', clean):
        raise ValueError(f"สีในรูปแบบ hex ไม่ถูกต้อง: {raw!r}")
    # ขยาย 3-หลักเป็น 6-หลัก
    if len(clean) == 3:
        clean = ''.join(c * 2 for c in clean)
    return clean

คืนค่ารูปแบบที่ normalize แล้วในการตอบสนอง — สิ่งนี้ขจัดความคลุมเครือและทำให้การ cache ที่ปลายทางคาดเดาได้มากขึ้น


Endpoint การแปลง Hex/RGB/HSL

GET /api/color/{hex}

Endpoint หลักคืนค่า object สีเต็มรูปแบบสำหรับ hex code ที่กำหนด นี่คือสิ่งที่ Color Converter ใช้เพื่อเติมฟิลด์รูปแบบทั้งหมดพร้อมกัน:

GET /api/color/FF5733
GET /api/color/%23FF5733   (# ที่ encode URL)
GET /api/color/f57          (3-หลัก, ตัวพิมพ์เล็ก)

ทั้งสามควรคืนค่าการตอบสนองเดียวกันสำหรับ #FF5733

การตอบสนอง:

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

สำหรับการแปลงอินพุตตามอำเภอใจที่อาจไม่ใช่ hex — ชื่อสี CSS, ชุด RGB, ค่า HSL:

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

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

การตอบสนอง: Object สีเต็มรูปแบบเดียวกัน

รับค่า from: hex, rgb, hsl, cmyk, oklch, name ตรวจสอบรูปแบบอินพุตให้ตรงกับประเภท from ที่ประกาศ และคืนค่า 422 พร้อมข้อความแสดงข้อผิดพลาดที่ชัดเจนหากไม่ตรงกัน

ตัวอย่าง 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 ข้อมูลสีอย่างจริงจัง — hex code ที่กำหนดจะให้ผลลัพธ์เดิมเสมอ ดังนั้น cache จึงมีอายุยาวนานมากได้


API การค้นหาสีและการเติมอัตโนมัติ

GET /api/search

การค้นหาสีที่มีชื่อรองรับการเติมอัตโนมัติในตัวเลือกสีและช่องค้นหา พารามิเตอร์ query คือชื่อสีบางส่วน:

GET /api/search?q=coral
GET /api/search?q=teal&limit=5
GET /api/search?q=%230d&source=css   // ค้นหาตามคำนำหน้า hex

การตอบสนอง:

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

ข้อพิจารณาในการพัฒนาการค้นหา

สำหรับฐานข้อมูลสีที่มีชื่อขนาดเล็ก (~2,000 รายการ) การค้นหาคำนำหน้าใน memory พร้อม fuzzy matching มีความเร็วเพียงพอ:

from difflib import SequenceMatcher

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

    for color in NAMED_COLORS:  # รายการที่โหลดไว้ล่วงหน้า
        name = color["name"].lower()
        if q in name:
            # การจับคู่ substring ตรง: คะแนนตามตำแหน่ง
            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]

สำหรับฐานข้อมูลที่มีมากกว่า 10,000 รายการ ใช้ส่วนขยาย pg_trgm ของ PostgreSQL พร้อม trigram indexing สำหรับ fuzzy search ระดับ sub-millisecond:

-- เปิดใช้ส่วนขยาย
CREATE EXTENSION IF NOT EXISTS pg_trgm;

-- เพิ่ม index
CREATE INDEX idx_color_name_trgm ON named_colors USING gin(name gin_trgm_ops);

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

การปรับปรุงการเติมอัตโนมัติ

สำหรับการเติมอัตโนมัติแบบ search-as-you-type ปรับให้เหมาะสมสำหรับ latency ต่ำ:

  1. Debounce ที่ client — ขั้นต่ำ 150ms ก่อนส่งคำขอ
  2. กำหนดความยาว query ขั้นต่ำ — ปฏิเสธ query ที่สั้นกว่า 2 ตัวอักษร (คืนค่าผลลัพธ์ว่างทันที)
  3. Cache ผลลัพธ์ตาม query — query เดียวกันจากผู้ใช้ที่ต่างกันได้ผลลัพธ์เดิม ใช้ Cache-Control: public, max-age=3600
  4. ตอบสนองเร็วเมื่อไม่มีผลลัพธ์ — ตอบสนองทันทีด้วย {"results": [], "total": 0} แทนการรัน fuzzy scan เต็มรูปแบบ

Endpoint การตรวจสอบคอนทราสต์

GET /api/contrast

API คอนทราสต์คือเครื่องยนต์เบื้องหลัง Contrast Checker รับ hex code สองรหัสและคืนค่าอัตราส่วนคอนทราสต์ WCAG พร้อมสถานะผ่าน/ล้มเหลวสำหรับแต่ละระดับ:

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

การตอบสนอง:

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

ฟิลด์ level คืนค่าระดับ WCAG สูงสุดที่คู่สีนั้นบรรลุ ("AAA", "AA", "AA Large", หรือ "Fail") ฟิลด์ passes เป็น true หากมีระดับ WCAG ใดระดับหนึ่งที่ผ่าน — มีประโยชน์สำหรับตัวบ่งชี้สีเขียว/แดงอย่างง่าย

การคำนวณคอนทราสต์

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

การแนะนำทางเลือกที่เข้าถึงได้

ส่วนขยายที่มีประโยชน์: กำหนดสีหน้าที่ล้มเหลวในการตรวจสอบคอนทราสต์กับพื้นหลัง แนะนำการปรับ lightness ขั้นต่ำที่บรรลุ WCAG AA:

def suggest_accessible_foreground(fg_hex: str, bg_hex: str, target_ratio: float = 4.5) -> str:
    """คืนค่าเวอร์ชันของ fg_hex ที่ปรับ lightness แล้วซึ่งผ่าน target_ratio"""
    hsl = rgb_to_hsl(*hex_to_rgb(fg_hex))
    bg_lum = relative_luminance(bg_hex)
    is_dark_bg = bg_lum < 0.179

    # ลองปรับ lightness ในทิศทางที่เพิ่มคอนทราสต์
    step = -2 if is_dark_bg else 2  # ทำให้มืดบนพื้นหลังสว่าง, สว่างบนพื้นหลังมืด
    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'

การจำกัดอัตราสำหรับ Color API

ทำไม Color API จึงต้องการการจำกัดอัตรา

Color API อาจดูเสี่ยงต่ำ แต่อาจเป็นเป้าหมายของ: - การโจมตีแบบ Enumeration: วนซ้ำผ่าน hex code ทั้ง 16.7 ล้านรหัสเพื่อเก็บเกี่ยวฐานข้อมูลชื่อสีทั้งหมด - Scraping: ใช้ logic การแปลงของคุณเป็นทรัพยากรการคำนวณฟรี - การละเมิด: คำขอปริมาณสูงจาก client เดียวที่ลดคุณภาพบริการสำหรับผู้อื่น

กลยุทธ์การพัฒนา

สำหรับ Django API, django-ratelimit ให้การจำกัดอัตราแบบ decorator พร้อม Redis backend:

# settings.py
RATELIMIT_USE_CACHE = 'default'  # ใช้ cache backend ของ Django (แนะนำ Redis)

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

รูปแบบการตอบสนองการจำกัดอัตรา

เมื่อเกินขีดจำกัดอัตรา คืนค่า 429 Too Many Requests พร้อมเนื้อหาการตอบสนองที่ชัดเจนและ 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": "คุณเกินขีดจำกัดอัตรา 100 คำขอต่อนาทีแล้ว",
  "retry_after": 60
}

ขีดจำกัดอัตราแบบแบ่งระดับ

โครงสร้างแบบแบ่งระดับที่สมเหตุสมผล:

ระดับ ขีดจำกัด Key
ไม่ระบุตัวตน 60 คำขอ/นาที ที่อยู่ IP
API key (ฟรี) 300 คำขอ/นาที API key
API key (เสียเงิน) 3000 คำขอ/นาที API key

API key ส่งผ่าน Authorization: Bearer {key} หรือ X-API-Key: {key} ใช้ API key เป็น key การจำกัดอัตราสำหรับคำขอที่ผ่านการตรวจสอบสิทธิ์:

@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 ตรวจสอบทั้งคู่; ขีดจำกัดที่หลวมกว่าจะชนะสำหรับคำขอที่ผ่านการตรวจสอบ
    ...

แนวทางปฏิบัติที่ดีที่สุดสำหรับรูปแบบการตอบสนอง

Schema ข้อผิดพลาดที่สอดคล้องกัน

ทุกการตอบสนองข้อผิดพลาด — ความล้มเหลวในการตรวจสอบ, การจำกัดอัตรา, ไม่พบ — ควรปฏิบัติตาม schema เดียวกัน:

{
  "error": "invalid_hex",
  "message": "ค่า 'gggggg' ไม่ใช่สีในรูปแบบ hex ที่ถูกต้อง คาดว่าเป็น hex string 3 หรือ 6 หลัก",
  "field": "hex_code",
  "value": "gggggg"
}
ฟิลด์ วัตถุประสงค์
error รหัสข้อผิดพลาดที่เครื่องอ่านได้ (snake_case)
message คำอธิบายที่มนุษย์อ่านได้
field ฟิลด์อินพุตใดที่ทำให้เกิดข้อผิดพลาด (สำหรับข้อผิดพลาดการตรวจสอบ)
value ค่าที่ไม่ถูกต้อง (ช่วยในการ debug)

การแบ่งหน้าสำหรับ Collection Endpoint

Endpoint การค้นหาพร้อม limit และ offset:

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

รวม total เสมอเพื่อให้ client สามารถแสดงตัวควบคุมการแบ่งหน้าได้โดยไม่ต้องรู้ชุดผลลัพธ์ทั้งหมด

กลยุทธ์การกำหนดเวอร์ชัน

กำหนดเวอร์ชัน API ของคุณในเส้นทาง URL ไม่ใช่ใน header การกำหนดเวอร์ชัน URL นั้นชัดเจน CDN สามารถ cache ได้ และไม่ต้องให้ client ตั้ง header แบบกำหนดเอง:

/api/v1/color/FF5733    # เวอร์ชัน 1
/api/v2/color/FF5733    # เวอร์ชัน 2 (เมื่อต้องการการเปลี่ยนแปลงที่ขัดแย้ง)

รักษาเวอร์ชันก่อนหน้าอย่างน้อยหนึ่งปีหลังจากการเปิดตัวเวอร์ชันหลัก ใช้ deprecation header บนเวอร์ชันเก่า:

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

สำหรับ API สาธารณะที่ใช้โดย browser client:

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

# หรือสำหรับ API สาธารณะอย่างสมบูรณ์:
CORS_ALLOW_ALL_ORIGINS = True

# จำกัด method และ header เสมอ
CORS_ALLOW_METHODS = ['GET', 'POST', 'OPTIONS']
CORS_ALLOW_HEADERS = ['Content-Type', 'Authorization', 'X-API-Key']

สถาปัตยกรรมการ Caching

ข้อมูลสี cache ได้สูง Object สีสำหรับ #FF5733 จะไม่เปลี่ยนแปลงเลย — การคำนวณการแปลงเป็น deterministic วางโครงสร้างกลยุทธ์ cache ของคุณตามนั้น:

Endpoint Cache-Control CDN TTL
/api/color/{hex} public, max-age=86400, stale-while-revalidate=604800 1 วัน
/api/search?q={q} public, max-age=3600 1 ชั่วโมง
/api/contrast?fg=X&bg=Y public, max-age=86400 1 วัน
/api/palette/{hex} public, max-age=86400 1 วัน

เพิ่ม header ETag ตาม hex code สำหรับคำขอแบบมีเงื่อนไข:

response["ETag"] = f'"{hex_code}"'
response["Last-Modified"] = "Thu, 01 Jan 2026 00:00:00 GMT"  # คงที่ — เนื้อหาไม่เปลี่ยนแปลง

Client ที่ส่ง If-None-Match: "FF5733" จะได้รับการตอบสนอง 304 Not Modified โดยไม่มีเนื้อหา — ประหยัด bandwidth สำหรับคำขอซ้ำ


ประเด็นสำคัญ

  • Object สี canonical ควรรวมการแสดงผลรูปแบบทั้งหมด (hex, RGB, HSL, CMYK, OKLCH) ในการตอบสนองเดียว — อย่าให้ client ต้องส่งคำขอหลายครั้งเพื่อรับรูปแบบที่แตกต่างกัน
  • Normalize อินพุต hex อย่างจริงจัง: ลบ #, ขยาย 3-หลัก, ใช้ตัวพิมพ์ใหญ่ ตรวจสอบด้วย regex ก่อนการประมวลผลใดๆ
  • Endpoint คอนทราสต์คือหัวใจของเครื่องมือการเข้าถึงใดๆ — คืนค่าอัตราส่วน WCAG, ผ่าน/ล้มเหลวต่อระดับ และระดับที่ผ่านสูงสุดในการตอบสนองเดียว
  • การค้นหาสีที่มีชื่อทำงานได้อย่างน่าเชื่อถือด้วยการค้นหาคำนำหน้าใน memory สำหรับฐานข้อมูลขนาดเล็ก; ใช้ PostgreSQL pg_trgm สำหรับฐานข้อมูลขนาดใหญ่
  • จำกัดอัตราตาม IP สำหรับคำขอที่ไม่ผ่านการตรวจสอบสิทธิ์ (60/นาทีเป็นค่าที่สมเหตุสมผล); ใช้ API key สำหรับระดับที่สูงกว่า
  • ทุกการตอบสนองข้อผิดพลาดต้องปฏิบัติตาม schema เดียวกันพร้อม error (เครื่องอ่านได้) และ message (มนุษย์อ่านได้) อย่างน้อย
  • ข้อมูลสี cache ได้สูง — CDN cache 24 ชั่วโมงเหมาะสมสำหรับ endpoint การแปลง; ใช้ ETag และ stale-while-revalidate สำหรับการยกเลิก cache ที่มีประสิทธิภาพ
  • ลอง Color Converter และ Contrast Checker เพื่อดู endpoint API เหล่านี้ในการใช้งานจริง

สีที่เกี่ยวข้อง

แบรนด์ที่เกี่ยวข้อง

เครื่องมือที่เกี่ยวข้อง