API Response Schema

The API Response Schema defines how the backend communicates UI instructions to the frontend. It's the contract between the UI Decision Layer and the frontend rendering system.

Schema Structure

Every UI decision response follows a consistent schema:

interface UIDecisionResponse {
  // High-level UI paradigm
  uiForm: 'form' | 'chat' | 'widget' | 'wizard';

  // Specific component from registry
  componentType: string;

  // Component props (validated by registry)
  props: Record<string, unknown>;

  // Context that influenced the decision
  context: {
    urgency: 'low' | 'medium' | 'high' | 'critical';
    complexity: 'simple' | 'medium' | 'complex';
    dataVolume: 'small' | 'medium' | 'large';
    timeContext?: string;
    userExpertise?: number;
  };

  // LLM confidence in decision (0.0 - 1.0)
  confidence: number;

  // Explanation of why this UI form was chosen
  reasoning: string;

  // Optional metadata
  metadata?: {
    privacyLevel?: 'local' | 'cloud' | 'external';
    estimatedDuration?: number;  // seconds
    requiresAuth?: boolean;
    canDismiss?: boolean;
  };
}

Field Explanations

uiForm

The high-level UI paradigm. Determines the overall interaction pattern:

form

Structured data input with fields. Best for creating or editing entities with known parameters.

Example: Budget creation, appointment scheduling

chat

Conversational text response. Best for simple queries or when clarification is needed.

Example: "What's my balance?", ambiguous queries

widget

Interactive data visualization. Best for displaying complex data with actions.

Example: Calendar view, budget charts, transaction lists

wizard

Multi-step guided flow. Best for complex processes requiring multiple decisions.

Example: Trip planning, budget setup, onboarding

componentType

Stable identifier for the specific component to render. This is looked up in the Component Registry.

Naming Convention

  • kebab-case format (e.g., budget-form, calendar-widget)
  • Descriptive and stable across versions
  • Must exist in Component Registry
  • Can include version suffix for backward compatibility (e.g., budget-form-v1)

props

Component-specific props validated by the Component Registry. Structure varies by component:

// Example: Budget Form Props
{
  "props": {
    "category": "food",
    "suggestedAmount": 500,
    "currency": "EUR",
    "period": "monthly",
    "existingBudgets": [
      { "category": "transport", "amount": 300 },
      { "category": "entertainment", "amount": 200 }
    ]
  }
}

// Example: Calendar Widget Props
{
  "props": {
    "date": "2025-10-29",
    "events": [
      {
        "id": "evt-1",
        "title": "Team Meeting",
        "start": "2025-10-29T10:00:00Z",
        "end": "2025-10-29T11:00:00Z",
        "location": "Office"
      }
    ],
    "highlightConflicts": true
  }
}

context

Contextual information that influenced the UI decision. Useful for debugging and analytics:

urgency
low - Can wait
medium - Should address soon
high - Needs attention
critical - Immediate action required
complexity
simple - Single value or yes/no
medium - Multiple fields or decisions
complex - Multi-step process
dataVolume
small - Single item or short list
medium - List of 10-50 items
large - 50+ items, needs pagination
timeContext
Optional. E.g., "morning", "before-meeting", "end-of-month"
userExpertise
Optional. User expertise level (0-10)

Examples for Each UI Form

Form: Budget Creation

{
  "uiForm": "form",
  "componentType": "budget-form",
  "props": {
    "category": "food",
    "suggestedAmount": 500,
    "currency": "EUR",
    "period": "monthly",
    "categoryOptions": [
      "food",
      "transport",
      "entertainment",
      "utilities",
      "other"
    ]
  },
  "context": {
    "urgency": "low",
    "complexity": "medium",
    "dataVolume": "small",
    "userExpertise": 7
  },
  "confidence": 0.92,
  "reasoning": "Expert user with clear intent to create budget. Direct form is most efficient.",
  "metadata": {
    "privacyLevel": "local",
    "estimatedDuration": 30,
    "canDismiss": true
  }
}

Chat: Balance Query

{
  "uiForm": "chat",
  "componentType": "text-response",
  "props": {
    "text": "Your current balance is 2,450 EUR across 3 accounts:\n\n• Checking: 1,200 EUR\n• Savings: 1,000 EUR\n• Cash: 250 EUR",
    "format": "markdown",
    "actions": [
      {
        "label": "View Transactions",
        "action": "view-transactions"
      },
      {
        "label": "Transfer Money",
        "action": "transfer"
      }
    ]
  },
  "context": {
    "urgency": "low",
    "complexity": "simple",
    "dataVolume": "small"
  },
  "confidence": 0.95,
  "reasoning": "Simple balance query. Text response is sufficient and fastest.",
  "metadata": {
    "privacyLevel": "local",
    "estimatedDuration": 5,
    "canDismiss": true
  }
}

Widget: Calendar View

{
  "uiForm": "widget",
  "componentType": "calendar-widget",
  "props": {
    "date": "2025-10-29",
    "view": "day",
    "events": [
      {
        "id": "evt-1",
        "title": "Team Standup",
        "start": "2025-10-29T09:00:00Z",
        "end": "2025-10-29T09:15:00Z",
        "location": "Zoom",
        "color": "blue"
      },
      {
        "id": "evt-2",
        "title": "Client Meeting",
        "start": "2025-10-29T14:00:00Z",
        "end": "2025-10-29T15:00:00Z",
        "location": "Office - Room 3",
        "color": "green"
      }
    ],
    "highlightConflicts": true,
    "showTimeSlots": true
  },
  "context": {
    "urgency": "medium",
    "complexity": "medium",
    "dataVolume": "medium",
    "timeContext": "morning"
  },
  "confidence": 0.89,
  "reasoning": "Morning context detected. User likely checking today's schedule. Calendar widget provides quick overview with interaction options.",
  "metadata": {
    "privacyLevel": "local",
    "estimatedDuration": 15,
    "canDismiss": true
  }
}

Wizard: Trip Planning

{
  "uiForm": "wizard",
  "componentType": "trip-planning-wizard",
  "props": {
    "initialStep": "destination",
    "steps": [
      {
        "id": "destination",
        "title": "Where are you going?",
        "description": "Enter your destination city or country",
        "fields": [
          {
            "name": "destination",
            "type": "location-autocomplete",
            "label": "Destination",
            "required": true
          }
        ]
      },
      {
        "id": "dates",
        "title": "When are you traveling?",
        "description": "Select your departure and return dates",
        "fields": [
          {
            "name": "departureDate",
            "type": "date",
            "label": "Departure",
            "required": true
          },
          {
            "name": "returnDate",
            "type": "date",
            "label": "Return",
            "required": true
          }
        ]
      },
      {
        "id": "preferences",
        "title": "Travel preferences",
        "description": "Help us find the best options for you",
        "fields": [
          {
            "name": "budget",
            "type": "number",
            "label": "Budget (EUR)",
            "required": false
          },
          {
            "name": "flightClass",
            "type": "select",
            "label": "Flight Class",
            "options": ["economy", "premium-economy", "business"],
            "required": false
          }
        ]
      }
    ],
    "allowSkip": true,
    "showProgress": true
  },
  "context": {
    "urgency": "low",
    "complexity": "complex",
    "dataVolume": "medium",
    "userExpertise": 3
  },
  "confidence": 0.85,
  "reasoning": "Complex trip planning task with multiple decisions. Beginner user (expertise: 3/10) benefits from guided multi-step wizard.",
  "metadata": {
    "privacyLevel": "cloud",
    "estimatedDuration": 180,
    "canDismiss": true,
    "requiresAuth": true
  }
}

Validation with Zod

Both backend and frontend validate the response schema using Zod:

import { z } from 'zod';

// Base response schema
export const UIDecisionResponseSchema = z.object({
  uiForm: z.enum(['form', 'chat', 'widget', 'wizard']),
  componentType: z.string().min(1),
  props: z.record(z.unknown()),
  context: z.object({
    urgency: z.enum(['low', 'medium', 'high', 'critical']),
    complexity: z.enum(['simple', 'medium', 'complex']),
    dataVolume: z.enum(['small', 'medium', 'large']),
    timeContext: z.string().optional(),
    userExpertise: z.number().min(0).max(10).optional()
  }),
  confidence: z.number().min(0).max(1),
  reasoning: z.string().min(1),
  metadata: z.object({
    privacyLevel: z.enum(['local', 'cloud', 'external']).optional(),
    estimatedDuration: z.number().positive().optional(),
    requiresAuth: z.boolean().optional(),
    canDismiss: z.boolean().optional()
  }).optional()
});

export type UIDecisionResponse = z.infer<typeof UIDecisionResponseSchema>;

// Usage
const response = await fetch('/api/ui-decision', { ... });
const data = await response.json();

const validationResult = UIDecisionResponseSchema.safeParse(data);

if (!validationResult.success) {
  console.error('Invalid response:', validationResult.error);
  // Handle error
} else {
  // TypeScript knows the shape!
  const uiResponse = validationResult.data;
  console.log(uiResponse.uiForm);  // ✅ Type-safe
}

Error Handling

When the UI Decision Layer encounters errors, it returns an error response:

// Error response schema
interface UIDecisionErrorResponse {
  error: {
    code: string;
    message: string;
    details?: Record<string, unknown>;
  };
  fallback?: UIDecisionResponse;  // Optional fallback UI
}

// Example error response
{
  "error": {
    "code": "LLM_UNAVAILABLE",
    "message": "Could not reach LLM service",
    "details": {
      "retryAfter": 30
    }
  },
  "fallback": {
    "uiForm": "chat",
    "componentType": "text-response",
    "props": {
      "text": "I'm having trouble processing your request right now. Please try again in a moment.",
      "variant": "error"
    },
    "context": {
      "urgency": "low",
      "complexity": "simple",
      "dataVolume": "small"
    },
    "confidence": 1.0,
    "reasoning": "LLM unavailable, using fallback response"
  }
}

Versioning Strategy

The API response schema supports versioning through content negotiation:

Version Headers

// Request with version
POST /api/ui-decision
Headers:
  Content-Type: application/json
  Accept: application/vnd.fidus.ui-decision.v2+json

// Response includes schema version
{
  "schemaVersion": "2.0.0",
  "uiForm": "form",
  "componentType": "budget-form",
  // ... rest of response
}

TypeScript Types

Complete TypeScript type definitions for the API response:

// types/ui-decision.ts
export type UIForm = 'form' | 'chat' | 'widget' | 'wizard';

export type Urgency = 'low' | 'medium' | 'high' | 'critical';
export type Complexity = 'simple' | 'medium' | 'complex';
export type DataVolume = 'small' | 'medium' | 'large';
export type PrivacyLevel = 'local' | 'cloud' | 'external';

export interface UIDecisionContext {
  urgency: Urgency;
  complexity: Complexity;
  dataVolume: DataVolume;
  timeContext?: string;
  userExpertise?: number;
}

export interface UIDecisionMetadata {
  privacyLevel?: PrivacyLevel;
  estimatedDuration?: number;
  requiresAuth?: boolean;
  canDismiss?: boolean;
}

export interface UIDecisionResponse {
  uiForm: UIForm;
  componentType: string;
  props: Record<string, unknown>;
  context: UIDecisionContext;
  confidence: number;
  reasoning: string;
  metadata?: UIDecisionMetadata;
}

export interface UIDecisionErrorResponse {
  error: {
    code: string;
    message: string;
    details?: Record<string, unknown>;
  };
  fallback?: UIDecisionResponse;
}

Benefits

  • Consistent: All UI decisions follow same structure
  • Type-Safe: Validated with Zod, typed with TypeScript
  • Debuggable: Context and reasoning included
  • Versioned: Support for schema evolution
  • Extensible: Add fields without breaking changes
  • Testable: Easy to mock and test

Testing

describe('UIDecisionResponse', () => {
  it('should validate valid response', () => {
    const response = {
      uiForm: 'form',
      componentType: 'budget-form',
      props: { category: 'food' },
      context: {
        urgency: 'low',
        complexity: 'medium',
        dataVolume: 'small'
      },
      confidence: 0.92,
      reasoning: 'Expert user intent'
    };

    const result = UIDecisionResponseSchema.safeParse(response);
    expect(result.success).toBe(true);
  });

  it('should reject invalid uiForm', () => {
    const response = {
      uiForm: 'invalid',  // ❌ Not in enum
      // ... rest
    };

    const result = UIDecisionResponseSchema.safeParse(response);
    expect(result.success).toBe(false);
  });

  it('should accept optional metadata', () => {
    const response = {
      uiForm: 'widget',
      componentType: 'calendar-widget',
      props: {},
      context: { urgency: 'medium', complexity: 'medium', dataVolume: 'small' },
      confidence: 0.89,
      reasoning: 'Morning check',
      metadata: {
        privacyLevel: 'local',
        estimatedDuration: 15
      }
    };

    const result = UIDecisionResponseSchema.safeParse(response);
    expect(result.success).toBe(true);
  });
});

Related Documentation