8.8 KiB
+++ title = "Refactor to single-crate with api and ui binaries" priority = 8 status = "done" ticket_type = "task" dependencies = ["ec118c"] +++
Goal
Collapse the three separate sub-crates (api/, ui/, tests/) into a single Cargo crate rooted at quotesdb/. This simplifies the project structure, enables direct code sharing between the api and ui via src/lib.rs, and makes cargo test run all tests (unit + integration) in a single invocation.
Current State
quotesdb/
├── api/ # independent crate "quotesdb-api"
│ ├── Cargo.toml
│ ├── src/main.rs
│ ├── src/tests.rs
│ ├── docs/
│ └── README.md
├── ui/ # independent crate "quotesdb-ui"
│ ├── Cargo.toml
│ ├── src/main.rs
│ ├── src/tests.rs
│ ├── index.html
│ ├── Trunk.toml
│ ├── docs/
│ └── README.md
├── tests/ # independent crate "quotesdb-tests"
│ ├── Cargo.toml
│ ├── docs/
│ └── README.md
├── infra/
└── docs/
Problems with current structure:
- Shared types/logic must go through
../../common— no quotesdb-specific shared code. - Running tests requires
cding into each sub-crate separately. - Three
Cargo.tomlfiles to maintain, threecargo fmt/check/clippyinvocations. - Trunk must be run from
ui/, not the project root.
Target State
quotesdb/
├── Cargo.toml # single crate "quotesdb", default-run = "api"
├── src/
│ ├── lib.rs # shared code (types, models, auth logic, etc.)
│ └── bin/
│ ├── api/
│ │ └── main.rs # api binary entrypoint
│ └── ui/
│ └── main.rs # ui binary entrypoint (for Trunk)
├── tests/ # integration tests — run by `cargo test`
│ └── (*.rs files)
├── index.html # Trunk HTML entry (moved from ui/)
├── Trunk.toml # updated to point to ui binary
├── infra/
└── docs/
├── PLANNING.md
├── ARCHITECTURE.md
└── plans/
└── 2026-02-27-quotesdb-design.md
Developer workflow after refactor (unchanged from user perspective):
cargo run— starts the API server (default binary isapi)trunk serve— compiles ui to Wasm and serves itcargo test— runs unit tests + integration tests
Changes Required
1. Create quotesdb/Cargo.toml
Single crate manifest with:
name = "quotesdb"default-run = "api"— ensurescargo runlaunches the apiedition = "2021",license = "MIT OR Apache-2.0"[profile.release]block (opt-level z, lto, strip, codegen-units 1)- All api dependencies (axum, tokio, workers-rs, sqlx, nanoid, etc.)
- All ui dependencies (yew, wasm-bindgen, web-sys, etc.)
- All test dependencies (reqwest, tokio, etc.) under
[dev-dependencies] - Two
[[bin]]entries:[[bin]] name = "api" path = "src/bin/api/main.rs" [[bin]] name = "ui" path = "src/bin/ui/main.rs"
2. Create src/lib.rs
Shared module for code used by both binaries:
- Domain types:
Quote,QuoteTag, pagination structs, request/response shapes - Auth code generation (4-word passphrase) — shared so ui can display it and api generates it
- NanoID generation utility
- Any other shared logic
3. Move api source
api/src/main.rs→src/bin/api/main.rsapi/src/tests.rs→src/bin/api/tests.rs(or inline unit tests within the binary module)- Delete
api/Cargo.toml
4. Move ui source
ui/src/main.rs→src/bin/ui/main.rsui/src/tests.rs→src/bin/ui/tests.rs- Delete
ui/Cargo.toml
5. Move Trunk files
ui/index.html→quotesdb/index.htmlui/Trunk.toml→quotesdb/Trunk.toml
Update Trunk.toml to explicitly name the ui binary so Trunk compiles the right entrypoint:
[build]
target = "index.html"
[build.cargo]
args = ["--bin", "ui"]
6. Move integration tests
- Content from
tests/sub-crate moves intoquotesdb/tests/as.rsfiles (standard Cargo integration test layout). - Delete
tests/Cargo.toml. - Integration tests import from the crate root (
use quotesdb::...) and from dev-dependencies. - They run with
cargo testautomatically — no separate crate needed.
7. Consolidate docs
Merge per-sub-crate docs into the project-level docs/ directory:
api/docs/PLANNING.mdandui/docs/PLANNING.md→ merge intodocs/PLANNING.mdapi/docs/ARCHITECTURE.mdandui/docs/ARCHITECTURE.md→ merge intodocs/ARCHITECTURE.mdapi/README.mdandui/README.mdandtests/README.md→ consolidate into the project-levelquotesdb/README.md- Delete the now-empty
api/docs/,ui/docs/,tests/docs/directories.
8. Update CLAUDE.md
Update quotesdb/CLAUDE.md to reflect:
- New directory structure (single crate, not three sub-crates)
- New validation commands run from
quotesdb/root, not from sub-directories - Updated branch naming and ticket hierarchy (the sub-project split is now logical, not a file-system split)
- Updated agent dispatch instructions (agents work in
src/bin/api/orsrc/bin/ui/, not separate crates)
9. Delete orphaned sub-crate roots
After moving all contents:
- Delete
api/directory entirely - Delete
ui/directory entirely - Delete
tests/old sub-crate directory (butquotesdb/tests/integration test files stay)
Considerations & Complications
Compilation targets
The api binary compiles for the host target during local dev (cargo run). The ui binary compiles for wasm32-unknown-unknown via Trunk. These are separate compilation invocations — they don't conflict in a single Cargo crate.
However, shared code in src/lib.rs must compile for both targets. Avoid host-only APIs (threading, filesystem) in lib.rs. Use #[cfg(target_arch = "wasm32")] and #[cfg(not(target_arch = "wasm32"))] guards where needed.
cargo test and Wasm
cargo test runs on the host target. The ui binary's tests cannot use DOM/browser APIs directly. Yew component tests that require a browser context must use wasm-bindgen-test and wasm-pack test — these cannot be run by cargo test. Therefore:
- Unit tests in
src/bin/ui/must be limited to pure logic (routing, data transformations, API client request construction) guarded with#[cfg(test)]. - Browser-only tests (component rendering) are out of scope for
cargo testand remain a future concern. - Integration tests in
tests/exercise the api only and run on the host — these work fine withcargo test.
workers-rs and local dev
The api uses workers-rs for Cloudflare Workers deployment. For local development cargo run, the api should either:
- Use a plain Axum server (conditional compilation:
#[cfg(not(target_env = "worker"))]), OR - Use the workers-rs local dev entrypoint.
The existing api/src/main.rs stub is empty — the implementation tickets will determine the approach. This ticket should preserve the stub structure and make no assumptions about the final api implementation.
Dependency conflicts
Some dependencies may not compile for all targets. Use [target.'cfg(not(target_arch = "wasm32"))'.dependencies] for api-only deps and [target.'cfg(target_arch = "wasm32")'.dependencies] for ui-only deps in Cargo.toml where needed. This keeps compile times reasonable and avoids linker conflicts.
Validation
From quotesdb/ root:
cargo fmt # must pass cleanly
cargo check # must pass for host target
cargo clippy # must pass with no warnings
cargo test # must run and pass all tests (unit + integration)
trunk build # must successfully compile the ui binary to wasm
The conventional commit for this work: refactor(quotesdb): collapse to single crate with api and ui binaries
Files to Create / Move / Delete (Summary)
| Action | Path |
|---|---|
| CREATE | quotesdb/Cargo.toml |
| CREATE | quotesdb/src/lib.rs |
| MOVE | api/src/main.rs → src/bin/api/main.rs |
| MOVE | api/src/tests.rs → src/bin/api/tests.rs |
| MOVE | ui/src/main.rs → src/bin/ui/main.rs |
| MOVE | ui/src/tests.rs → src/bin/ui/tests.rs |
| MOVE | ui/index.html → index.html |
| MOVE | ui/Trunk.toml → Trunk.toml (update --bin ui) |
| MERGE | api/docs/ + ui/docs/ + tests/docs/ → docs/ |
| MERGE | api/README.md, ui/README.md, tests/README.md → README.md |
| DELETE | api/Cargo.toml |
| DELETE | ui/Cargo.toml |
| DELETE | tests/Cargo.toml |
| DELETE | api/ (after moving contents) |
| DELETE | ui/ (after moving contents) |
| UPDATE | Trunk.toml (add [build.cargo] args = ["--bin", "ui"]) |
| UPDATE | quotesdb/CLAUDE.md (structure, validation paths, agent instructions) |