24. Pattern: Lookup List

The Lookup List pattern provides a dedicated, read-only API operation to retrieve dynamic lists of code-value pairs, human-readable labels, and associated metadata.

Instead of hardcoding frequently changing values as static enum definitions inside OpenAPI specifications or client codebases, this pattern exposes reference endpoints (e.g., GET /lookups/support-categories or GET /reference/currencies). This allows backend systems to introduce, modify, or deprecate options dynamically without breaking API contracts or requiring client application re-deployments.

24.1. Overview

In API design, enumerations (enum) work well for strictly fixed sets of values that drive core control flow (such as status: [DRAFT, SUBMITTED, PUBLISHED]). However, domain models often contain reference data that evolves over time—such as support ticket categories, product classification codes, country lists, or industry types.

If these values are defined as static OpenAPI enums, adding a new option requires:

  1. Updating the OpenAPI specification.
  2. Re-generating and publishing client SDKs.
  3. Deploying updates across all mobile, web, and third-party consumer applications.

The Lookup List pattern solves this by moving dynamic reference data into cacheable, read-only endpoints.

Typical examples:

  • GET /lookups/support-categories (Returns available support ticket topics with display labels)
  • GET /reference/currencies (Returns ISO currency codes with symbols and formatting metadata)
  • GET /reference/countries (Returns localized country names and dial codes)

These operations:

  • Decouple dynamic dropdown choices from static client code
  • Support localization (via headers like Accept-Language)
  • Leverage HTTP caching (Cache-Control, ETag) for sub-millisecond client performance

24.2. When to Use Lookup List

Use Lookup List when:

  • Reference data changes more frequently than client software deployment cycles.
    • e.g., Tax codes, merchant category codes, or seasonal promotion types added by business administrators.
  • Values require human-readable labels or localization.
    • e.g., Translating a category code ("CAT_PAYMENT_ISSUE") into localized UI strings ("Payment & Billing Issue" in English vs. "Problema de Pago y Facturación" in Spanish).
  • Options depend on metadata or status flags.
    • e.g., Inactive options that must remain valid for historical record lookups but should be hidden from new entry dropdowns (isActive: false).
  • Form UI elements are driven dynamically by the backend.
    • Client web or mobile applications render dropdown menus dynamically by querying lookup endpoints on load.

24.3. When NOT to Use Lookup List

Avoid Lookup List when:

  • Values are strictly fixed and drive core application logic.
    • e.g., HTTP methods (GET, POST), boolean flags, or core state machine states (DRAFT, PUBLISHED). Standard OpenAPI enum fields are appropriate here.
  • The resource requires full CRUD lifecycle management.
    • If clients need to create, update, or delete entries directly, use standard resource collections (e.g., /categories/{id}) rather than a read-only lookup list.
  • The data set is massive or unbounded.
    • Searchable datasets containing tens of thousands of records (e.g., customer directories or full address databases) should use Search/Query patterns with pagination rather than bulk lookup lists.

24.4. What the Pattern Looks Like

Below are HTTP request and response interaction flows demonstrating dynamic lookup retrieval, localization, and active-status filtering.

1. Standard Retrieval (GET /lookups/support-categories)

The client requests reference categories to populate a UI dropdown menu.

Request

GET /lookups/support-categories HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOi...
Accept-Language: en-US

Response

HTTP/1.1 200 OK
Cache-Control: public, max-age=86400, must-revalidate
ETag: "w/99a2b8"
Content-Type: application/json

{
  "category": "support-categories",
  "items": [
    {
      "code": "BILLING_ISSUE",
      "label": "Billing & Invoicing",
      "description": "Questions regarding charges, refunds, or payment methods.",
      "displayOrder": 1,
      "isActive": true
    },
    {
      "code": "TECH_SUPPORT",
      "label": "Technical Support",
      "description": "Report bugs or platform access issues.",
      "displayOrder": 2,
      "isActive": true
    },
    {
      "code": "FEATURE_REQ",
      "label": "Feature Request",
      "description": "Suggest new features or integrations.",
      "displayOrder": 3,
      "isActive": true
    }
  ]
}

2. Localized Request (Accept-Language: es-ES)

The client requests the same lookup list with a Spanish localization header.

Request

GET /lookups/support-categories HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOi...
Accept-Language: es-ES

Response

HTTP/1.1 200 OK
Cache-Control: public, max-age=86400, must-revalidate
Vary: Accept-Language
ETag: "w/es-33d19"
Content-Type: application/json

{
  "category": "support-categories",
  "items": [
    {
      "code": "BILLING_ISSUE",
      "label": "Facturación y Cobros",
      "description": "Consultas sobre cargos, reembolsos o métodos de pago.",
      "displayOrder": 1,
      "isActive": true
    },
    {
      "code": "TECH_SUPPORT",
      "label": "Soporte Técnico",
      "description": "Informar fallos o problemas de acceso.",
      "displayOrder": 2,
      "isActive": true
    }
  ]
}

3. Filtering Inactive Codes (GET /lookups/support-categories?activeOnly=true)

Clients creating new records query only active options, while reporting dashboards query all codes including deprecated ones.

Request

GET /lookups/support-categories?activeOnly=true HTTP/1.1
Host: api.example.com

Response

Returns only entries where isActive: true.

24.5. Anti-Patterns to Avoid

1. Hardcoding frequently changing domain lists as OpenAPI enums

# Anti-Pattern: Specifying dynamic business options as an enum
type: string
enum: [BILLING_ISSUE, TECH_SUPPORT, FEATURE_REQ, ACCOUNT_RESET]
  • Why it’s bad: Every time a new option is added, all OpenAPI specifications, SDKs, and client validators must be updated and re-deployed. Use standard type: string with a reference lookup endpoint instead.

2. Returning key-only arrays without display labels

/* Anti-Pattern: Key-only lookup */
["BILLING_ISSUE", "TECH_SUPPORT", "FEATURE_REQ"]
  • Why it’s bad: Returning raw code strings forces every client application (iOS, Android, Web) to duplicate translation maps and display logic. Return objects containing code, label, and description.

3. Omitting Cache-Control headers on lookup endpoints

  • Lookup data rarely changes minute-to-minute. Omitting Cache-Control directives forces client applications to re-fetch identical data repeatedly, creating unnecessary server load. Always return long cache windows (e.g., Cache-Control: public, max-age=86400) alongside ETag validators.

4. Polluting domain resources with nested lookup enumerations

  • Embedding static lists of all possible options directly inside unrelated resource responses (e.g., returning all possible ticket categories inside every GET /tickets/{id} response) bloats payload sizes. Keep lookup definitions in dedicated reference endpoints.

24.6. OpenAPI Example

A complete OpenAPI 3.0.3 specification illustrating how to document a Lookup List endpoint supporting category parameterization, caching headers, and localized items.

openapi: 3.0.3
info:
  title: Reference Data API - Lookup List Pattern Example
  version: 1.0.0
servers:
  - url: https://api.example.com

paths:
  /lookups/{category}:
    get:
      summary: Retrieve reference lookup values
      description: Returns a cacheable list of valid codes, display labels, and metadata for a specified reference category.
      tags: [Lookups]
      parameters:
        - in: path
          name: category
          required: true
          schema:
            type: string
            enum: [support-categories, currencies, countries, tax-codes]
          example: support-categories
        - in: query
          name: activeOnly
          required: false
          schema:
            type: boolean
            default: true
          description: When true, filters out deprecated or inactive lookup codes.
        - in: header
          name: Accept-Language
          required: false
          schema:
            type: string
            example: "en-US"
          description: Localizes response labels to the requested language.
      responses:
        '200':
          description: Lookup values retrieved successfully
          headers:
            Cache-Control:
              schema:
                type: string
                example: "public, max-age=86400, must-revalidate"
            ETag:
              schema:
                type: string
                example: '"w/99a2b8"'
            Vary:
              schema:
                type: string
                example: "Accept-Language"
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LookupListResponse'

components:
  schemas:
    LookupListResponse:
      type: object
      required:
        - category
        - items
      properties:
        category:
          type: string
          example: support-categories
        items:
          type: array
          items:
            $ref: '#/components/schemas/LookupItem'

    LookupItem:
      type: object
      required:
        - code
        - label
        - isActive
      properties:
        code:
          type: string
          example: BILLING_ISSUE
          description: The stable programmatic code saved in database records.
        label:
          type: string
          example: Billing & Invoicing
          description: The localized human-readable display string for UIs.
        description:
          type: string
          example: Questions regarding charges, refunds, or payment methods.
        displayOrder:
          type: integer
          example: 1
        isActive:
          type: boolean
          example: true

24.7. Visualizing Lookup List (Mermaid Diagram)

sequenceDiagram
    autonumber
    actor User as User / Browser
    actor Client as Client App (Form UI)
    participant CDN as Edge CDN / Cache
    participant API as API Server
    participant DB as Reference DB

    Note over Client,DB: Phase 1: Dynamic Form Initialization
    User->>Client: Opens "Create Support Ticket" Page
    
    Client->>CDN: GET /lookups/support-categories (Accept-Language: es-ES)
    alt Cached Copy Fresh in CDN
        CDN-->>Client: 200 OK (From Cache: [Facturación, Soporte Técnico])
    else Cache Expired or Miss
        CDN->>API: GET /lookups/support-categories
        API->>DB: Query active categories & es-ES translations
        DB-->>API: Return category records
        API-->>CDN: 200 OK (Cache-Control: max-age=86400, ETag: "w/es-33d19")
        CDN-->>Client: 200 OK (JSON Lookup Payload)
    end

    Note over Client,User: Phase 2: Dynamic UI Rendering
    Client->>Client: Populate <select> dropdown with code/label pairs
    User->>Client: Selects "Facturación y Cobros" (code: "BILLING_ISSUE") & Submits
    
    Note over Client,API: Phase 3: Domain Entity Creation
    Client->>API: POST /tickets { "categoryCode": "BILLING_ISSUE", "subject": "..." }
    API->>API: Validate "BILLING_ISSUE" against active lookup list
    API-->>Client: 201 Created { id: "tkt_88192", status: "OPEN" }