+++
title = "Add build.rs — convert api/openapi.yaml to JSON at compile time for Workers embed"
priority = 8
status = "done"
ticket_type = "task"
dependencies = []
+++
Resolved from TRIAGE ticket 2ec8b1. The `GET /api/` endpoint must serve the OpenAPI spec as JSON.
The three strategies were:
1. Compile-time embed (chosen)
2. Runtime load from filesystem — impossible on Cloudflare Workers (no filesystem)
3. utoipa programmatic generation — significant complexity; spec already exists and is complete
The chosen approach: a `build.rs` script reads `api/openapi.yaml`, parses it to a
`serde_json::Value`, serialises it as compact JSON, and writes the result to
`$OUT_DIR/openapi.json`. The `GET /api/` handler then serves this via:
```rust
const OPENAPI_JSON: &str = include_str!(concat!(env!("OUT_DIR"), "/openapi.json"));
```
This means:
- `serde_yaml` ships only as a `[build-dependencies]` entry — it never enters the Workers binary.
- The handler is a zero-overhead static string response with no runtime parsing.
- `cargo:rerun-if-changed=api/openapi.yaml` ensures the conversion re-runs whenever the spec
is edited — no manual JSON regeneration step needed.
- `api/openapi.yaml` remains the single source of truth; the JSON output is ephemeral (in
`$OUT_DIR`, not committed to the repository).
1. Create `build.rs` at the `quotesdb/` project root containing:
```rust
use std::{env, fs, path::Path};
fn main() {
// Re-run this script whenever the OpenAPI spec changes.
println!("cargo:rerun-if-changed=api/openapi.yaml");
let yaml =
fs::read_to_string("api/openapi.yaml").expect("api/openapi.yaml not found");
// Parse YAML to a generic JSON value, then re-serialise as compact JSON.
// serde_yaml is a build-only dependency — it does not appear in the final binary.
let value: serde_json::Value =
serde_yaml::from_str(&yaml).expect("api/openapi.yaml is invalid YAML");
let json = serde_json::to_string(&value).expect("JSON serialisation failed");
let out_dir = env::var("OUT_DIR").expect("OUT_DIR not set");
let out_path = Path::new(&out_dir).join("openapi.json");
fs::write(&out_path, json).expect("failed to write openapi.json");
}
```
2. Add the following to `Cargo.toml` (ticket 1f5bb5 should also include this):
```toml
[build-dependencies]
serde_json = "1"
serde_yaml = "0.9"
```
3. Verify the build succeeds and `$OUT_DIR/openapi.json` is produced:
```sh
cargo check
# $OUT_DIR is typically target/debug/build/quotesdb-*/out/openapi.json
```
- `serde_yaml` must be a `[build-dependencies]` entry only — NOT in `[dependencies]`.
Adding it to `[dependencies]` would bloat the Workers WASM binary.
- Do NOT commit `$OUT_DIR/openapi.json` — it is generated automatically at build time.
- The `build.rs` file lives at the crate root (same level as `Cargo.toml`), not in `src/`.
- `api/openapi.yaml` is the source of truth; do not create or commit an `api/openapi.json`.
Ticket 28e7d9 (GET /api/ handler) depends on this ticket. The handler uses
`include_str!(concat!(env!("OUT_DIR"), "/openapi.json"))` to serve the spec — see 28e7d9
for the Axum handler implementation.
```sh
cargo fmt
cargo check
cargo clippy
cargo test
```
`chore(quotesdb): add build.rs to convert api/openapi.yaml to JSON at compile time`
quotesdb/api