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.
chat
Conversational text response. Best for simple queries or when clarification is needed.
widget
Interactive data visualization. Best for displaying complex data with actions.
wizard
Multi-step guided flow. Best for complex processes requiring multiple decisions.
componentType
Stable identifier for the specific component to render. This is looked up in the Component Registry.
Naming Convention
kebab-caseformat (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 waitmedium- Should address soonhigh- Needs attentioncritical- Immediate action required- complexity
simple- Single value or yes/nomedium- Multiple fields or decisionscomplex- Multi-step process- dataVolume
small- Single item or short listmedium- List of 10-50 itemslarge- 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
- UI Decision Layer - How decisions are made
- Component Registry - Component resolution
- AI-Driven UI Paradigm - Core principles