Overview
The @kosServiceRequest decorator enables declarative HTTP service integration in KOS models, automatically handling request lifecycle, caching, error management, and baseline-delta synchronization. This pattern eliminates manual service calls and provides type-safe, automatic data loading tied to model lifecycle.
Important: The
@kosServiceRequestdecorator is app-specific and must be imported from versioned service files (either SDK packages like@kosdev-code/kos-ui-sdkor localservices-index). Each app’s decorator is fully typed for that app’s OpenAPI specification, providing complete IntelliSense for paths, parameters, and responses.
Quick Start: Progressive Examples
Level 1: Automatic Data Loading (Lifecycle)
The simplest use case: automatically load data when your model becomes ready.
import { kosModel, IKosDataModel, KosModelType, DependencyLifecycle } from '@kosdev-code/kos-ui-sdk';
import { ICE_APP } from '../../../utils/services-index';
const { kosServiceRequest } = ICE_APP;
@kosModel()
export class RecipeList implements IKosDataModel {
modelType = KosModelType.Standard;
recipes = [];
@kosServiceRequest({
path: '/api/ice/recipes',
lifecycle: DependencyLifecycle.READY
})
async handleRecipesLoaded(data): Promise<void> {
this.recipes = data.recipes;
}
}What happens:
- When the model reaches the
READYlifecycle phase, the decorator automatically sends a GET request to/api/ice/recipes - When the response arrives,
handleRecipesLoadedis called with the data - Your model updates with the latest recipes
Key concepts introduced:
- Basic decorator syntax
pathparameter (the API endpoint)lifecycleparameter (when to execute)- Handler method receives response data
Level 2: Load Specific Resource (Path Params)
Load data for a specific resource using path parameters.
import { kosModel, IKosDataModel, KosModelType, createPropKey, DependencyLifecycle } from '@kosdev-code/kos-ui-sdk';
import { ICE_APP } from '../../../utils/services-index';
const { kosServiceRequest } = ICE_APP;
const PROP_MACHINE_ID = createPropKey<IceMachine>('machineId');
@kosModel()
export class IceMachine implements IKosDataModel {
modelType = KosModelType.Standard;
machineId: string;
machineData = null;
constructor(machineId: string) {
this.machineId = machineId;
}
@kosServiceRequest({
path: '/api/ice/machines/{machineId}',
pathParams: { machineId: PROP_MACHINE_ID },
lifecycle: DependencyLifecycle.READY
})
async handleMachineLoaded(data): Promise<void> {
this.machineData = data;
}
}What happens:
- The
{machineId}placeholder in the path is replaced with the actualthis.machineIdvalue - For a machine with ID “ICE-001”, the request goes to
/api/ice/machines/ICE-001
Key concepts introduced:
pathParamsfor dynamic path segmentscreatePropKeyfor type-safe property references- Path parameter substitution (e.g.,
{machineId})
Level 3: Submit Data (POST with Body)
Send data to the server using POST requests.
import { kosModel, IKosDataModel, KosModelType } from '@kosdev-code/kos-ui-sdk';
import { ICE_APP } from '../../../utils/services-index';
const { kosServiceRequest } = ICE_APP;
@kosModel()
export class RecipeCreator implements IKosDataModel {
modelType = KosModelType.Standard;
newRecipeName = '';
temperature = 0;
recipeId = null;
@kosServiceRequest({
path: '/api/ice/recipes',
method: 'post',
bodyParams: function(this: RecipeCreator) {
return {
name: this.newRecipeName,
temperature: this.temperature
};
}
})
async createRecipe(params: {}, ctx: KosExecutionContext): Promise<void> {
const [error, data] = await ctx.$request();
if (error) {
console.error('Failed to create recipe:', error);
return;
}
console.log('Created recipe:', data.id);
this.recipeId = data.id;
}
// Called from UI or other methods
async submitNewRecipe(name: string, temp: number): Promise<void> {
this.newRecipeName = name;
this.temperature = temp;
await this.createRecipe({}, {} as KosExecutionContext);
}
}What happens:
- When
createRecipeis called (manually, not via lifecycle), the decorator injectsctx.$request - The
bodyParamsfunction builds the request body from current model state - Calling
ctx.$request()executes the POST and returns[error, data]tuple - The method handles the response and updates model state
Key concepts introduced:
method: 'post'for POST requestsbodyParamsfunction to build request body dynamically- Manual invocation with
ctx.$request()pattern - Error-first tuple response handling
Level 4: Filter with Query Parameters
Use query parameters for filtering, pagination, or search.
import { kosModel, IKosDataModel, KosModelType, DependencyLifecycle } from '@kosdev-code/kos-ui-sdk';
import { ICE_APP } from '../../../utils/services-index';
const { kosServiceRequest } = ICE_APP;
@kosModel()
export class BatchViewer implements IKosDataModel {
modelType = KosModelType.Standard;
pageSize = 50;
currentPage = 0;
filterStatus = 'active';
batches = [];
@kosServiceRequest({
path: '/api/ice/batches',
queryParams: function(this: BatchViewer) {
return {
limit: this.pageSize,
offset: this.currentPage * this.pageSize,
status: this.filterStatus
};
},
lifecycle: DependencyLifecycle.READY
})
async handleBatchesLoaded(data): Promise<void> {
this.batches = data.batches;
}
}What happens:
- Query parameters are appended to the URL:
/api/ice/batches?limit=50&offset=0&status=active - The
queryParamsfunction computes values dynamically from model state - Changing
currentPageorfilterStatusallows re-triggering with new parameters
Key concepts introduced:
queryParamsfor URL query string parameters- Dynamic parameter computation
- Pagination and filtering patterns
Level 5: Transform Response Data
Transform API response data into your preferred model format.
import { kosModel, IKosDataModel, KosModelType, DependencyLifecycle } from '@kosdev-code/kos-ui-sdk';
import { ICE_APP } from '../../../utils/services-index';
const { kosServiceRequest } = ICE_APP;
interface ModelRecipe {
id: string;
name: string;
created: Date;
}
@kosModel()
export class RecipeManager implements IKosDataModel {
modelType = KosModelType.Standard;
recipes: ModelRecipe[] = [];
@kosServiceRequest({
path: '/api/ice/recipes',
transform: (response) => {
return response.recipes.map(r => ({
id: r.recipe_id,
name: r.display_name,
created: new Date(r.created_timestamp)
}));
},
lifecycle: DependencyLifecycle.READY
})
async handleRecipesLoaded(transformedData: ModelRecipe[]): Promise<void> {
this.recipes = transformedData;
}
}What happens:
- The
transformfunction runs before the handler is called - API field names (
recipe_id,display_name) are mapped to model names (id,name) - Timestamp strings are converted to Date objects
- Handler receives already-transformed data
Key concepts introduced:
transformfunction for data transformation- Field name mapping (API format → model format)
- Type conversion (string → Date)
- Separation of API types from model types
Working with Lifecycles
Service requests execute at specific model lifecycle phases. Understanding when to use each phase is critical for proper initialization order.
enum DependencyLifecycle {
CONSTRUCT = 'construct', // Model construction (rare)
INIT = 'init', // Model initialization (common)
READY = 'ready', // After init, before activate (most common)
ACTIVATE = 'activate' // UI activation phase
}Multiple Lifecycle Requests
Load different data at different lifecycle phases to ensure proper dependency order:
import { kosModel, IKosDataModel, KosModelType, DependencyLifecycle } from '@kosdev-code/kos-ui-sdk';
import { ICE_APP } from '../../../utils/services-index';
const { kosServiceRequest, type ApiPath, type ApiResponse } = ICE_APP;
const PATH_APP_CONFIG: ApiPath = '/api/ice/config';
const PATH_ICE_STATUS: ApiPath = '/api/ice/machine/status';
type AppConfigResponse = ApiResponse<typeof PATH_APP_CONFIG, 'get'>;
type IceStatusResponse = ApiResponse<typeof PATH_ICE_STATUS, 'get'>;
@kosModel()
export class IceMachineManager implements IKosDataModel {
modelType = KosModelType.Standard;
config: AppConfigResponse | null = null;
status: IceStatusResponse | null = null;
// Load configuration early during INIT
@kosServiceRequest({
path: PATH_APP_CONFIG,
lifecycle: DependencyLifecycle.INIT
})
async handleConfigLoaded(data: AppConfigResponse): Promise<void> {
this.config = data;
}
// Load status during READY (after config is available)
@kosServiceRequest({
path: PATH_ICE_STATUS,
lifecycle: DependencyLifecycle.READY
})
async handleStatusLoaded(data: IceStatusResponse): Promise<void> {
this.status = data;
}
}Best practices:
- INIT: Core configuration data needed before other operations
- READY: Most service requests (default, recommended)
- ACTIVATE: Data needed only when UI is active
Response Caching
Control how long responses are cached to optimize performance and reduce unnecessary network requests.
Cache Retention Strategies
import { kosModel, IKosDataModel, KosModelType, ResponseRetention } from '@kosdev-code/kos-ui-sdk';
import { ICE_APP } from '../../../utils/services-index';
const { kosServiceRequest, type ApiPath } = ICE_APP;
const PATH_ONE_TIME_DATA: ApiPath = '/api/ice/onetime';
const PATH_MACHINE_STATUS: ApiPath = '/api/ice/machine/status';
const PATH_CACHED_DATA: ApiPath = '/api/ice/cached';
const PATH_CONFIG: ApiPath = '/api/ice/config';
@kosModel()
export class CachingModel implements IKosDataModel {
modelType = KosModelType.Standard;
// IMMEDIATE - Clear after handler completes
@kosServiceRequest({
path: PATH_ONE_TIME_DATA,
cache: { retention: ResponseRetention.IMMEDIATE }
})
async handleOneTimeData(data: any): Promise<void> { }
// SINGLE - Keep until next request (default)
@kosServiceRequest({
path: PATH_MACHINE_STATUS,
cache: { retention: ResponseRetention.SINGLE }
})
async handleStatus(data: any): Promise<void> { }
// TTL - Time-based expiration
@kosServiceRequest({
path: PATH_CACHED_DATA,
cache: {
retention: ResponseRetention.TTL,
ttl: 5 * 60 * 1000 // 5 minutes
}
})
async handleCachedData(data: any): Promise<void> { }
// PERMANENT - Keep for model lifetime
@kosServiceRequest({
path: PATH_CONFIG,
cache: { retention: ResponseRetention.PERMANENT }
})
async handleConfig(data: any): Promise<void> { }
}TTL Extension Pattern
Extend TTL on refresh instead of re-fetching for expensive operations:
import { kosModel, IKosDataModel, KosModelType, ResponseRetention } from '@kosdev-code/kos-ui-sdk';
import { ICE_APP } from '../../../utils/services-index';
const { kosServiceRequest, type ApiPath } = ICE_APP;
const PATH_EXPENSIVE_OPERATION: ApiPath = '/api/ice/analytics/compute';
@kosModel()
export class AnalyticsModel implements IKosDataModel {
modelType = KosModelType.Standard;
@kosServiceRequest({
path: PATH_EXPENSIVE_OPERATION,
cache: {
retention: ResponseRetention.TTL,
ttl: 10 * 60 * 1000, // 10 minutes
extendOnRefresh: true // Extend TTL instead of re-fetching
}
})
async handleAnalyticsData(data: any): Promise<void> {
// If cached data exists and hasn't expired,
// TTL is extended without re-fetching
}
}Creating Child Models (iterateOver)
Create child models from array responses using the container pattern:
import {
kosModel,
kosContainerAware,
KosContainerAware,
IKosDataModel,
KosModelType,
DependencyLifecycle
} from '@kosdev-code/kos-ui-sdk';
import { ICE_APP } from '../../../utils/services-index';
const { kosServiceRequest, type ApiPath, type ApiResponse } = ICE_APP;
const PATH_RECIPES: ApiPath = '/api/ice/recipes';
type RecipesResponse = ApiResponse<typeof PATH_RECIPES, 'get'>;
interface RecipeContainerImpl extends KosContainerAware<RecipeModel> {}
@kosModel()
@kosContainerAware<RecipeModel>()
export class RecipeContainerImpl implements IKosDataModel {
modelType = KosModelType.Container;
@kosServiceRequest({
path: PATH_RECIPES,
lifecycle: DependencyLifecycle.READY,
iterateOver: 'recipes',
modelFactory: RecipeModel, // Registration object (NOT .factory)
idExtractor: (item) => item.id, // Extract ID from mapped options
mappings: [
{ from: 'recipe_id', to: 'id' },
{ from: 'display_name', to: 'name' }
]
})
async handleRecipesLoaded(recipes: RecipeModel[]): Promise<void> {
// Framework creates RecipeModel instances using:
// 1. mappings transform API data: { recipe_id, display_name } -> { id, name }
// 2. idExtractor gets ID from mapped options
// 3. Registration object creates model: RecipeModel.instance(id).options({ id, name }).build()
// 4. Automatically adds models to this container
}
}Key Points:
modelFactorytakes the registration object (e.g.,RecipeModel), NOT a factory function- Framework uses builder pattern:
Model.instance(id).options(opts).build() mappingstransform API fields into valid model optionsidExtractorgets ID from the mapped options (after mappings applied)- Without
modelFactory, returns array of plain objects with mappings applied
Alternative: Use transform function instead of mappings
@kosServiceRequest({
path: PATH_RECIPES,
iterateOver: 'recipes',
modelFactory: RecipeModel,
idExtractor: (item) => item.id,
transform: (response: RecipesResponse) => {
// Transform each recipe item into model options
return response.recipes.map(item => ({
id: item.recipe_id,
name: item.display_name,
calories: item.metadata?.calories || 0,
isAvailable: !item.sold_out
}));
}
})Baseline-Delta Synchronization ⭐
The Problem
When initializing models that use both HTTP baseline requests (@kosServiceRequest) and WebSocket delta events (@kosTopicHandler), race conditions can occur:
T0: HTTP Request sent (baseline recipes list)
T100: WebSocket event arrives (recipe R4 quantity updated)
T200: HTTP Response received (baseline recipes list)If the WebSocket event handler runs before the baseline loads, the update is lost when the baseline overwrites the model state.
The Solution: requiresBaseline
The requiresBaseline parameter on @kosTopicHandler ensures delta events wait for baseline data to load:
import {
kosModel,
kosTopicHandler,
IKosDataModel,
KosModelType,
DependencyLifecycle,
ReplayStrategy
} from '@kosdev-code/kos-ui-sdk';
import { ICE_APP } from '../../../utils/services-index';
const { kosServiceRequest, type ApiPath, type ApiResponse } = ICE_APP;
const PATH_RECIPES: ApiPath = '/api/ice/recipes';
const TOPIC_RECIPE_UPDATE = '/ice/recipes/update';
type RecipesResponse = ApiResponse<typeof PATH_RECIPES, 'get'>;
interface RecipeUpdatePayload {
id: string;
quantity: number;
}
interface Recipe {
id: string;
quantity: number;
}
@kosModel()
export class RecipeContainer implements IKosDataModel {
modelType = KosModelType.Container;
recipes: Map<string, Recipe> = new Map();
// Step 1: Load baseline data
@kosServiceRequest({
path: PATH_RECIPES,
lifecycle: DependencyLifecycle.READY
})
async handleRecipesLoaded(data: RecipesResponse): Promise<void> {
data.recipes.forEach(recipe => {
this.recipes.set(recipe.id, recipe);
});
}
// Step 2: Handle delta events with baseline dependency
@kosTopicHandler({
topic: TOPIC_RECIPE_UPDATE,
websocket: true,
lifecycle: DependencyLifecycle.READY,
requiresBaseline: {
path: PATH_RECIPES,
method: 'get',
replayStrategy: ReplayStrategy.AFTER_REQUEST
}
})
handleRecipeUpdate(payload: RecipeUpdatePayload): void {
const recipe = this.recipes.get(payload.id);
if (recipe) {
recipe.quantity = payload.quantity;
}
}
}Replay Strategies
The replayStrategy parameter controls which queued events are replayed after the baseline loads:
enum ReplayStrategy {
// Replay all queued events (for state machines, audit trails)
ALL = 'all',
// Replay events received >= baseline request time (default, recommended)
// Best for: High-frequency idempotent updates
AFTER_REQUEST = 'after-request',
// Replay events received >= baseline response time
// Best for: Low-frequency config where baseline likely includes changes
AFTER_RESPONSE = 'after-response',
// Discard all queued events (baseline is complete source of truth)
NONE = 'none'
}How It Works
T0: @kosServiceRequest sends HTTP baseline request
└─> Framework captures requestedAt = Date.now()
T100: @kosTopicHandler subscriptions register
└─> requiresBaseline: { path, replayStrategy }
T150: Delta events arrive (model not ready OR baseline not ready)
└─> Events queued with receivedAt = Date.now()
T300: Baseline HTTP response arrives
└─> Framework captures receivedAt = Date.now()
└─> Baseline handler executes
└─> Framework triggers queue flush with temporal filtering
└─> ReplayStrategy.AFTER_REQUEST:
- Only replay events where receivedAt >= requestedAt
- Discard events that arrived before baseline requestMultiple Baseline Dependencies
Handle multiple baseline requests with separate delta handlers:
import {
kosModel,
kosTopicHandler,
IKosDataModel,
KosModelType,
DependencyLifecycle,
ReplayStrategy
} from '@kosdev-code/kos-ui-sdk';
import { ICE_APP } from '../../../utils/services-index';
const { kosServiceRequest, type ApiPath, type ApiResponse } = ICE_APP;
const PATH_RECIPES: ApiPath = '/api/ice/recipes';
const PATH_INVENTORY: ApiPath = '/api/ice/inventory';
const TOPIC_RECIPE_UPDATE = '/ice/recipes/update';
const TOPIC_INVENTORY_UPDATE = '/ice/inventory/update';
type RecipesResponse = ApiResponse<typeof PATH_RECIPES, 'get'>;
type InventoryResponse = ApiResponse<typeof PATH_INVENTORY, 'get'>;
interface RecipeUpdatePayload {
id: string;
quantity: number;
}
interface InventoryUpdatePayload {
id: string;
available: boolean;
}
interface Recipe {
id: string;
quantity: number;
}
interface InventoryItem {
id: string;
available: boolean;
}
@kosModel()
export class IceSystemManager implements IKosDataModel {
modelType = KosModelType.Standard;
recipes: Map<string, Recipe> = new Map();
inventory: Map<string, InventoryItem> = new Map();
// Baseline 1: Recipes
@kosServiceRequest({
path: PATH_RECIPES,
lifecycle: DependencyLifecycle.READY
})
async handleRecipesLoaded(data: RecipesResponse): Promise<void> {
data.recipes.forEach(recipe => this.recipes.set(recipe.id, recipe));
}
// Baseline 2: Inventory
@kosServiceRequest({
path: PATH_INVENTORY,
lifecycle: DependencyLifecycle.READY
})
async handleInventoryLoaded(data: InventoryResponse): Promise<void> {
data.items.forEach(item => this.inventory.set(item.id, item));
}
// Delta handler 1: Depends on recipes baseline
@kosTopicHandler({
topic: TOPIC_RECIPE_UPDATE,
websocket: true,
requiresBaseline: {
path: PATH_RECIPES,
replayStrategy: ReplayStrategy.AFTER_REQUEST
}
})
handleRecipeUpdate(payload: RecipeUpdatePayload): void {
const recipe = this.recipes.get(payload.id);
if (recipe) {
recipe.quantity = payload.quantity;
}
}
// Delta handler 2: Depends on inventory baseline
@kosTopicHandler({
topic: TOPIC_INVENTORY_UPDATE,
websocket: true,
requiresBaseline: {
path: PATH_INVENTORY,
replayStrategy: ReplayStrategy.AFTER_RESPONSE
}
})
handleInventoryUpdate(payload: InventoryUpdatePayload): void {
const item = this.inventory.get(payload.id);
if (item) {
item.available = payload.available;
}
}
}Best practices:
- Always use
requiresBaselinewhen combining HTTP + WebSocket - Choose
AFTER_REQUESTfor high-frequency updates - Choose
AFTER_RESPONSEfor low-frequency config changes
Advanced Topics
Conditional Execution
Execute requests based on runtime conditions:
import { kosModel, IKosDataModel, KosModelType } from '@kosdev-code/kos-ui-sdk';
import { ICE_APP } from '../../../utils/services-index';
const { kosServiceRequest, type ApiPath } = ICE_APP;
const PATH_OPTIONAL_DATA: ApiPath = '/api/ice/optional';
@kosModel()
export class ConditionalLoader implements IKosDataModel {
modelType = KosModelType.Standard;
shouldLoadData = true;
@kosServiceRequest({
path: PATH_OPTIONAL_DATA,
condition: (model: ConditionalLoader) => model.shouldLoadData
})
async handleOptionalData(data: any): Promise<void> {
// Only executes if shouldLoadData is true
}
}Multiple Service Interfaces
KOS applications interact with multiple API domains, each with its own OpenAPI specification and typed service interface:
Core SDK Services (shipped with SDK packages):
- KOS_SERVICE (
@kosdev-code/kos-ui-sdk) – Device system services (config, logs, device info) - DISPENSE_SERVICE (
@kosdev-code/kos-dispense-sdk) – Beverage dispensing operations - FREESTYLE_SERVICE (
@kosdev-code/kos-freestyle-sdk) – Freestyle cartridge management - VFS_SERVICE (
@kosdev-code/kos-ui-sdk) – Virtual file system operations
Custom Application Services (generated for your app):
- ICE_APP (from
services-index) – Your application-specific APIs - CUSTOM_BACKEND (from
services-index) – Custom backend integrations
Example: SDK Service for Device Configuration
import {
kosModel,
kosTopicHandler,
IKosDataModel,
KosModelType,
DependencyLifecycle,
ReplayStrategy,
KOS_SERVICE
} from '@kosdev-code/kos-ui-sdk';
const { kosServiceRequest, type ApiPath, type ApiResponse } = KOS_SERVICE;
const PATH_DEVICE_CONFIG: ApiPath = '/api/kos/config/details/1';
const TOPIC_CONFIG_UPDATE = '/kos/config/update';
type DeviceConfigResponse = ApiResponse<typeof PATH_DEVICE_CONFIG, 'get'>;
interface ConfigUpdatePayload {
path: string;
value: any;
}
@kosModel()
export class DeviceConfigManager implements IKosDataModel {
modelType = KosModelType.Standard;
config: Map<string, any> = new Map();
// Load device configuration baseline
@kosServiceRequest({
path: PATH_DEVICE_CONFIG,
lifecycle: DependencyLifecycle.READY
})
async handleConfigLoaded(data: DeviceConfigResponse): Promise<void> {
Object.entries(data).forEach(([path, value]) => {
this.config.set(path, value);
});
}
// Handle configuration updates with baseline dependency
@kosTopicHandler({
topic: TOPIC_CONFIG_UPDATE,
websocket: true,
requiresBaseline: {
path: PATH_DEVICE_CONFIG,
replayStrategy: ReplayStrategy.AFTER_RESPONSE
}
})
handleConfigUpdate(payload: ConfigUpdatePayload): void {
this.config.set(payload.path, payload.value);
}
}Decision Guide:
Use Custom Service Interfaces (ICE_APP, etc.) when:
- Building application-specific features with custom APIs
- Working with backend endpoints unique to your application
- Implementing business logic specific to your app domain
- Your OpenAPI spec defines the endpoints you need
Use Core SDK Services (KOS_SERVICE, DISPENSE_SERVICE, etc.) when:
- Accessing device-level functionality (config, logs, device status)
- Working with standard beverage dispensing operations
- Managing Freestyle cartridges or ingredients
- Using VFS or other standard KOS platform services
Complex Transformations
Use property mappings for complex field transformations:
import { kosModel, IKosDataModel, KosModelType } from '@kosdev-code/kos-ui-sdk';
import { ICE_APP } from '../../../utils/services-index';
const { kosServiceRequest, type ApiPath, type ApiResponse } = ICE_APP;
const PATH_MACHINE_INFO: ApiPath = '/api/ice/machine/info';
type MachineInfoResponse = ApiResponse<typeof PATH_MACHINE_INFO, 'get'>;
interface TransformedMachineInfo {
name: string;
serialNumber: string;
lastMaintenance: Date;
}
@kosModel()
export class MachineInfoModel implements IKosDataModel {
modelType = KosModelType.Standard;
@kosServiceRequest({
path: PATH_MACHINE_INFO,
mappings: [
{ from: 'machine_name', to: 'name' },
{ from: 'serial_number', to: 'serialNumber' },
{
from: 'last_maintenance_timestamp',
to: 'lastMaintenance',
transform: (timestamp: number) => new Date(timestamp)
}
]
})
async handleMachineInfoLoaded(data: TransformedMachineInfo): Promise<void> {
Object.assign(this, data);
}
}Reference
Complete Configuration Options
interface KosServiceRequestParams {
// Required
path: string; // API endpoint path
// Optional - Request Configuration
method?: 'get' | 'post' | 'put' | 'delete'; // Default: 'get'
lifecycle?: DependencyLifecycle; // When to execute (Default: READY)
// Optional - Dynamic Parameters
pathParams?: PropKeyTemplate | (() => Record<string, string>);
queryParams?: PropKeyTemplate | (() => Record<string, any>);
bodyParams?: () => any;
// Optional - Response Processing
transform?: (response: any) => any;
mappings?: PropertyMapping[];
iterateOver?: string; // Create models from array
// Optional - Caching
cache?: ResponseCacheConfig;
// Optional - Conditional Execution
condition?: (model: any) => boolean;
}Error Handling Strategies
The framework provides automatic error handling with retry logic:
import { kosModel, IKosDataModel, KosModelType, ResponseRetention } from '@kosdev-code/kos-ui-sdk';
import { ICE_APP } from '../../../utils/services-index';
const { kosServiceRequest, type ApiPath, type ApiResponse } = ICE_APP;
const PATH_FLAKY_ENDPOINT: ApiPath = '/api/ice/flaky';
type DataResponse = ApiResponse<typeof PATH_FLAKY_ENDPOINT, 'get'>;
@kosModel()
export class DataLoader implements IKosDataModel {
modelType = KosModelType.Standard;
@kosServiceRequest({
path: PATH_FLAKY_ENDPOINT,
cache: { retention: ResponseRetention.SINGLE }
})
async handleData(data: DataResponse): Promise<void> {
// Handler only called on successful response
// Errors are automatically logged and can be accessed via:
// this.getServiceResponse(PATH_FLAKY_ENDPOINT)
}
}Migration from Manual Pattern
Before: Manual Service Calls
import { kosModel, IKosDataModel, KosModelType } from '@kosdev-code/kos-ui-sdk';
import { fetchRecipes } from './services/recipe-service';
interface Recipe {
id: string;
name: string;
}
@kosModel()
export class RecipeManager implements IKosDataModel {
modelType = KosModelType.Standard;
recipes: Recipe[] = [];
async init(): Promise<void> {
const [error, data] = await fetchRecipes();
if (error) {
console.error('Failed to load recipes:', error);
return;
}
this.recipes = data.recipes;
}
}After: Declarative with @kosServiceRequest
import {
kosModel,
IKosDataModel,
KosModelType,
DependencyLifecycle
} from '@kosdev-code/kos-ui-sdk';
import { ICE_APP } from '../../../utils/services-index';
const { kosServiceRequest, type ApiPath, type ApiResponse } = ICE_APP;
const PATH_RECIPES: ApiPath = '/api/ice/recipes';
type RecipesResponse = ApiResponse<typeof PATH_RECIPES, 'get'>;
interface Recipe {
id: string;
name: string;
}
@kosModel()
export class RecipeManager implements IKosDataModel {
modelType = KosModelType.Standard;
recipes: Recipe[] = [];
@kosServiceRequest({
path: PATH_RECIPES,
lifecycle: DependencyLifecycle.READY
})
async handleRecipesLoaded(data: RecipesResponse): Promise<void> {
this.recipes = data.recipes;
}
}Complete Example
A comprehensive example combining multiple features:
import {
kosModel,
kosTopicHandler,
KosContainerAware,
kosContainerAware,
IKosDataModel,
KosModelType,
DependencyLifecycle,
ResponseRetention,
ReplayStrategy
} from '@kosdev-code/kos-ui-sdk';
import { ICE_APP } from '../../../utils/services-index';
const { kosServiceRequest, type ApiPath, type ApiResponse } = ICE_APP;
const PATH_RECIPES: ApiPath = '/api/ice/recipes';
const TOPIC_RECIPE_UPDATE = '/ice/recipes/update';
type RecipesResponse = ApiResponse<typeof PATH_RECIPES, 'get'>;
interface RecipeUpdatePayload {
recipeId: string;
newQuantity: number;
}
interface RecipeContainerImpl extends KosContainerAware<RecipeModel> {}
@kosModel()
@kosContainerAware<RecipeModel>()
export class RecipeContainerImpl implements IKosDataModel {
modelType = KosModelType.Container;
// Baseline: Load all recipes during READY phase
@kosServiceRequest({
path: PATH_RECIPES,
lifecycle: DependencyLifecycle.READY,
cache: {
retention: ResponseRetention.TTL,
ttl: 5 * 60 * 1000 // Cache for 5 minutes
},
iterateOver: 'recipes',
mappings: [
{ from: 'recipe_id', to: 'id' },
{ from: 'display_name', to: 'name' },
{ from: 'quantity_ml', to: 'quantity' }
]
})
async handleRecipesLoaded(recipes: RecipeModel[]): Promise<void> {
// Framework automatically creates child models and adds to container
}
// Delta: Handle real-time updates with baseline dependency
@kosTopicHandler({
topic: TOPIC_RECIPE_UPDATE,
websocket: true,
lifecycle: DependencyLifecycle.READY,
requiresBaseline: {
path: PATH_RECIPES,
method: 'get',
replayStrategy: ReplayStrategy.AFTER_REQUEST
},
transform: (payload: RecipeUpdatePayload) => ({
id: payload.recipeId,
quantity: payload.newQuantity
})
})
handleRecipeUpdate(data: { id: string; quantity: number }): void {
const recipe = this.getModel(data.id);
if (recipe) {
recipe.quantity = data.quantity;
}
}
}Appendix: Service Architecture Decision Guide
Understanding KOS Service Architecture
Most KOS applications use both custom application services and core SDK services:
// Custom app models use ICE_APP for business logic
import { ICE_APP } from '../../../utils/services-index';
const { kosServiceRequest, type ApiPath, type ApiResponse } = ICE_APP;
// Device integration models use SDK services for platform features
import { KOS_SERVICE } from '@kosdev-code/kos-ui-sdk';
const { kosServiceRequest, type ApiPath, type ApiResponse } = KOS_SERVICE;Typical Application Architecture
Custom Application Layer:
- Business logic specific to your application
- Application-specific API endpoints
- Domain models for your use case
- Uses
ICE_APP,CUSTOM_BACKEND, or other generated service interfaces
Platform Integration Layer:
- Device-level functionality
- Standard beverage operations
- System configuration and monitoring
- Uses
KOS_SERVICE,DISPENSE_SERVICE,FREESTYLE_SERVICE, etc.
Import Guide
For custom application services (most common):
import {
kosModel,
IKosDataModel,
KosModelType,
DependencyLifecycle
} from '@kosdev-code/kos-ui-sdk';
import { ICE_APP } from '../../../utils/services-index';
const { kosServiceRequest, type ApiPath, type ApiResponse } = ICE_APP;For SDK core services (device functionality):
import {
kosModel,
IKosDataModel,
KosModelType,
DependencyLifecycle,
KOS_SERVICE
} from '@kosdev-code/kos-ui-sdk';
const { kosServiceRequest, type ApiPath, type ApiResponse } = KOS_SERVICE;Define path constants and import response types:
// For ice app recipes
import { ICE_APP } from '../../../utils/services-index';
const { kosServiceRequest, type ApiPath, type ApiResponse } = ICE_APP;
const PATH_RECIPES: ApiPath = '/api/ice/recipes';
type RecipesResponse = ApiResponse<typeof PATH_RECIPES, 'get'>;Conclusion
The @kosServiceRequest decorator provides a powerful, declarative approach to HTTP service integration in KOS models. Combined with the baseline-delta race condition solution via requiresBaseline, it ensures reliable, type-safe data synchronization across HTTP and WebSocket channels.
Key takeaways:
- Start simple: Begin with basic GET requests and add complexity progressively
- Declarative > Imperative: Define what data you need, not how to fetch it
- Lifecycle-Aware: Requests execute at the right time automatically
- Baseline-Delta Safe: Use
requiresBaselineto prevent race conditions - Type-Safe: Full TypeScript inference from OpenAPI specifications
- Cache-Smart: Flexible retention strategies for optimal performance
- Service-Specific: Import from custom services (ICE_APP) for app logic, SDK services for device functionality
