This document provides guidelines for agents working on the btcmap-api codebase.
BTC Map API is a Rust web service built with actix-web that provides a REST and RPC APIs for managing Bitcoin adoption data in meatspace. It uses SQLite for persistence.
cargo build --release # Production build
cargo build # Debug buildcargo run # Run the server (binds to 127.0.0.1:8000)cargo test # Run all tests
cargo test --verbose # Run all tests with verbose output
cargo test <test_name> # Run a single test by name
cargo test -- --nocapture # Run tests with stdout/stderr outputcargo clippy # Run linter (includes many warnings)
cargo clippy -- -D warnings # Treat warnings as errors
cargo fmt # Format code
cargo fmt --check # Check formatting without modifyingTo run a single test, use the test name filter:
cargo test get_empty_array
cargo test get_not_empty_arrayAlways run these before committing:
cargo fmt # Format code
cargo clippy -- -D warnings # Lint (must pass with zero warnings)
cargo test # Run testsThe project pins its Rust version in rust-toolchain.toml. Rustup handles this automatically — no manual version management needed. The toolchain file also ensures rustfmt and clippy components are installed.
src/
├── main.rs # Application entry point
├── error.rs # Main Error enum with From impls
├── db/ # Database layer (queries, blocking_queries, schema)
│ ├── mod.rs
│ ├── element/
│ ├── user/
│ ├── area/
│ └── ...
├── service/ # Business logic layer
│ ├── mod.rs
│ ├── element.rs
│ ├── user.rs
│ └── ...
├── rest/ # RWST HTTP handlers (v2, v3, v4 APIs)
│ ├── mod.rs
│ ├── error.rs # REST API error types
│ └── v4/
├── rpc/ # RPC handlers
├── og/ # OpenGraph image generation
└── feed/ # Atom/RSS feeds
- Modules: lowercase with underscores (e.g.,
element_comment) - Types/Structs: PascalCase (e.g.,
User,RestApiError) - Functions: snake_case (e.g.,
select_by_id,get_places) - Constants: SCREAMING_SNAKE_CASE for static values
- Database tables: singular, lowercase (e.g.,
user,element)
Order imports by category with blank lines between groups:
use crate::(internal modules)- External crate imports (actix-web, serde, etc.)
use std::imports
use crate::db;
use crate::db::element::schema::Element;
use crate::rest::error::RestApiError;
use crate::service;
use crate::Error;
use actix_web::get;
use actix_web::web::Data;
use deadpool_sqlite::Pool;
use serde::Deserialize;
use serde::Serialize;- Use the custom
Errorenum fromsrc/error.rs - Implement
Fromtraits for automatic error conversion - For REST endpoints, wrap errors with
RestApiError
// In service/handler code
.map_err(|_| RestApiError::database())?The codebase uses a two-layer pattern:
queries.rs- Async functions using deadpool SQLite poolblocking_queries.rs- Synchronous functions running on blocking threads
// queries.rs (async)
pub async fn select_by_id(id: i64, pool: &Pool) -> Result<User> {
pool.get()
.await?
.interact(move |conn| blocking_queries::select_by_id(id, conn))
.await?
}
// blocking_queries.rs (sync)
pub fn select_by_id(id: i64, conn: &mut Connection) -> Result<User> {
// ... rusqlite queries
}Full database schema is always available in schema.sql, use it as reference and don't try to make up non-existing tables and fields.
All SQL queries must go through the blocking_queries/queries layer. Never embed raw SQL in REST or RPC handlers. If you need a new query, add it to the appropriate blocking_queries.rs file and create an async wrapper in queries.rs. Each table has its own subfolder under src/db/main/ (e.g., element_event/, element_comment/, area_element/).
Handlers return RestResult<T> which is Result<Json<T>, RestApiError>:
#[get("")]
pub async fn get(args: Query<GetListArgs>, pool: Data<MainPool>) -> Res<Vec<JsonObject>> {
// ... implementation
Ok(Json(items))
}Tests are inline using #[cfg(test)] modules:
#[cfg(test)]
mod test {
use crate::db::test::pool;
use actix_web::test::{self, TestRequest};
use actix_web::web::scope;
use actix_web::App;
#[test]
async fn test_name() -> Result<()> {
let app = test::init_service(
App::new()
.app_data(Data::new(pool()))
.service(scope("/").service(super::my_handler)),
).await;
let req = TestRequest::get().uri("/").to_request();
let res: Vec<JsonObject> = test::call_and_read_body_json(&app, req).await;
assert!(res.is_empty());
Ok(())
}
}Use db::test::pool() for in-memory SQLite test databases.
- Use parameterized queries to prevent SQL injection
- Table names are singular (e.g.,
user, notusers) - Use migrations in
src/db/migration.rs
- Server binds to
127.0.0.1:8000(hardcoded in main.rs) - Database stored in data directory (configurable via
data_dir_file_path) - Logging controlled via
RUST_LOGenv var (defaults to "info") - Release builds use native CPU optimization (via .cargo/config.toml)
- actix-web: HTTP server
- rusqlite + deadpool-sqlite: SQLite database with async pool
- serde: JSON serialization
- reqwest: HTTP client
- time: Date/time handling
- tracing: Logging
- geo + geojson: Geographic data