Skip to main content

OpenAPI Generation

API documentation is generated using utoipa for OpenAPI specs and specta for TypeScript types.

Key Files

  • crates/bf_resident/src/openapi.rs - OpenAPI spec definition
  • crates/bf_types/ - Types with utoipa derives

Architecture

┌─────────────────────────────────────────────────────────────────────────┐
│ Rust Types │
│ │
│ #[derive(Serialize, Deserialize, ToSchema)] │
│ pub struct Property { ... } │
│ │
└─────────────────────────────────────────────────────────────────────────┘

┌───────────────┴───────────────┐
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ utoipa │ │ specta │
│ │ │ │
│ OpenAPI 3.0 Spec │ │ TypeScript Types │
│ (JSON/YAML) │ │ │
└─────────────────────┘ └─────────────────────┘
│ │
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ /api/openapi.json │ │ types.d.ts │
│ /api/openapi.yaml │ │ │
└─────────────────────┘ └─────────────────────┘

OpenAPI Spec Definition

// crates/bf_resident/src/openapi.rs

use utoipa::OpenApi;

#[derive(OpenApi)]
#[openapi(
info(
title = "Broadford Living API",
version = "1.0.0",
description = "Property management and smart home API"
),
servers(
(url = "/api", description = "API base path")
),
paths(
// Health
handlers::health_check,

// Properties
handlers::properties::create_property,
handlers::properties::list_properties,
handlers::properties::get_property,
handlers::properties::update_property,
handlers::properties::delete_property,
handlers::properties::commission_property,
handlers::properties::prepare_tenancy,
handlers::properties::schedule_move_in,
handlers::properties::activate_tenancy,
handlers::properties::schedule_move_out,
handlers::properties::complete_move_out,

// Devices
handlers::devices::create_device,
handlers::devices::list_devices,
handlers::devices::get_device,
handlers::devices::update_device,
handlers::devices::delete_device,

// Portfolios
handlers::portfolios::create_portfolio,
handlers::portfolios::list_portfolios,
handlers::portfolios::get_portfolio,
handlers::portfolios::update_portfolio,
handlers::portfolios::add_pm_to_portfolio,
handlers::portfolios::remove_pm_from_portfolio,

// IT Setup
handlers::it_setup::initialize_it_setup,
handlers::it_setup::confirm_smartthings_setup,
handlers::it_setup::validate_yale,
handlers::it_setup::validate_hive,
handlers::it_setup::complete_commissioning,

// ... more endpoints
),
components(
schemas(
// Property types
Property,
CreatePropertyRequest,
UpdatePropertyRequest,
PropertyStatus,
PropertyListResponse,

// Device types
Device,
CreateDeviceRequest,
DeviceType,
DevicePlatform,
DeviceStatus,

// Portfolio types
Portfolio,
CreatePortfolioRequest,

// Error types
ErrorResponse,

// ... more schemas
)
),
tags(
(name = "properties", description = "Property management"),
(name = "devices", description = "Device operations"),
(name = "portfolios", description = "Portfolio management"),
(name = "it-setup", description = "IT commissioning"),
(name = "admin", description = "Admin operations"),
),
modifiers(&SecurityAddon)
)]
pub struct ApiDoc;

struct SecurityAddon;

impl utoipa::Modify for SecurityAddon {
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
let components = openapi.components.as_mut().unwrap();
components.add_security_scheme(
"bearer_auth",
utoipa::openapi::security::SecurityScheme::Http(
utoipa::openapi::security::Http::new(
utoipa::openapi::security::HttpAuthScheme::Bearer
)
),
);
}
}

Multiple API Specs

Different specs for different roles:

// Admin/PM API (full access)
#[derive(OpenApi)]
#[openapi(
info(title = "Broadford Admin API", ...),
paths(
// All admin and PM endpoints
),
...
)]
pub struct AdminPmApiDoc;

// Installer API (limited access)
#[derive(OpenApi)]
#[openapi(
info(title = "Broadford Installer API", ...),
paths(
// Only installer-relevant endpoints
handlers::installer::list_assigned_properties,
handlers::installer::get_property_devices,
handlers::installer::report_error,
handlers::installer::resolve_error,
),
...
)]
pub struct InstallerApiDoc;

Generating Specs

// JSON format
pub fn generate_openapi_json() -> String {
ApiDoc::openapi().to_pretty_json().unwrap()
}

// YAML format
pub fn generate_openapi_yaml() -> String {
ApiDoc::openapi().to_yaml().unwrap()
}

// Role-specific specs
pub fn generate_admin_pm_openapi_json() -> String {
AdminPmApiDoc::openapi().to_pretty_json().unwrap()
}

pub fn generate_installer_openapi_json() -> String {
InstallerApiDoc::openapi().to_pretty_json().unwrap()
}

Endpoint Documentation

// crates/bf_resident/src/handlers/properties.rs

/// Create a new property
#[utoipa::path(
post,
path = "/admin/properties",
tag = "properties",
request_body = CreatePropertyRequest,
responses(
(status = 201, description = "Property created", body = Property),
(status = 400, description = "Invalid request", body = ErrorResponse),
(status = 401, description = "Unauthorized", body = ErrorResponse),
(status = 403, description = "Forbidden", body = ErrorResponse),
),
security(
("bearer_auth" = [])
)
)]
pub async fn create_property(
Extension(auth): Extension<ClerkAuth>,
State(env): State<Env>,
Json(req): Json<CreatePropertyRequest>,
) -> Result<Json<Property>> {
// Implementation
}

/// List properties with filtering
#[utoipa::path(
get,
path = "/admin/properties",
tag = "properties",
params(
("portfolio_id" = Option<String>, Query, description = "Filter by portfolio"),
("status" = Option<PropertyStatus>, Query, description = "Filter by status"),
("limit" = Option<i32>, Query, description = "Max results"),
("offset" = Option<i32>, Query, description = "Pagination offset"),
),
responses(
(status = 200, description = "List of properties", body = PropertyListResponse),
(status = 401, description = "Unauthorized", body = ErrorResponse),
),
security(
("bearer_auth" = [])
)
)]
pub async fn list_properties(
Extension(auth): Extension<ClerkAuth>,
State(env): State<Env>,
Query(filters): Query<PropertyFilters>,
) -> Result<Json<PropertyListResponse>> {
// Implementation
}

Schema Derives

// crates/bf_types/src/resident/property.rs

use serde::{Deserialize, Serialize};
use utoipa::ToSchema;

#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct Property {
/// Unique property identifier
#[schema(example = "prop_abc123")]
pub id: String,

/// Portfolio this property belongs to
#[schema(example = "port_xyz789")]
pub portfolio_id: String,

/// Street address
#[schema(example = "123 Main Street")]
pub address: String,

/// Unit/flat number
#[schema(example = "Flat 1")]
pub unit: String,

/// Current status
pub status: PropertyStatus,

/// Number of bedrooms
#[schema(example = 2)]
pub bedrooms: u32,

/// Creation timestamp
pub created_at: i64,
}

#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum PropertyStatus {
Setup,
Commissioning,
Commissioned,
PreTenancy,
ScheduledMovedIn,
Tenancy,
ScheduledMovedOut,
}

#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct CreatePropertyRequest {
/// Portfolio ID
#[schema(example = "port_xyz789")]
pub portfolio_id: String,

/// Street address
#[schema(example = "123 Main Street")]
pub address: String,

/// Unit number
#[schema(example = "Flat 1")]
pub unit: String,

/// Assigned property manager email
#[schema(example = "pm@example.com")]
pub assigned_pm: String,

/// Number of bedrooms
#[schema(example = 2, minimum = 0, maximum = 10)]
pub bedrooms: u32,

/// House type
#[schema(example = "apartment")]
pub house_type: String,

/// Optional notes
pub notes: Option<String>,
}

TypeScript Generation

Using specta for TypeScript type generation:

// Types with specta derive
#[derive(Serialize, Deserialize, ToSchema, specta::Type)]
pub struct Property {
// ...
}

// Generation script
fn main() {
let ts = specta::ts::export::<Property>(&Default::default()).unwrap();
std::fs::write("types/property.ts", ts).unwrap();
}

Generated TypeScript:

// types/property.ts

export interface Property {
id: string;
portfolio_id: string;
address: string;
unit: string;
status: PropertyStatus;
bedrooms: number;
created_at: number;
}

export type PropertyStatus =
| "setup"
| "commissioning"
| "commissioned"
| "pre_tenancy"
| "scheduled_moved_in"
| "tenancy"
| "scheduled_moved_out";

export interface CreatePropertyRequest {
portfolio_id: string;
address: string;
unit: string;
assigned_pm: string;
bedrooms: number;
house_type: string;
notes?: string;
}

API Endpoints

// Serve OpenAPI specs

Router::new()
.route("/api/openapi.json", get(|| async {
Response::builder()
.header("Content-Type", "application/json")
.body(generate_openapi_json())
}))
.route("/api/openapi.yaml", get(|| async {
Response::builder()
.header("Content-Type", "text/yaml")
.body(generate_openapi_yaml())
}))

See Also