feat(api): generate TypeScript API client from OpenAPI specification

Create type-safe TypeScript API client automatically generated from the
OpenAPI specification. Includes generated schema types and documented client
wrapper for type-safe backend communication.

Ref: T008 (specs/001-modbus-relay-control)
This commit is contained in:
2026-01-01 23:29:31 +01:00
parent 614c82a6dc
commit 84ee4ef228
6 changed files with 498 additions and 38 deletions

View File

@@ -1,5 +1,7 @@
# STA - Smart Temperature & Appliance Control
> **🤖 AI-Assisted Development Notice**: This project uses Claude Code as a development assistant for task planning, code organization, and workflow management. However, all code is human-written, reviewed, and validated by the project maintainer. AI is used as a productivity tool, not as the author of the implementation.
Web-based Modbus relay control system for managing 8-channel relay modules over TCP.
> **⚠️ Development Status**: This project is in early development. Core features are currently being implemented following a specification-driven approach.
@@ -10,26 +12,33 @@ STA will provide a modern web interface for controlling Modbus-compatible relay
## Current Status
**Implemented:**
-Basic Rust web server with Poem framework
**Phase 1 Complete - Foundation:**
-Monorepo structure (backend + frontend at root)
- ✅ Rust web server with Poem 3.1 framework
- ✅ Configuration system (YAML + environment variables)
- ✅ Modbus TCP settings structure
- ✅ Modbus TCP and relay settings structures
- ✅ Health check and metadata API endpoints
- ✅ OpenAPI documentation with Swagger UI
- ✅ Rate limiting middleware
- ✅ SQLite schema and repository for relay labels
- ✅ Vue 3 + TypeScript frontend scaffolding with Vite
- ✅ Type-safe API client generation from OpenAPI specs
**In Progress:**
- 🚧 Domain model for relay control (Phase 2)
- 🚧 Modbus TCP client implementation (Phase 3)
**Phase 2 In Progress - Domain Layer:**
- 🚧 Domain types with Type-Driven Development (RelayId, RelayState, RelayLabel)
- 🚧 100% test coverage for domain layer
**Planned Features:**
- 📋 8-Channel Relay Control: Individual and bulk relay control (on/off/toggle)
- 📋 Real-Time Monitoring: Live relay state updates via HTTP polling
- 📋 Custom Labels: Name your relays for easy identification
- 📋 Health Monitoring: Connection status and device health tracking
- 📋 Vue 3 + TypeScript frontend
**Planned - Phases 3-8:**
- 📋 Modbus TCP client with tokio-modbus (Phase 3)
- 📋 Mock controller for testing (Phase 3)
- 📋 Health monitoring service (Phase 3)
- 📋 US1: Monitor & toggle relay states - MVP (Phase 4)
- 📋 US2: Bulk relay controls (Phase 5)
- 📋 US3: Health status display (Phase 6)
- 📋 US4: Relay labeling (Phase 7)
- 📋 Production deployment (Phase 8)
See [tasks.md](specs/001-modbus-relay-control/tasks.md) for detailed implementation roadmap.
See [tasks.md](specs/001-modbus-relay-control/tasks.md) for detailed implementation roadmap (94 tasks across 8 phases).
## Architecture
@@ -111,31 +120,41 @@ The server provides OpenAPI documentation via Swagger UI:
## Project Structure
**Current:**
**Monorepo Layout:**
```
src/
├── lib.rs - Library entry point
├── main.rs - Binary entry point
├── startup.rs - Application builder and server configuration
├── settings.rs - Configuration management
├── telemetry.rs - Logging and tracing setup
├── route/ - HTTP endpoint handlers
│ ├── health.rs - Health check endpoints
└── meta.rs - Application metadata
└── middleware/ - Custom middleware
└── rate_limit.rs
specs/ - Feature specifications and documentation
settings/ - YAML configuration files
```
**Planned (Hexagonal Architecture):**
```
src/
├── domain/ - Business logic and domain models (Phase 2)
├── application/ - Use cases and orchestration (Phase 3-4)
├── infrastructure/ - Modbus, persistence, external services (Phase 3)
└── presentation/ - API endpoints and DTOs (Phase 4)
sta/ # Repository root
├── backend/ # Rust backend workspace member
│ ├── src/
│ │ ├── lib.rs - Library entry point
│ │ ├── main.rs - Binary entry point
│ │ ├── startup.rs - Application builder and server config
├── settings.rs - Configuration management
│ ├── telemetry.rs - Logging and tracing setup
│ ├── domain/ - Business logic (Phase 2 in progress)
│ │ │ └── relay/ - Relay domain types and repository traits
│ ├── application/ - Use cases (planned Phase 3-4)
│ │ ├── infrastructure/ - External integrations (Phase 3)
└── persistence/ - SQLite repository implementation
│ │ ├── presentation/ - API layer (planned Phase 4)
│ │ ├── route/ - HTTP endpoint handlers
│ │ │ ├── health.rs - Health check endpoints
│ │ │ └── meta.rs - Application metadata
│ │ └── middleware/ - Custom middleware
│ │ └── rate_limit.rs
│ ├── settings/ - YAML configuration files
│ └── tests/ - Integration tests
├── src/ # Frontend source (Vue/TypeScript)
│ └── api/ - Type-safe API client
├── specs/ # Feature specifications and documentation
│ ├── constitution.md - Architectural principles
│ └── 001-modbus-relay-control/
│ ├── spec.md - Feature specification
│ ├── plan.md - Implementation plan
│ ├── tasks.md - Task breakdown (94 tasks)
│ └── research-cors.md - CORS configuration research
├── package.json - Frontend dependencies
├── vite.config.ts - Vite build configuration
└── justfile - Build commands
```
## Technology Stack

View File

@@ -6,15 +6,18 @@
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
"preview": "vite preview",
"generate:api": "curl -s http://localhost:3100/specs > openapi.yaml && openapi-typescript openapi.yaml -o src/api/schema.ts && rm openapi.yaml"
},
"dependencies": {
"openapi-fetch": "^0.15.0",
"vue": "^3.5.24"
},
"devDependencies": {
"@types/node": "^24.10.1",
"@vitejs/plugin-vue": "^6.0.1",
"@vue/tsconfig": "^0.8.1",
"openapi-typescript": "^7.10.1",
"typescript": "~5.9.3",
"vite": "^7.2.4",
"vue-tsc": "^3.1.4"

240
pnpm-lock.yaml generated
View File

@@ -8,6 +8,9 @@ importers:
.:
dependencies:
openapi-fetch:
specifier: ^0.15.0
version: 0.15.0
vue:
specifier: ^3.5.24
version: 3.5.26(typescript@5.9.3)
@@ -21,6 +24,9 @@ importers:
'@vue/tsconfig':
specifier: ^0.8.1
version: 0.8.1(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3))
openapi-typescript:
specifier: ^7.10.1
version: 7.10.1(typescript@5.9.3)
typescript:
specifier: ~5.9.3
version: 5.9.3
@@ -33,6 +39,10 @@ importers:
packages:
'@babel/code-frame@7.27.1':
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
engines: {node: '>=6.9.0'}
'@babel/helper-string-parser@7.27.1':
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
engines: {node: '>=6.9.0'}
@@ -209,6 +219,16 @@ packages:
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
'@redocly/ajv@8.17.1':
resolution: {integrity: sha512-EDtsGZS964mf9zAUXAl9Ew16eYbeyAFWhsPr0fX6oaJxgd8rApYlPBf0joyhnUHz88WxrigyFtTaqqzXNzPgqw==}
'@redocly/config@0.22.2':
resolution: {integrity: sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==}
'@redocly/openapi-core@1.34.6':
resolution: {integrity: sha512-2+O+riuIUgVSuLl3Lyh5AplWZyVMNuG2F98/o6NrutKJfW4/GTZdPpZlIphS0HGgcOHgmWcCSHj+dWFlZaGSHw==}
engines: {node: '>=18.17.0', npm: '>=9.5.0'}
'@rolldown/pluginutils@1.0.0-beta.53':
resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==}
@@ -387,12 +407,44 @@ packages:
vue:
optional: true
agent-base@7.1.4:
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
engines: {node: '>= 14'}
alien-signals@3.1.2:
resolution: {integrity: sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==}
ansi-colors@4.1.3:
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
engines: {node: '>=6'}
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
brace-expansion@2.0.2:
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
change-case@5.4.4:
resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==}
colorette@1.4.0:
resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==}
csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
entities@7.0.0:
resolution: {integrity: sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==}
engines: {node: '>=0.12'}
@@ -405,6 +457,12 @@ packages:
estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
fast-uri@3.1.0:
resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==}
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
@@ -419,9 +477,38 @@ packages:
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
https-proxy-agent@7.0.6:
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
engines: {node: '>= 14'}
index-to-position@1.2.0:
resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==}
engines: {node: '>=18'}
js-levenshtein@1.1.6:
resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==}
engines: {node: '>=0.10.0'}
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
js-yaml@4.1.1:
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
hasBin: true
json-schema-traverse@1.0.0:
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
minimatch@5.1.6:
resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==}
engines: {node: '>=10'}
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
muggle-string@0.4.1:
resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
@@ -430,6 +517,22 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
openapi-fetch@0.15.0:
resolution: {integrity: sha512-OjQUdi61WO4HYhr9+byCPMj0+bgste/LtSBEcV6FzDdONTs7x0fWn8/ndoYwzqCsKWIxEZwo4FN/TG1c1rI8IQ==}
openapi-typescript-helpers@0.0.15:
resolution: {integrity: sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==}
openapi-typescript@7.10.1:
resolution: {integrity: sha512-rBcU8bjKGGZQT4K2ekSTY2Q5veOQbVG/lTKZ49DeCyT9z62hM2Vj/LLHjDHC9W7LJG8YMHcdXpRZDqC1ojB/lw==}
hasBin: true
peerDependencies:
typescript: ^5.x
parse-json@8.3.0:
resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==}
engines: {node: '>=18'}
path-browserify@1.0.1:
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
@@ -440,10 +543,18 @@ packages:
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
engines: {node: '>=12'}
pluralize@8.0.0:
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
engines: {node: '>=4'}
postcss@8.5.6:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
require-from-string@2.0.2:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
rollup@4.54.0:
resolution: {integrity: sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
@@ -453,10 +564,18 @@ packages:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
supports-color@10.2.2:
resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==}
engines: {node: '>=18'}
tinyglobby@0.2.15:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'}
type-fest@4.41.0:
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
engines: {node: '>=16'}
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
@@ -522,8 +641,21 @@ packages:
typescript:
optional: true
yaml-ast-parser@0.0.43:
resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==}
yargs-parser@21.1.1:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'}
snapshots:
'@babel/code-frame@7.27.1':
dependencies:
'@babel/helper-validator-identifier': 7.28.5
js-tokens: 4.0.0
picocolors: 1.1.1
'@babel/helper-string-parser@7.27.1': {}
'@babel/helper-validator-identifier@7.28.5': {}
@@ -617,6 +749,29 @@ snapshots:
'@jridgewell/sourcemap-codec@1.5.5': {}
'@redocly/ajv@8.17.1':
dependencies:
fast-deep-equal: 3.1.3
fast-uri: 3.1.0
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
'@redocly/config@0.22.2': {}
'@redocly/openapi-core@1.34.6(supports-color@10.2.2)':
dependencies:
'@redocly/ajv': 8.17.1
'@redocly/config': 0.22.2
colorette: 1.4.0
https-proxy-agent: 7.0.6(supports-color@10.2.2)
js-levenshtein: 1.1.6
js-yaml: 4.1.1
minimatch: 5.1.6
pluralize: 8.0.0
yaml-ast-parser: 0.0.43
transitivePeerDependencies:
- supports-color
'@rolldown/pluginutils@1.0.0-beta.53': {}
'@rollup/rollup-android-arm-eabi@4.54.0':
@@ -778,10 +933,32 @@ snapshots:
typescript: 5.9.3
vue: 3.5.26(typescript@5.9.3)
agent-base@7.1.4: {}
alien-signals@3.1.2: {}
ansi-colors@4.1.3: {}
argparse@2.0.1: {}
balanced-match@1.0.2: {}
brace-expansion@2.0.2:
dependencies:
balanced-match: 1.0.2
change-case@5.4.4: {}
colorette@1.4.0: {}
csstype@3.2.3: {}
debug@4.4.3(supports-color@10.2.2):
dependencies:
ms: 2.1.3
optionalDependencies:
supports-color: 10.2.2
entities@7.0.0: {}
esbuild@0.27.2:
@@ -815,6 +992,10 @@ snapshots:
estree-walker@2.0.2: {}
fast-deep-equal@3.1.3: {}
fast-uri@3.1.0: {}
fdir@6.5.0(picomatch@4.0.3):
optionalDependencies:
picomatch: 4.0.3
@@ -822,26 +1003,77 @@ snapshots:
fsevents@2.3.3:
optional: true
https-proxy-agent@7.0.6(supports-color@10.2.2):
dependencies:
agent-base: 7.1.4
debug: 4.4.3(supports-color@10.2.2)
transitivePeerDependencies:
- supports-color
index-to-position@1.2.0: {}
js-levenshtein@1.1.6: {}
js-tokens@4.0.0: {}
js-yaml@4.1.1:
dependencies:
argparse: 2.0.1
json-schema-traverse@1.0.0: {}
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
minimatch@5.1.6:
dependencies:
brace-expansion: 2.0.2
ms@2.1.3: {}
muggle-string@0.4.1: {}
nanoid@3.3.11: {}
openapi-fetch@0.15.0:
dependencies:
openapi-typescript-helpers: 0.0.15
openapi-typescript-helpers@0.0.15: {}
openapi-typescript@7.10.1(typescript@5.9.3):
dependencies:
'@redocly/openapi-core': 1.34.6(supports-color@10.2.2)
ansi-colors: 4.1.3
change-case: 5.4.4
parse-json: 8.3.0
supports-color: 10.2.2
typescript: 5.9.3
yargs-parser: 21.1.1
parse-json@8.3.0:
dependencies:
'@babel/code-frame': 7.27.1
index-to-position: 1.2.0
type-fest: 4.41.0
path-browserify@1.0.1: {}
picocolors@1.1.1: {}
picomatch@4.0.3: {}
pluralize@8.0.0: {}
postcss@8.5.6:
dependencies:
nanoid: 3.3.11
picocolors: 1.1.1
source-map-js: 1.2.1
require-from-string@2.0.2: {}
rollup@4.54.0:
dependencies:
'@types/estree': 1.0.8
@@ -872,11 +1104,15 @@ snapshots:
source-map-js@1.2.1: {}
supports-color@10.2.2: {}
tinyglobby@0.2.15:
dependencies:
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
type-fest@4.41.0: {}
typescript@5.9.3: {}
undici-types@7.16.0: {}
@@ -910,3 +1146,7 @@ snapshots:
'@vue/shared': 3.5.26
optionalDependencies:
typescript: 5.9.3
yaml-ast-parser@0.0.43: {}
yargs-parser@21.1.1: {}

61
src/api/README.md Normal file
View File

@@ -0,0 +1,61 @@
# API Client
This directory contains the auto-generated TypeScript API client for the STA backend.
## Files
- `schema.ts` - Auto-generated OpenAPI type definitions (do not edit manually)
- `client.ts` - API client instance with type-safe methods
## Regenerating the Client
To regenerate the TypeScript client after backend API changes:
1. Start the backend server:
```bash
cargo run
```
2. Download the OpenAPI spec:
```bash
curl http://localhost:3100/specs > openapi.yaml
```
3. Generate TypeScript types:
```bash
pnpm exec openapi-typescript openapi.yaml -o src/api/schema.ts
```
## Usage Example
```typescript
import { apiClient } from '@/api/client';
// GET request
const { data, error } = await apiClient.GET('/api/health');
if (error) {
console.error('Health check failed:', error);
} else {
console.log('Server is healthy');
}
// GET request with response data
const { data: meta, error: metaError } = await apiClient.GET('/api/meta');
if (metaError) {
console.error('Failed to get metadata:', metaError);
} else {
console.log('App name:', meta.name);
console.log('App version:', meta.version);
}
```
## Configuration
The API base URL can be configured via the `VITE_API_BASE_URL` environment variable.
Create a `.env` file in the project root:
```env
VITE_API_BASE_URL=http://localhost:3100
```
For production builds, set the environment variable to point to your deployed backend.

31
src/api/client.ts Normal file
View File

@@ -0,0 +1,31 @@
/**
* API client for the STA backend.
*
* This client is generated from the OpenAPI specification and provides
* type-safe access to all backend endpoints.
*
* Usage:
* ```typescript
* import { apiClient } from '@/api/client';
*
* const { data, error } = await apiClient.GET('/api/health');
* ```
*/
import createClient from 'openapi-fetch';
import type { paths } from './schema';
// Get the API base URL from environment variables or default to localhost
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3100';
/**
* Typed API client instance.
*
* All requests are type-checked against the OpenAPI schema.
*/
export const apiClient = createClient<paths>({ baseUrl: API_BASE_URL });
/**
* Re-export the types for convenience
*/
export type { paths, components } from './schema';

106
src/api/schema.ts Normal file
View File

@@ -0,0 +1,106 @@
/**
* This file was auto-generated by openapi-typescript.
* Do not make direct changes to the file.
*/
export interface paths {
"/api/health": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Success */
200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Too Many Requests - rate limit exceeded */
429: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/meta": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Success */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json; charset=utf-8": components["schemas"]["Meta"];
};
};
/** @description Too Many Requests - rate limit exceeded */
429: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
}
export type webhooks = Record<string, never>;
export interface components {
schemas: {
/** Meta */
Meta: {
version: string;
name: string;
};
};
responses: never;
parameters: never;
requestBodies: never;
headers: never;
pathItems: never;
}
export type $defs = Record<string, never>;
export type operations = Record<string, never>;