Single-crate Rust/Axum REST API for a Web3 agricultural investment platform.
No workspace, no monorepo — one Cargo.toml, one binary target.
# Start database dependencies (required before cargo run/test)
docker-compose up -d postgres redis
# Copy env and configure (DATABASE_URL + JWT_SECRET are required)
cp .env.example .env
# Run migrations
cargo install sqlx-cli
sqlx migrate run
# Build & run
cargo run
# CI gate (run all four before committing)
cargo fmt -- --check
cargo clippy -- -D warnings
cargo check
cargo testTests require live Postgres and Redis (no mocks). Integration test dirs exist but are empty.
dotenvy loads .env automatically. Two vars are hard-required: DATABASE_URL, JWT_SECRET.
All others have defaults or are optional (see .env.example).
main.rs → AppConfig::from_env() → AppState::new() → build_router() → axum::serve
- routes/ — HTTP handlers, each module has a
pub fn routes() -> Router<AppState> - services/ — business logic, takes
&PgPoolor&AppState, returnsResult<T, ApiError> - models/ — DB row structs (
FromRow+Serialize+Deserialize) - middleware/auth.rs —
AuthUserextractor (JWT Bearer token, HS256) - error.rs —
ApiErrorenum, converts to JSON{ "error": { "code", "message" } } - blockchain/ — Soroban RPC client, event polling (indexer spawns background tokio tasks)
- db/redis.rs — Redis caching helpers
- utils/crypto.rs — Ed25519 signature verification
- utils/pagination.rs — shared pagination query parsing
All routes are nested under /api/v1.
Queries use runtime sqlx::query_as (not compile-time checked macros).
No .sqlx/ offline cache directory exists. CI sets SQLX_OFFLINE=true to skip checking.
- Create migration in
migrations/NNN_description.sql(sequential numbering,IF NOT EXISTS) - Add model in
src/models/with#[derive(Debug, Serialize, Deserialize, FromRow)] - Add service in
src/services/— functions returnResult<T, ApiError> - Add route in
src/routes/— createpub fn routes() -> Router<AppState>and merge it inroutes/mod.rs - Use
ApiErrorvariants for error responses, not rawStatusCode
routes/mod.rs currently hardcodes allow_origin(Any) — the CORS_ORIGINS env var
in config.rs is parsed but unused in the router. Don't assume it takes effect.