Forberedelse til Rust-interview

Rust Backend-udvikler Interviewspørgsmål

15 udvalgte Rust backend-interviewspørgsmål grupperet efter anciennitetsniveau. Brug dem til at gennemgå grundlæggende principper, praktiske afvejninger og ræsonnement på seniorniveau i produktion.

Start et Rust AI-interviewIntet kreditkort kræves. 1 gratis session er tilgængelig.
Øv tekniske jobsamtaler på engelskEn tilstand, hvor ikke-modersmålstalende kan øve sig i at bestå tekniske interviews.

Junior spørgsmål

1Forklar Rusts ejerskabsregler, og hvordan de former praktiske backend-valg såsom kloning, lån og deling af applikationstilstand.

Rust giver hver værdi en enkelt ejer. At flytte en ikke-`Copy` værdi overfører ejerskab, og værdien friges, når dens ejer udgår af gyldighedsområde. Kode kan enten låne data uforanderligt via delte referencer eller foranderligt via en eksklusiv foranderlig reference. I backend-kode former dette, om en funktion tager ejerskab, låner midlertidigt, kloner for at opnå en uafhængig ejet værdi, eller deler langlivede tilstande via håndtag som `Arc`, med synkronisering eller andre primitiver for parallelitet, når delt foranderlighed er nødvendig.

use std::sync::Arc;

struct AppState {
    service_name: String,
}

async fn handler(state: Arc<AppState>, request_id: String) -> String {
    // Borrow: no ownership transfer of the String inside state.
    let name: &str = &state.service_name;

    // Clone only if an owned independent value is needed.
    let owned_name = name.to_owned();

    format!("{owned_name}:{request_id}")
}

fn configure(state: Arc<AppState>) {
    // Cloning Arc increments the reference count; it does not clone AppState.
    let state_for_route = Arc::clone(&state);
    let _ = state_for_route;
}
Prøv at besvare dette spørgsmål med en AI-coach

2Hvad er forskellen mellem lån (borrowing) med `&T` og `&mut T`, og hvordan påvirker disse regler API- og handler-signaturer?

`&T` er en delt reference: den tillader skrivebeskyttet adgang, og mange delte referencer til den samme værdi kan være aktive samtidigt. `&mut T` er en eksklusiv foranderlig reference: den tillader mutation (ændring), men mens den er aktiv, må ingen andre delte eller foranderlige referencer til den samme værdi bruges. Disse regler former API'er ved at få skrivebeskyttede funktioner til at acceptere `&T` eller smallere lånte typer som `&str`, mens muterende funktioner accepterer `&mut T`, overtager ejerskab (ownership), eller bruger indre foranderlighed (interior mutability)/synkronisering. Backend-handlere undgår normalt direkte `&mut` adgang til delt applikationsstatus på tværs af samtidige forespørgsler og bruger i stedet delte håndtag (handles) plus synkronisering eller andre ejerskabs-mønstre.

struct User {
    name: String,
    login_count: u64,
}

fn display_name(user: &User) -> &str {
    &user.name
}

fn record_login(user: &mut User) {
    user.login_count += 1;
}

fn main() {
    let mut user = User { name: "Ada".into(), login_count: 0 };

    let name = display_name(&user); // shared borrow for reading
    println!("{name}");

    record_login(&mut user); // exclusive mutable borrow for mutation
}
Prøv at besvare dette spørgsmål med en AI-coach

3Hvordan fungerer `Box`, og hvilke problemer løser heap-allokering via `Box`?

`Box<T>` er en ejer-smart-pointer, hvis `T` gemmes på heapen, mens `Box`-håndtaget er en pointer-lignende værdi med fast størrelse. Den har enkelt ejerskab og frigør heap-allokeringen, når den går ud af gyldighedsområde. `Box` giver indirekte adressering, hvilket hjælper med store værdier, rekursive typer, der kræver en kendt størrelse, dynamisk størrelse værdier og trait-objekter som `Box<dyn Trait>`. At flytte en `Box` overfører ejerskabet af allokeringen uden at flytte eller kopiere den værdi, der er gemt på heapen.

enum List {
    Cons(i32, Box<List>),
    Nil,
}

let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
Prøv at besvare dette spørgsmål med en AI-coach

4Beskriv `Result` og `Option` samt de primære idiomatiske måder at arbejde med fravær og fejlbehæftede operationer på.

`Option<T>` repræsenterer enten `Some(T)` eller `None` og bruges til forventet fravær af en værdi. `Result<T, E>` repræsenterer enten `Ok(T)` eller `Err(E)` og bruges til fejlbehæftede operationer, hvor fejlinformation er relevant. Idiomatisk håndtering inkluderer `match`, `if let`/`let else`, kombinatorer som `map`, `and_then`, `ok_or`, `unwrap_or` og `unwrap_or_else`, samt `?`-operatoren til tidlig videresendelse fra funktioner, der returnerer kompatible `Option`- eller `Result`-typer. `unwrap` og `expect` bør kun bruges til invarianter, tests, prototyper eller uigenkaldelige situationer, ikke til normale, genoprettelige backend-fejl.

fn parse_port(s: Option<&str>) -> u16 {
    s.and_then(|v| v.parse::<u16>().ok())
     .unwrap_or(8080)
}

assert_eq!(parse_port(Some("3000")), 3000);
assert_eq!(parse_port(None), 8080);
Prøv at besvare dette spørgsmål med en AI-coach

5Hvordan fungerer `?`-operatoren, og hvad muliggør konvertering mellem forskellige fejlt_yper?

`?`-operatoren er en forkortelse for at udbrede fejl. For `Result` vil `expr?` give `Ok`-værdien og fortsætte, eller returnere tidligt fra den aktuelle funktion med en `Err`; for `Option` vil den give `Some`-værdien eller returnere `None`. Den omgivende funktion skal returnere en kompatibel type. For `Result` kan forskellige fejlt_yper kombineres, fordi kilde-fejlen konverteres til funktionens fejlt_ype ved hjælp af `From`/`Into`-stil konvertering, ofte implementeret manuelt eller afledt med hjælpe-crates som `thiserror`.

use std::{fs, io, num::ParseIntError};

#[derive(Debug)]
enum AppError {
    Io(io::Error),
    Parse(ParseIntError),
}

impl From<io::Error> for AppError {
    fn from(e: io::Error) -> Self { AppError::Io(e) }
}
impl From<ParseIntError> for AppError {
    fn from(e: ParseIntError) -> Self { AppError::Parse(e) }
}

fn read_number(path: &str) -> Result<i32, AppError> {
    let s = fs::read_to_string(path)?; // io::Error -> AppError
    let n = s.trim().parse::<i32>()?;  // ParseIntError -> AppError
    Ok(n)
}
Prøv at besvare dette spørgsmål med en AI-coach

6Forklar mønstermatchning på enums, og hvordan udtømmende matchning forbedrer domæne- og API-korrekthed.

Enums modellerer en værdi, der kan være en af et fast sæt varianter, og varianterne kan indeholde data. `match` forgrener sig baseret på variant og kan dekonstruere de interne data. Rust kontrollerer, at enum-matches er udtømmende, medmindre et wildcard- eller catch-all-mønster bruges. Dette forbedrer domæne- og API-korrektheden, fordi når en ny tilstands- eller svarvariant tilføjes, kan compileren tvinge kode, der matcher på den, til at beslutte, hvordan den nye situation skal håndteres, i stedet for blot at ignorere den.

enum PaymentStatus {
    Pending,
    Paid { receipt_id: String },
    Failed(String),
}

fn message(status: PaymentStatus) -> String {
    match status {
        PaymentStatus::Pending => "Payment is pending".to_string(),
        PaymentStatus::Paid { receipt_id } => format!("Paid, receipt {receipt_id}"),
        PaymentStatus::Failed(reason) => format!("Payment failed: {reason}"),
    }
}
Prøv at besvare dette spørgsmål med en AI-coach

7Hvad er newtype-wrappere, og hvordan forbedrer de typesikkerheden for ID'er, penge, tokens og hemmeligheder?

En newtype-wrapper er en særskilt Rust-type, ofte en `tuple struct` med et felt, omkring en eksisterende repræsentation, såsom `struct UserId(Uuid);` eller `struct AccessToken(String);`. Den forbedrer typesikkerheden, fordi kompilatoren ikke forveksler semantisk forskellige værdier, der deler den samme underliggende type, såsom `UserId` versus `OrderId`, øre versus kroner, eller rå strenge versus validerede tokens. Newtypes kan også centralisere konstruktions- og valideringsregler og styre traits såsom `Display`, `Debug`, `Serialize` eller redigeringsadfærd for hemmeligheder.

use uuid::Uuid;

#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
struct UserId(Uuid);

#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
struct OrderId(Uuid);

struct SecretToken(String);

impl std::fmt::Debug for SecretToken {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str("SecretToken(**redacted**)")
    }
}

fn load_user(id: UserId) {
    // query by user id
}

fn example(user_id: UserId, order_id: OrderId) {
    load_user(user_id);
    // load_user(order_id); // does not compile: expected UserId, found OrderId
}
Prøv at besvare dette spørgsmål med en AI-coach

Mellem spørgsmål

8Hvordan fungerer `Drop`-traiten, herunder frigivelsesrækkefølge, RAII-oprydning og begrænsninger omkring asynkron oprydning?

`Drop` er Rusts deterministiske oprydnings-hook. Når en ejet værdi destrueres, f.eks. når den går ud af scope, kalder Rust automatisk dens `drop(&mut self)`-metode, hvis den implementerer `Drop`, og derefter frigives dens felter. Dette understøtter RAII (Resource Acquisition Is Initialization): ressourcer som filer, sockets, låse, transaktioner, tilladelser eller buffere er bundet til ejende værdier/guards og frigives, når disse værdier frigives. Lokale variabler frigives i omvendt rækkefølge af oprettelse; for en struct med `Drop` kører structens `drop`-metode, før dens felter frigives, og felterne frigives i deklarationsrækkefølge. `Drop::drop` er synkron og kan ikke være `async` eller awaitet, så yndefuld asynkron oprydning kræver normalt en eksplicit asynkron lukke-/nedluknings-/flush-metode eller et andet design; `Drop` kan kun udføre synkron eller bedste-indsats oprydning.

struct Guard(&'static str);

impl Drop for Guard {
    fn drop(&mut self) {
        println!("drop {}", self.0);
    }
}

struct Pair {
    first: Guard,
    second: Guard,
}

impl Drop for Pair {
    fn drop(&mut self) {
        println!("drop Pair");
    }
}

fn main() {
    let _a = Guard("a");
    let _pair = Pair {
        first: Guard("first"),
        second: Guard("second"),
    };
    let _b = Guard("b");
}
Prøv at besvare dette spørgsmål med en AI-coach

9Forklar levetider (lifetimes), og hvorfor eksplicitte levetidsannotationer nogle gange er nødvendige i Rust API'er.

En levetid beskriver, hvor længe en reference er gyldig, hvilket gør det muligt for compileren at afvise ugyldige referencer (dangling references). Levetidsannotationer er compile-time begrænsninger, der udtrykker relationer mellem referencer; de udvider ikke levetiden for de underliggende data eller tilføjer runtime-adfærd. Mange simple signaturer håndteres af levetidselision, men eksplicitte annotationer er nødvendige, når compileren ikke kan udlede, hvordan input- og outputreferencer relaterer, når typer gemmer referencer, eller når et API skal udtrykke generiske lånebegrænsninger (borrowing constraints).

fn longest<'a>(left: &'a str, right: &'a str) -> &'a str {
    if left.len() >= right.len() { left } else { right }
}

struct UserView<'a> {
    name: &'a str,
}

fn main() {
    let a = String::from("short");
    let b = String::from("longer");
    let result = longest(&a, &b);
    let view = UserView { name: result };
    println!("{}", view.name);
}
Prøv at besvare dette spørgsmål med en AI-coach

10Hvad betyder 'static levetiden, og hvordan interagerer den med spawnede tasks og backend API-design?

`'static` på en reference betyder, at de refererede data er gyldige for hele programmet, som med strenglitteraler eller `static` elementer. En grænse som `T: 'static` betyder, at værdier af type `T` ikke indeholder ikke-`'static` lånte referencer, så de kan holdes i en vilkårlig levetid; det betyder ikke, at værdien selv skal leve evigt eller ikke kan slettes (droppes). Spawnede tasks kræver ofte `'static` futures, fordi eksekutoren kan fortsætte med at køre dem, efter at den kaldende stack-frame er returneret. I backend-kode betyder dette typisk at flytte ejede data ind i tasken med `async move`, bruge ejede typer eller klone delte håndtag som `Arc`, frem for at låne lokale variabler. API'er (Application Programming Interfaces) bør bruge `'static` grænser for lagrede callbacks, baggrundsjobs og langlivede tilstande, men undgå unødvendige `'static` grænser for kortlivede lånte operationer.

use std::sync::Arc;

struct AppState {
    name: String,
}

fn spawn_background(state: Arc<AppState>, user_id: String) {
    tokio::spawn(async move {
        // The task owns an Arc handle and a String, so it does not borrow this function's stack.
        println!("{}:{user_id}", state.name);
    });
}
Prøv at besvare dette spørgsmål med en AI-coach

11Beskriv, hvordan Rust bruger `traits` til polymorfi, og sammenlign `impl Trait`, `generics` og `dyn Trait`.

Rust bruger `traits` til at udtrykke delt adfærd. Generiske bindinger som `fn f<T: Trait>(x: T)` bruger normalt statisk dispatch gennem monomorfisering. `impl Trait` i argumentposition er mestendels en forkortelse for en anonym generisk parameter. `impl Trait` i returposition skjuler en konkret returtype, der vælges af funktionen. `dyn Trait` er et `trait` objekt, der bruges bag en pointer som `&dyn Trait` eller `Box<dyn Trait>`; det bruger dynamisk dispatch via en vtable og understøtter runtime type-erasure/heterogene værdier underlagt `object-safety` restriktioner.

trait Render { fn render(&self) -> String; }

struct Html;
struct Json;

impl Render for Html { fn render(&self) -> String { "html".into() } }
impl Render for Json { fn render(&self) -> String { "json".into() } }

fn render_generic<T: Render>(x: &T) -> String { x.render() } // static dispatch
fn render_impl_arg(x: &impl Render) -> String { x.render() } // anonymous generic
fn make_renderer() -> impl Render { Html } // one hidden concrete type
fn render_dyn(x: &dyn Render) -> String { x.render() } // dynamic dispatch

fn main() {
    let items: Vec<Box<dyn Render>> = vec![Box::new(Html), Box::new(Json)];
    for item in items { println!("{}", render_dyn(item.as_ref())); }
}
Prøv at besvare dette spørgsmål med en AI-coach

12Hvad er associerede typer på traits, og hvornår foretrækkes de frem for generiske typeparametre?

Associerede typer er navngivne typepladsholdere, der er erklæret inde i en trait og specificeret af hver implementering, for eksempel `Iterator::Item`. De foretrækkes, når typen er en del af traitens kontrakt, og hver implementor har ét naturligt valg til den. Generiske typeparametre foretrækkes, når den, der kalder, skal vælge typen, eller når den samme implementor kan have brug for flere implementeringer for forskellige typevalg.

trait Repository {
    type Entity;
    fn get(&self, id: u64) -> Option<Self::Entity>;
}

struct User;
struct UserRepo;

impl Repository for UserRepo {
    type Entity = User;
    fn get(&self, _id: u64) -> Option<Self::Entity> { Some(User) }
}

fn load<R: Repository>(repo: &R) -> Option<R::Entity> {
    repo.get(1)
}
Prøv at besvare dette spørgsmål med en AI-coach

Senior spørgsmål

13Hvordan ville du implementere det transaktionelle outbox-mønster i en Rust-tjeneste?

For at implementere det transaktionelle outbox-mønster i en Rust-tjeneste skriver du domænedataopdateringer og udgående event-payloads atomisk inden for en enkelt databasetransaktion (f.eks. ved brug af `sqlx::Transaction`). En `outbox`-tabel gemmer destinations-topic'en, payload'et og status eller oprettelsestidsstempel. En asynkron baggrundsarbejder (eller en Change Data Capture-proces som Debezium) afspørger eller streamer derefter ventende events og publicerer dem til meddelelsesmægleren (såsom Kafka, RabbitMQ eller NATS). Ved brug af en afspørgende arbejder i Rust (f.eks. i en `tokio::spawn`-løkke) tillader brug af `SELECT ... FOR UPDATE SKIP LOCKED`, at flere tjenesteinstanser kan hente og afsende outbox-rækker samtidigt uden låsekonflikter. Når meddelelsesmæglerleverancen er bekræftet, opdaterer arbejderen outbox-recordens status eller sletter rækken. Da netværksgenforsøg kan resultere i mindst-én-gang-levering, skal downstream-forbrugere designes til at håndtere meddelelser idempotent.

use sqlx::{PgPool, Postgres, Transaction};
use uuid::Uuid;

pub async fn create_user_with_outbox(
    mut tx: Transaction<'_, Postgres>,
    username: &str,
    email: &str,
) -> Result<(), sqlx::Error> {
    let user_id = Uuid::new_v4();
    sqlx::query!(
        "INSERT INTO users (id, username, email) VALUES ($1, $2, $3)",
        user_id, username, email
    )
    .execute(&mut *tx)
    .await?;

    let payload = serde_json::json!({"event": "UserCreated", "user_id": user_id, "email": email});
    sqlx::query!(
        "INSERT INTO outbox_events (id, topic, payload, status) VALUES ($1, $2, $3, 'PENDING')",
        Uuid::new_v4(),
        "user_events",
        payload
    )
    .execute(&mut *tx)
    .await?;

    tx.commit().await?;
    Ok(())
}

pub async fn run_outbox_relay(pool: PgPool) {
    loop {
        let mut tx = match pool.begin().await { Ok(t) => t, Err(_) => continue };
        let rows = sqlx::query!(
            "SELECT id, topic, payload FROM outbox_events WHERE status = 'PENDING' LIMIT 50 FOR UPDATE SKIP LOCKED"
        )
        .fetch_all(&mut *tx)
        .await;

        if let Ok(events) = rows {
            for event in events {
                // publish_to_broker(&event.topic, &event.payload).await;
                let _ = sqlx::query!("UPDATE outbox_events SET status = 'PROCESSED' WHERE id = $1", event.id)
                    .execute(&mut *tx)
                    .await;
            }
            let _ = tx.commit().await;
        }
        tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
    }
}
Prøv at besvare dette spørgsmål med en AI-coach

14Hvordan ville du designe en flertrins-operation, der opdaterer en database og kalder en ekstern API?

At designe en flertrins-operation, der omfatter en databaseskrivning og et kald til en ekstern API, kræver håndtering af delvise fejl og netværkspartitioneringer uden at være afhængig af 2PC (Two-Phase Commit): 1. **Først Persistér Tilstand / Hensigt**: Registrer en initial tilstand (såsom `PENDING` eller `INITIATED`) og den nødvendige nyttelast inden for den lokale databasetransaktion. Commit denne transaktion, før det eksterne netværkskald initieres, for at undgå at holde databaselåse og forbindelsespuljeslots under langsom netværks I/O (Input/Output). 2. **Idempotent Ekstern Forespørgsel**: Kald den eksterne API ved hjælp af en deterministisk idempotensnøgle (f.eks. afledt af operationens UUID (Universally Unique Identifier)). Dette sikrer, at gentagne forsøg ikke forårsager duplikerede virkelige bivirkninger (såsom dobbelt opkrævning). 3. **Overgang af Tilstand og Håndtering af Timeouts**: Hvis API-kaldet returnerer et succesfuldt svar, opdateres databasenoteringen til `COMPLETED`. Hvis det fejler permanent, markeres det som `FAILED` og der udføres kompenserende handlinger. Hvis kaldet får timeout eller returnerer en netværksfejl, markeres noteringen som `INDETERMINATE` / `REQUIRES_RECONCILIATION`, og en baggrundsarbejder afstemmer tilstanden ved at forespørge udbyderens status-endpoint eller genforsøge med den samme idempotensnøgle.

use uuid::Uuid;

pub async fn process_external_charge(pool: &sqlx::PgPool, user_id: Uuid, amount: i64) -> Result<(), String> {
    let payment_id = Uuid::new_v4();
    let idempotency_key = format!("pay_{}", payment_id);

    // 1. Commit intent locally before making external network calls
    sqlx::query!(
        "INSERT INTO payments (id, user_id, amount, status, idempotency_key) VALUES ($1, $2, $3, 'PENDING', $4)",
        payment_id, user_id, amount, idempotency_key
    )
    .execute(pool)
    .await
    .map_err(|e| e.to_string())?;

    // 2. Call external API with timeout and idempotency key
    let client = reqwest::Client::new();
    let api_result = client.post("https://api.payment.com/v1/charges")
        .header("Idempotency-Key", &idempotency_key)
        .json(&serde_json::json!({ "amount": amount }))
        .timeout(std::time::Duration::from_secs(5))
        .send()
        .await;

    // 3. Update state based on outcome; handle timeouts via reconciliation
    match api_result {
        Ok(resp) if resp.status().is_success() => {
            sqlx::query!("UPDATE payments SET status = 'SUCCESS' WHERE id = $1", payment_id)
                .execute(pool)
                .await
                .map_err(|e| e.to_string())?;
        }
        Ok(_) => {
            sqlx::query!("UPDATE payments SET status = 'FAILED' WHERE id = $1", payment_id)
                .execute(pool)
                .await
                .map_err(|e| e.to_string())?;
        }
        Err(_) => {
            sqlx::query!("UPDATE payments SET status = 'REQUIRES_RECONCILIATION' WHERE id = $1", payment_id)
                .execute(pool)
                .await
                .map_err(|e| e.to_string())?;
        }
    }
    Ok(())
}
Prøv at besvare dette spørgsmål med en AI-coach

15Hvordan ville du designe et multi-tenant Rust-backend med streng dataisolering og dynamisk routing?

Design af et multi-tenant Rust-backend involverer tre nøgleområder: identifikation af tenant (lejer), strategi for dataisolering og dynamisk routing. 1. **Tenant-ekstraktion og kontekst-propagering**: En HTTP (Hypertext Transfer Protocol)-middleware (f.eks. i `Axum` eller `Actix-web`) løser tenant-identiteten fra JWT (JSON Web Token)-claims, subdomæner eller headers. Den konstruerer en stærkt typet `TenantContext`, der lagres i request-extensions. Handlere bruger Rust type-extractors til at håndhæve, at forretningslogik ikke kan udføres uden en gyldig tenant-kontekst. 2. **Strategi for dataisolering**: * ***Delt database med Column- / Row-Level Security (RLS)***: Alle tenants deler tabeller med en `tenant_id`-kolonne. Isolering håndhæves via PostgreSQL RLS ved hjælp af session-variabler på forbindelsesniveau (f.eks. `SET LOCAL app.tenant_id = $1`) eller query builder-wrappers. * ***Skema-per-Tenant***: Tenants deler en database-instans, men har isolerede skemaer; routing justerer søgestien per forbindelse. * ***Database-per-Tenant***: Tenants har separate databaser. Tjenesten opretholder et pool-register (f.eks. `Arc<DashMap<TenantId, PgPool>>` eller en LRU (Least Recently Used) pool-cache) for dynamisk at løse forbindelsespools. 3. **Forhindring af lækager**: For at undgå kontamination på tværs af tenants i asynkrone `Tokio`-applikationer skal tenant-kontekst sendes eksplicit på tværs af task-grænser i stedet for at blive gemt i global tilstand eller `thread-local storage` (`thread_local!`), som bryder sammen på tværs af asynkrone worker thread-migreringer.

use axum::{extract::{FromRequestParts, State}, http::request::Parts, async_trait};
use std::sync::Arc;
use dashmap::DashMap;
use sqlx::PgPool;

#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct TenantId(pub String);

#[derive(Clone)]
pub struct AppState {
    pub pool_registry: Arc<DashMap<TenantId, PgPool>>,
}

#[derive(Clone)]
pub struct TenantContext {
    pub tenant_id: TenantId,
    pub pool: PgPool,
}

#[async_trait]
impl FromRequestParts<AppState> for TenantContext {
    type Rejection = (axum::http::StatusCode, &'static str);

    async fn from_request_parts(parts: &mut Parts, state: &AppState) -> Result<Self, Self::Rejection> {
        let tenant_header = parts.headers.get("X-Tenant-ID")
            .and_then(|v| v.to_str().ok())
            .ok_or((axum::http::StatusCode::BAD_REQUEST, "Missing tenant ID"))?;
        
        let tenant_id = TenantId(tenant_header.to_string());
        
        let pool = match state.pool_registry.get(&tenant_id) {
            Some(p) => p.value().clone(),
            None => return Err((axum::http::StatusCode::NOT_FOUND, "Tenant database pool not found")),
        };

        Ok(TenantContext { tenant_id, pool })
    }
}
Prøv at besvare dette spørgsmål med en AI-coach