Skip to content

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.ts loaders for data fetching
  • prerender = false in +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:

  1. Extracts parameters (Path, Json, Query)
  2. Validates input
  3. Calls domain service
  4. 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 → ProblemDetails conversion in handlers
  • Repository errors → RepositoryErrorProblemDetails
  • Validation errors → ProblemDetails::validation(message, field)