ADR-004: API Architecture (v4) - Part 2: Technical Implementation
Status: Implemented
Date: 2025-08-27
Context: Technical blueprint for AI agents and developers
Implementation Details​
Core Dependencies​
[dependencies]
actix-web = "4.4"
actix-cors = "0.6"
tokio = { version = "1.35", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
jsonwebtoken = "9.2"
uuid = { version = "1.6", features = ["serde", "v4"] }
chrono = { version = "0.4", features = ["serde"] }
anyhow = "1.0"
foundationdb = "0.8"
Server Initialization​
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// Initialize logging
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
// Boot FoundationDB
let _network = unsafe { foundationdb::boot() };
let cluster_file = env::var("FDB_CLUSTER_FILE")
.unwrap_or_else(|_| "./config/fdb.cluster".to_string());
let db = Database::from_path(&cluster_file)?;
// Configure auth
let auth_config = AuthConfig::from_env();
let jwt_service = JwtService::new(auth_config.clone());
let rate_limiter = RateLimiter::new(RateLimitConfig::default());
// Create app state
let app_state = AppState::new(db, auth_config, jwt_service, rate_limiter);
// Start server
let bind_address = env::var("BIND_ADDRESS")
.unwrap_or_else(|_| "0.0.0.0:8080".to_string());
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(app_state.clone()))
.wrap(configure_cors())
.wrap(middleware::Logger::default())
.wrap(JwtAuth::new(app_state.jwt_service.clone()))
.wrap(AuditMiddleware::new(app_state.audit_service.clone()))
.configure(configure_routes)
})
.bind(&bind_address)?
.run()
.await
}
Route Configuration​
fn configure_routes(cfg: &mut web::ServiceConfig) {
cfg
// Authentication routes
.service(
web::scope("/auth")
.route("/login", web::post().to(auth::login))
.route("/register", web::post().to(auth::register))
.route("/logout", web::post().to(auth::logout))
.route("/validate", web::get().to(auth::validate_token))
.route("/refresh", web::post().to(auth::refresh_token))
)
// Resource routes
.service(
web::scope("/users")
.route("/me", web::get().to(user::get_current_user))
.route("/me", web::put().to(user::update_current_user))
)
// Health check
.route("/health", web::get().to(health_check));
}
Request/Response DTOs​
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct ApiResponse<T> {
pub success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<T>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<ErrorDetail>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ErrorDetail {
pub code: String,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<serde_json::Value>,
}
#[derive(Debug, Deserialize)]
pub struct PaginationParams {
#[serde(default = "default_page")]
pub page: u32,
#[serde(default = "default_per_page")]
pub per_page: u32,
}
fn default_page() -> u32 { 1 }
fn default_per_page() -> u32 { 20 }
JWT Middleware Implementation​
pub struct JwtAuth {
jwt_service: Arc<JwtService>,
}
impl<S, B> Transform<S, ServiceRequest> for JwtAuth
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Transform = JwtAuthMiddleware<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ok(JwtAuthMiddleware {
service,
jwt_service: self.jwt_service.clone(),
})
}
}
Handler Example: User Registration​
pub async fn register(
data: web::Data<AppState>,
req: web::Json<RegisterRequest>,
) -> Result<HttpResponse, ApiError> {
// Validate input
req.validate()?;
// Begin transaction
let tx = data.db.create_transaction()?;
// Check if user exists
let user_repo = UserRepository::new(data.db.clone());
if user_repo.get_by_email(&req.email, &tx).await?.is_some() {
return Err(ApiError::Conflict("Email already registered".into()));
}
// Create tenant for personal workspace
let tenant = Tenant {
id: Uuid::new_v4(),
name: format!("{}'s workspace", req.display_name),
created_at: Utc::now(),
updated_at: Utc::now(),
};
// Create user
let user = User {
id: Uuid::new_v4(),
tenant_id: tenant.id,
email: req.email.clone(),
display_name: req.display_name.clone(),
password_hash: hash_password(&req.password)?,
created_at: Utc::now(),
updated_at: Utc::now(),
};
// Save to database
let tenant_repo = TenantRepository::new(data.db.clone());
tenant_repo.create(&tenant, &tx).await?;
user_repo.create(&user, &tx).await?;
// Commit transaction
tx.commit().await?;
// Generate tokens
let access_token = data.jwt_service.generate_access_token(&user)?;
let refresh_token = data.jwt_service.generate_refresh_token(&user)?;
// Return response
Ok(HttpResponse::Ok().json(ApiResponse {
success: true,
data: Some(RegisterResponse {
user: user.into(),
workspace: tenant.into(),
access_token,
refresh_token,
}),
error: None,
}))
}
Repository Pattern Example​
pub struct UserRepository {
db: Arc<Database>,
}
impl UserRepository {
pub fn new(db: Arc<Database>) -> Self {
Self { db }
}
pub async fn create(&self, user: &User, tx: &Transaction) -> Result<()> {
let key = format!("{}/users/{}", user.tenant_id, user.id);
let value = serde_json::to_vec(user)?;
tx.set(&key.as_bytes(), &value);
// Create email index
let email_key = format!("{}/users_by_email/{}", user.tenant_id, user.email);
tx.set(&email_key.as_bytes(), user.id.as_bytes());
Ok(())
}
pub async fn get_by_id(
&self,
tenant_id: &Uuid,
user_id: &Uuid,
tx: &Transaction,
) -> Result<Option<User>> {
let key = format!("{}/users/{}", tenant_id, user_id);
match tx.get(&key.as_bytes(), false).await? {
Some(value) => Ok(Some(serde_json::from_slice(&value)?)),
None => Ok(None),
}
}
}
Rate Limiting Implementation​
pub struct RateLimiter {
limits: Arc<RwLock<HashMap<String, TokenBucket>>>,
config: RateLimitConfig,
}
impl RateLimiter {
pub async fn check_rate_limit(
&self,
key: &str,
profile: RateLimitProfile,
) -> Result<(), RateLimitError> {
let mut limits = self.limits.write().await;
let bucket = limits
.entry(key.to_string())
.or_insert_with(|| TokenBucket::new(profile));
if bucket.try_consume() {
Ok(())
} else {
Err(RateLimitError::LimitExceeded {
retry_after: bucket.next_refill(),
})
}
}
}
Testing Strategy​
Unit Test Example​
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_user_registration() {
let db = setup_test_db().await;
let app_state = create_test_app_state(db);
let req = RegisterRequest {
email: "test@example.com".to_string(),
password: "SecurePass123!".to_string(),
display_name: "Test User".to_string(),
};
let result = register(
web::Data::new(app_state),
web::Json(req),
).await;
assert!(result.is_ok());
let response = result.unwrap();
assert_eq!(response.status(), 200);
}
}
Integration Test Example​
#[actix_web::test]
async fn test_full_auth_flow() {
let app = test::init_service(create_app()).await;
// Register
let req = test::TestRequest::post()
.uri("/auth/register")
.set_json(&json!({
"email": "test@example.com",
"password": "SecurePass123!",
"display_name": "Test User"
}))
.to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
let body: ApiResponse<RegisterResponse> = test::read_body_json(resp).await;
let access_token = body.data.unwrap().access_token;
// Use token
let req = test::TestRequest::get()
.uri("/users/me")
.append_header(("Authorization", format!("Bearer {}", access_token)))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), 200);
}
Deployment Configuration​
Cloud Build​
steps:
- name: 'gcr.io/cloud-builders/docker'
args: ['build', '-t', 'gcr.io/$PROJECT_ID/coditect-api:$COMMIT_SHA', '.']
- name: 'gcr.io/cloud-builders/docker'
args: ['push', 'gcr.io/$PROJECT_ID/coditect-api:$COMMIT_SHA']
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
args:
- 'run'
- 'deploy'
- 'coditect-api'
- '--image=gcr.io/$PROJECT_ID/coditect-api:$COMMIT_SHA'
- '--region=us-central1'
- '--platform=managed'
- '--allow-unauthenticated'
- '--set-env-vars=FDB_CLUSTER_FILE=/config/fdb.cluster'
- '--min-instances=1'
- '--max-instances=100'
- '--memory=512Mi'
- '--cpu=1'
Monitoring & Observability​
Structured Logging​
#[derive(Serialize)]
struct LogEntry {
timestamp: DateTime<Utc>,
level: String,
message: String,
#[serde(flatten)]
context: HashMap<String, Value>,
}
info!("API request processed", {
"method": req.method(),
"path": req.path(),
"status": response.status().as_u16(),
"duration_ms": duration.as_millis(),
"tenant_id": tenant_id,
});
Metrics Collection​
pub struct Metrics {
request_counter: IntCounter,
request_duration: Histogram,
active_connections: IntGauge,
}
impl Metrics {
pub fn record_request(&self, method: &str, path: &str, status: u16, duration: Duration) {
self.request_counter
.with_label_values(&[method, path, &status.to_string()])
.inc();
self.request_duration
.with_label_values(&[method, path])
.observe(duration.as_secs_f64());
}
}
Error Handling​
#[derive(Debug, thiserror::Error)]
pub enum ApiError {
#[error("Bad request: {0}")]
BadRequest(String),
#[error("Unauthorized: {0}")]
Unauthorized(String),
#[error("Resource not found: {0}")]
NotFound(String),
#[error("Conflict: {0}")]
Conflict(String),
#[error("Internal server error")]
InternalError(#[from] anyhow::Error),
}
impl ResponseError for ApiError {
fn status_code(&self) -> StatusCode {
match self {
ApiError::BadRequest(_) => StatusCode::BAD_REQUEST,
ApiError::Unauthorized(_) => StatusCode::UNAUTHORIZED,
ApiError::NotFound(_) => StatusCode::NOT_FOUND,
ApiError::Conflict(_) => StatusCode::CONFLICT,
ApiError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
fn error_response(&self) -> HttpResponse {
HttpResponse::build(self.status_code()).json(ApiResponse::<()> {
success: false,
data: None,
error: Some(ErrorDetail {
code: self.status_code().to_string(),
message: self.to_string(),
details: None,
}),
})
}
}
Note: This technical implementation serves as the blueprint for AI agents to autonomously implement and extend the CODITECT API.