HTTP Integration and Routing – Building Web-Accessible KOS Applications

Beyond Device Communication: Web API Integration

KOS excels at managing device communication and real-time operations, but modern applications often need to expose their functionality through web APIs. Whether building REST endpoints for external integration, creating webhook handlers for third-party services, or providing administrative interfaces accessible via HTTP, the ability to bridge KOS’s device-centric architecture with standard web protocols becomes essential.

KOS HTTP integration transforms models into web-accessible services through declarative routing decorators that feel familiar to developers coming from web frameworks like Express or NestJS. This integration maintains KOS’s reactive patterns and lifecycle management while providing the HTTP semantics expected by web clients.

Declarative HTTP Routing

The foundation of KOS HTTP integration lies in decorative routing that allows you to expose model methods as HTTP endpoints with minimal configuration. The system automatically handles the complexity of HTTP request routing, parameter parsing, and response formatting.

import { kosModel, get, post, put, del, IKosDataModel } from "@kosdev-code/kos-ui-sdk";
import type { KosRequest, KosResponse } from "@kosdev-code/kos-ui-sdk";
@kosModel({ modelTypeId: "device-api-model", singleton: false })
export class DeviceApiModelImpl implements IKosDataModel {
  private devices = new Map<string, DeviceInfo>();
  @get("/api/devices")
  async getAllDevices(req: KosRequest<"/api/devices">, res: KosResponse): Promise<void> {
    const devices = Array.from(this.devices.values());
    res.status?.(200).send({
      devices,
      count: devices.length,
      timestamp: new Date().toISOString()
    });
  }
  @get("/api/devices/:id")
  async getDevice(req: KosRequest<"/api/devices/:id">, res: KosResponse): Promise<void> {
    const device = this.devices.get(req.params.id);
    if (!device) {
      res.status?.(404).send({ error: "Device not found" });
      return;
    }
    res.status?.(200).send(device);
  }
  @post("/api/devices")
  async createDevice(req: KosRequest<"/api/devices">, res: KosResponse): Promise<void> {
    const { name, type, location } = req.body;
    if (!name || !type) {
      res.status?.(400).send({ 
        error: "Device name and type are required" 
      });
      return;
    }
    const id = `device-${Date.now()}`;
    const newDevice: DeviceInfo = {
      id,
      name,
      type,
      location,
      status: "online",
      createdAt: new Date()
    };
    this.devices.set(id, newDevice);
    res.status?.(201).send(newDevice);
  }
  @put("/api/devices/:id/status")
  async updateDeviceStatus(req: KosRequest<"/api/devices/:id/status">, res: KosResponse): Promise<void> {
    const device = this.devices.get(req.params.id);
    if (!device) {
      res.status?.(404).send({ error: "Device not found" });
      return;
    }
    const { status } = req.body;
    if (!["online", "offline", "maintenance"].includes(status)) {
      res.status?.(400).send({ 
        error: "Invalid status. Must be: online, offline, or maintenance" 
      });
      return;
    }
    device.status = status;
    device.lastUpdated = new Date();
    res.status?.(200).send(device);
  }
  @del("/api/devices/:id")
  async removeDevice(req: KosRequest<"/api/devices/:id">, res: KosResponse): Promise<void> {
    if (!this.devices.has(req.params.id)) {
      res.status?.(404).send({ error: "Device not found" });
      return;
    }
    this.devices.delete(req.params.id);
    res.status?.(204).send({});
  }
}

This approach provides clean, type-safe HTTP endpoints that integrate seamlessly with KOS model lifecycle and state management. The decorators handle request routing automatically, while the model methods focus on business logic and proper HTTP response handling.

Automatic Route Lifecycle Management

One of the most powerful aspects of KOS HTTP integration is its automatic lifecycle management. Routes are registered and cleaned up in sync with model lifecycle, eliminating common issues like route conflicts and memory leaks.

@kosModel({ modelTypeId: "sensor-monitoring-api", singleton: false })
export class SensorMonitoringApiImpl implements IKosDataModel {
  private sensorReadings = new Map<string, SensorReading[]>();
  async load(): Promise<void> {
    // Routes are automatically registered during model load phase
    this.logger.info("Sensor monitoring API is now available");
    // No manual route registration needed
  }
  @get("/api/sensors/:sensorId/readings")
  async getSensorReadings(req: KosRequest<"/api/sensors/:sensorId/readings">, res: KosResponse): Promise<void> {
    const readings = this.sensorReadings.get(req.params.sensorId) || [];
    const limit = parseInt(req.query?.limit || "100");
    const offset = parseInt(req.query?.offset || "0");
    const paginatedReadings = readings.slice(offset, offset + limit);
    res.status?.(200).send({
      sensorId: req.params.sensorId,
      readings: paginatedReadings,
      totalCount: readings.length,
      limit,
      offset
    });
  }
  @post("/api/sensors/:sensorId/readings")
  async addSensorReading(req: KosRequest<"/api/sensors/:sensorId/readings">, res: KosResponse): Promise<void> {
    const { value, unit, timestamp } = req.body;
    const sensorId = req.params.sensorId;
    if (typeof value !== "number") {
      res.status?.(400).send({ error: "Reading value must be a number" });
      return;
    }
    const reading: SensorReading = {
      value,
      unit: unit || "unknown",
      timestamp: timestamp ? new Date(timestamp) : new Date(),
      sensorId
    };
    if (!this.sensorReadings.has(sensorId)) {
      this.sensorReadings.set(sensorId, []);
    }
    this.sensorReadings.get(sensorId)!.push(reading);
    res.status?.(201).send(reading);
  }
  unload(): void {
    // Routes are automatically removed during model unload phase
    this.logger.info("Sensor monitoring API is shutting down");
    // No manual route cleanup needed
  }
}

When the model loads, all decorated methods automatically become available as HTTP endpoints. When the model unloads, these routes are automatically removed, ensuring clean resource management and preventing stale endpoint handlers.

Dynamic Routing with PropKeys

KOS HTTP integration supports dynamic route generation based on model properties through PropKeys. This enables creating flexible, configurable APIs that adapt to model state and configuration.

import { kosModel, get, post, createPropKey, IKosDataModel } from "@kosdev-code/kos-ui-sdk";
interface TenantApiModel {
  tenantId: string;
  apiVersion: string;
  region: string;
}
const PROP_TENANT_ID = createPropKey<TenantApiModel>("tenantId");
const PROP_API_VERSION = createPropKey<TenantApiModel>("apiVersion");
const PROP_REGION = createPropKey<TenantApiModel>("region");
@kosModel({ modelTypeId: "multi-tenant-api-model", singleton: false })
export class MultiTenantApiModelImpl implements IKosDataModel {
  tenantId = "acme-corp";
  apiVersion = "v2";
  region = "us-west";
  private tenantData = new Map<string, any>();
  // PropKeys are resolved at model load time
  @get(`/api/${PROP_API_VERSION}/tenants/${PROP_TENANT_ID}/data`)
  async getTenantData(req: KosRequest, res: KosResponse): Promise<void> {
    // Route registered as: /api/v2/tenants/acme-corp/data
    const data = Array.from(this.tenantData.values());
    res.status?.(200).send({
      tenantId: this.tenantId,
      region: this.region,
      apiVersion: this.apiVersion,
      data,
      count: data.length
    });
  }
  @post(`/api/regions/${PROP_REGION}/tenants/${PROP_TENANT_ID}/config`)
  async updateTenantConfig(req: KosRequest, res: KosResponse): Promise<void> {
    // Route registered as: /api/regions/us-west/tenants/acme-corp/config
    const configUpdate = req.body;
    // Store configuration specific to this tenant and region
    const configKey = `${this.tenantId}-${this.region}-config`;
    this.tenantData.set(configKey, {
      ...configUpdate,
      updatedAt: new Date(),
      tenantId: this.tenantId,
      region: this.region
    });
    res.status?.(200).send({ 
      message: "Configuration updated",
      tenantId: this.tenantId,
      region: this.region 
    });
  }
  // Using MODEL_ID for instance-specific endpoints
  @get("/api/models/{MODEL_ID}/health")
  async getModelHealth(req: KosRequest, res: KosResponse): Promise<void> {
    // Route registered as: /api/models/[actual-model-id]/health
    res.status?.(200).send({
      modelId: this.id,
      status: "healthy",
      tenantId: this.tenantId,
      region: this.region,
      uptime: process.uptime(),
      memoryUsage: process.memoryUsage()
    });
  }
}

PropKeys enable creating sophisticated routing patterns that scale across multi-tenant applications, versioned APIs, and region-specific deployments while maintaining type safety and clear route organization.

Integration with KOS Services and Models

HTTP endpoints can seamlessly integrate with other KOS models and services, creating web APIs that expose the full power of KOS device management and business logic.

@kosModel({ modelTypeId: "beverage-dispenser-api", singleton: false })
export class BeverageDispenserApiImpl implements IKosDataModel {
  @kosDependency({ modelType: IngredientContainer.type })
  private ingredients!: IngredientContainerModel;
  @kosDependency({ modelType: DispenserController.type })
  private dispenser!: DispenserControllerModel;
  @get("/api/beverages/available")
  async getAvailableBeverages(req: KosRequest, res: KosResponse): Promise<void> {
    const availableIngredients = this.ingredients.getAvailableIngredients();
    const beverages = availableIngredients
      .filter(ingredient => ingredient.category === "beverage-base")
      .map(ingredient => ({
        id: ingredient.id,
        name: ingredient.name,
        quantity: ingredient.quantity,
        estimatedServings: Math.floor(ingredient.quantity / ingredient.servingSize)
      }));
    res.status?.(200).send({
      beverages,
      totalAvailable: beverages.length,
      dispenserStatus: this.dispenser.status,
      lastUpdate: new Date().toISOString()
    });
  }
  @post("/api/beverages/:beverageId/dispense")
  async dispenseBeverage(req: KosRequest<"/api/beverages/:beverageId/dispense">, res: KosResponse): Promise<void> {
    const { size = "medium", customization = {} } = req.body;
    const beverageId = req.params.beverageId;
    // Validate beverage availability
    const ingredient = this.ingredients.getIngredient(beverageId);
    if (!ingredient || !ingredient.isAvailable) {
      res.status?.(404).send({ 
        error: "Beverage not available",
        beverageId 
      });
      return;
    }
    // Check dispenser status
    if (!this.dispenser.canDispense()) {
      res.status?.(503).send({ 
        error: "Dispenser not available",
        status: this.dispenser.status,
        reason: this.dispenser.unavailableReason 
      });
      return;
    }
    try {
      // Initiate dispensing operation (returns a Future)
      const dispenseFuture = await this.dispenser.startDispense({
        ingredientId: beverageId,
        size,
        customization
      });
      // For HTTP API, we return the operation ID for tracking
      res.status?.(202).send({
        message: "Dispensing started",
        operationId: dispenseFuture.id,
        estimatedDuration: dispenseFuture.estimatedDuration,
        tracking: {
          statusEndpoint: `/api/operations/${dispenseFuture.id}/status`,
          resultsEndpoint: `/api/operations/${dispenseFuture.id}/result`
        }
      });
    } catch (error) {
      res.status?.(500).send({
        error: "Failed to start dispensing",
        message: error.message
      });
    }
  }
  @get("/api/operations/:operationId/status")
  async getOperationStatus(req: KosRequest<"/api/operations/:operationId/status">, res: KosResponse): Promise<void> {
    const future = this.dispenser.getActiveFuture(req.params.operationId);
    if (!future) {
      res.status?.(404).send({ error: "Operation not found" });
      return;
    }
    res.status?.(200).send({
      operationId: future.id,
      status: future.status,
      progress: future.progress,
      phase: future.currentPhase,
      timeRemaining: future.estimatedTimeRemaining,
      canCancel: future.canCancel
    });
  }
  @get("/api/system/status")
  async getSystemStatus(req: KosRequest, res: KosResponse): Promise<void> {
    const ingredientSummary = {
      total: this.ingredients.ingredients.length,
      available: this.ingredients.getAvailableIngredients().length,
      lowStock: this.ingredients.getLowStockIngredients().length,
      expired: this.ingredients.getExpiredIngredients().length
    };
    const dispenserSummary = {
      status: this.dispenser.status,
      activeOperations: this.dispenser.getActiveOperations().length,
      totalDispensedToday: this.dispenser.getTodaysDispenseCount(),
      maintenanceRequired: this.dispenser.requiresMaintenance
    };
    res.status?.(200).send({
      system: {
        status: "operational",
        timestamp: new Date().toISOString()
      },
      ingredients: ingredientSummary,
      dispenser: dispenserSummary
    });
  }
}

This integration pattern creates web APIs that expose sophisticated device operations through clean HTTP interfaces while maintaining all the benefits of KOS’s reactive architecture and lifecycle management.

Request Testing and API Consumption

KOS provides the kosFetch utility for testing HTTP routes and consuming APIs within the KOS environment. This tool handles the low-level messaging required to route HTTP requests between models and applications.

import { kosFetch } from "@kosdev-code/kos-ui-sdk";
@kosModel({ modelTypeId: "api-client-model", singleton: false })
export class ApiClientModelImpl implements IKosDataModel {
  private apiBaseUrl = "/api/v2";
  private destinationAddress = "beverage-system";
  async fetchAvailableBeverages(): Promise<BeverageInfo[]> {
    try {
      const response = await kosFetch({
        method: "GET",
        url: `${this.apiBaseUrl}/beverages/available`,
        destinationAddress: this.destinationAddress
      });
      return response.beverages || [];
    } catch (error) {
      this.logger.error("Failed to fetch available beverages", error);
      return [];
    }
  }
  async dispenseBeverage(beverageId: string, options: DispenseOptions): Promise<DispenseResult> {
    try {
      const response = await kosFetch({
        method: "POST",
        url: `${this.apiBaseUrl}/beverages/${beverageId}/dispense`,
        destinationAddress: this.destinationAddress,
        body: {
          size: options.size || "medium",
          customization: options.customization || {}
        }
      });
      // Poll for completion if we got an operation ID
      if (response.operationId) {
        return await this.pollForCompletion(response.operationId);
      }
      return response;
    } catch (error) {
      this.logger.error("Failed to dispense beverage", error);
      throw new Error(`Dispense failed: ${error.message}`);
    }
  }
  private async pollForCompletion(operationId: string): Promise<DispenseResult> {
    let attempts = 0;
    const maxAttempts = 30; // 30 seconds timeout
    while (attempts < maxAttempts) {
      try {
        const statusResponse = await kosFetch({
          method: "GET",
          url: `/api/operations/${operationId}/status`,
          destinationAddress: this.destinationAddress
        });
        if (statusResponse.status === "completed") {
          return statusResponse.result;
        }
        if (statusResponse.status === "failed") {
          throw new Error(`Operation failed: ${statusResponse.error}`);
        }
        // Wait 1 second before next poll
        await new Promise(resolve => setTimeout(resolve, 1000));
        attempts++;
      } catch (error) {
        this.logger.error("Failed to poll operation status", error);
        throw error;
      }
    }
    throw new Error("Operation timeout");
  }
  async getSystemHealth(): Promise<SystemHealthInfo> {
    const response = await kosFetch({
      method: "GET",
      url: `${this.apiBaseUrl}/system/status`,
      destinationAddress: this.destinationAddress
    });
    return {
      overall: response.system.status,
      ingredients: response.ingredients,
      dispenser: response.dispenser,
      lastChecked: new Date()
    };
  }
  async searchBeverages(query: string): Promise<BeverageInfo[]> {
    const response = await kosFetch({
      method: "GET",
      url: `${this.apiBaseUrl}/beverages/search?q=${encodeURIComponent(query)}&limit=20`,
      destinationAddress: this.destinationAddress
    });
    return response.results || [];
  }
}

The kosFetch utility handles destination routing, request/response serialization, and error handling, making it straightforward to consume HTTP APIs from within the KOS environment.

Error Handling and API Robustness

Robust HTTP APIs require comprehensive error handling that provides meaningful feedback to clients while maintaining system stability. KOS HTTP integration supports structured error handling patterns that align with REST API best practices.

@kosModel({ modelTypeId: "robust-device-api", singleton: false })
export class RobustDeviceApiImpl implements IKosDataModel {
  @get("/api/devices/:id/calibrate")
  async calibrateDevice(req: KosRequest<"/api/devices/:id/calibrate">, res: KosResponse): Promise<void> {
    try {
      // Validate device exists
      const device = await this.getDeviceById(req.params.id);
      if (!device) {
        return this.sendNotFoundError(res, "Device", req.params.id);
      }
      // Check device state
      if (device.status === "offline") {
        return this.sendConflictError(res, "Cannot calibrate offline device");
      }
      if (device.isCalibrating) {
        return this.sendConflictError(res, "Device is already calibrating");
      }
      // Validate calibration parameters
      const validationResult = this.validateCalibrationParams(req.query);
      if (!validationResult.isValid) {
        return this.sendValidationError(res, validationResult.errors);
      }
      // Start calibration
      const calibrationFuture = await device.startCalibration(req.query);
      res.status?.(202).send({
        message: "Calibration started",
        deviceId: device.id,
        operationId: calibrationFuture.id,
        estimatedDuration: calibrationFuture.estimatedDuration,
        statusUrl: `/api/operations/${calibrationFuture.id}/status`
      });
    } catch (error) {
      this.handleUnexpectedError(res, error, "Device calibration failed");
    }
  }
  @post("/api/devices/:id/maintenance")
  async scheduleMaintenance(req: KosRequest<"/api/devices/:id/maintenance">, res: KosResponse): Promise<void> {
    try {
      const device = await this.getDeviceById(req.params.id);
      if (!device) {
        return this.sendNotFoundError(res, "Device", req.params.id);
      }
      // Validate maintenance window
      const { scheduledTime, maintenanceType, estimatedDuration } = req.body;
      if (!scheduledTime || !maintenanceType) {
        return this.sendValidationError(res, {
          scheduledTime: scheduledTime ? null : "Scheduled time is required",
          maintenanceType: maintenanceType ? null : "Maintenance type is required"
        });
      }
      const scheduledDate = new Date(scheduledTime);
      if (scheduledDate <= new Date()) {
        return this.sendValidationError(res, {
          scheduledTime: "Scheduled time must be in the future"
        });
      }
      // Check for conflicting maintenance
      const existingMaintenance = await this.getMaintenanceForDevice(device.id, scheduledDate);
      if (existingMaintenance) {
        return this.sendConflictError(res, "Device already has maintenance scheduled for this time");
      }
      // Schedule maintenance
      const maintenanceRecord = await this.scheduleDeviceMaintenance({
        deviceId: device.id,
        scheduledTime: scheduledDate,
        maintenanceType,
        estimatedDuration: estimatedDuration || 60,
        requestedBy: req.headers?.['user-id'] || 'system'
      });
      res.status?.(201).send({
        message: "Maintenance scheduled",
        maintenanceId: maintenanceRecord.id,
        deviceId: device.id,
        scheduledTime: maintenanceRecord.scheduledTime,
        statusUrl: `/api/maintenance/${maintenanceRecord.id}/status`
      });
    } catch (error) {
      this.handleUnexpectedError(res, error, "Failed to schedule maintenance");
    }
  }
  // Structured error handling methods
  private sendNotFoundError(res: KosResponse, entityType: string, id: string): void {
    res.status?.(404).send({
      error: "Not Found",
      message: `${entityType} with ID '${id}' not found`,
      code: "ENTITY_NOT_FOUND",
      timestamp: new Date().toISOString()
    });
  }
  private sendValidationError(res: KosResponse, errors: Record<string, string | null>): void {
    const validationErrors = Object.entries(errors)
      .filter(([, message]) => message !== null)
      .map(([field, message]) => ({ field, message }));
    res.status?.(400).send({
      error: "Validation Error",
      message: "Request validation failed",
      code: "VALIDATION_FAILED",
      validationErrors,
      timestamp: new Date().toISOString()
    });
  }
  private sendConflictError(res: KosResponse, message: string): void {
    res.status?.(409).send({
      error: "Conflict",
      message,
      code: "RESOURCE_CONFLICT",
      timestamp: new Date().toISOString()
    });
  }
  private handleUnexpectedError(res: KosResponse, error: any, context: string): void {
    this.logger.error(context, error);
    res.status?.(500).send({
      error: "Internal Server Error",
      message: "An unexpected error occurred",
      code: "INTERNAL_ERROR",
      requestId: this.generateRequestId(),
      timestamp: new Date().toISOString()
    });
  }
  private validateCalibrationParams(params: any): ValidationResult {
    const errors: Record<string, string | null> = {};
    if (params.calibrationType && !["full", "partial", "verification"].includes(params.calibrationType)) {
      errors.calibrationType = "Must be one of: full, partial, verification";
    }
    if (params.precision && (isNaN(params.precision) || params.precision < 0.1 || params.precision > 10)) {
      errors.precision = "Precision must be a number between 0.1 and 10";
    }
    return {
      isValid: Object.values(errors).every(e => e === null),
      errors
    };
  }
  private generateRequestId(): string {
    return `req-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
  }
}

This structured approach to error handling ensures that API clients receive meaningful, actionable feedback while protecting internal system details and maintaining consistent error response formats.

Building Scalable Web-Accessible Device Systems

KOS HTTP integration bridges the gap between device-centric architectures and web-accessible services. By providing declarative routing, automatic lifecycle management, and seamless integration with KOS models and services, it enables building sophisticated web APIs that expose device functionality through standard HTTP interfaces.

The combination of type-safe routing, PropKey-based dynamic paths, and comprehensive error handling creates a foundation for building scalable, maintainable web APIs that can evolve with changing business requirements while maintaining the reliability and real-time responsiveness that characterizes KOS applications.

Whether creating simple device status APIs, complex multi-tenant beverage dispensing services, or sophisticated monitoring and control interfaces, KOS HTTP integration provides the architectural patterns and developer experience necessary for building professional web-accessible device management systems.


Ready to expand your KOS expertise? Continue with Internationalization and Localization to learn about building multi-language device interfaces, or explore Advanced Testing Patterns for comprehensive KOS application testing strategies.