Request Flow¶
End-to-end lifecycle of a typical API request, from Svelte component to database and back.
Sequence Diagram¶
sequenceDiagram
participant U as User
participant S as Svelte Component
participant A as API Client (lib/api/)
participant H as Axum Handler
participant SVC as Domain Service
participant R as Repository (infra)
participant DB as PostgreSQL
U->>S: Click action
S->>A: apiClient.cases.get(id)
A->>H: GET /api/cases/{id} + JWT header
H->>H: Extract AuthUser from JWT
H->>SVC: case_service.get(id, office_id)
SVC->>R: case_repo.find_by_id(id)
R->>DB: SELECT * FROM cases WHERE id = $1
DB-->>R: Row data
R-->>SVC: Case entity
SVC-->>H: Case entity
H-->>A: JSON response (camelCase)
A-->>S: TypeScript object
S-->>U: Rendered UI
Frontend Layer¶
API Client (client/src/lib/api/client.ts)¶
All HTTP requests go through a centralized API client:
// Simplified — real client handles auth, error mapping, base URL
const response = await fetch(`${baseUrl}/api/cases/${id}`, {
headers: { Authorization: `Bearer ${token}` }
});
- Base URL configured at runtime (Tauri environment)
- JWT token attached automatically to every request
- 401 responses trigger re-authentication
- 409 responses (version conflict) show a global toast
SvelteKit Pages (client/src/routes/)¶
- Pages use
+page.tsloaders for data fetching prerender = falsein+layout.ts(Tauri SPA)- Navigation via
<a href>(SvelteKit router),goto()only for programmatic nav
Backend Layer¶
Router (server/crates/api/src/router.rs)¶
Routes are grouped by resource:
Router::new()
.route("/api/cases", get(list_cases).post(create_case))
.route("/api/cases/:id", get(get_case).put(update_case))
// ...
Handlers (server/crates/api/src/handlers/)¶
Each handler:
- Extracts parameters (
Path,Json,Query) - Validates input
- Calls domain service
- Returns JSON response
pub async fn get_case(
State(state): State<AppState>,
auth: AuthUser,
Path(id): Path<Uuid>,
) -> Result<Json<CaseResponse>, ProblemDetails> {
let case = state.case_service.get(id, auth.office_id).await?;
Ok(Json(CaseResponse::from(case)))
}
Domain Services (server/crates/domain/src/services/)¶
Business logic lives here. Services receive repository traits via DI:
pub struct CaseService {
repo: Arc<dyn CaseRepository>,
// ...
}
Repositories (server/crates/infra/src/postgres/)¶
SQL queries via sqlx:
impl CaseRepository for PgCaseRepository {
async fn find_by_id(&self, id: Uuid) -> Result<Option<Case>, RepositoryError> {
sqlx::query_as!(Case, "SELECT * FROM cases WHERE id = $1", id)
.fetch_optional(&self.pool)
.await
.map_err(Into::into)
}
}
Error Handling¶
Errors use RFC 7807 ProblemDetails throughout:
{
"type": "validation_error",
"title": "Validation Failed",
"status": 422,
"detail": "Le champ email est invalide",
"field": "email"
}
- Domain errors →
ProblemDetailsconversion in handlers - Repository errors →
RepositoryError→ProblemDetails - Validation errors →
ProblemDetails::validation(message, field)