Package contract
The planned crate is nordic-devhouse and its crate name is nordic_devhouse. It is asynchronous, serde-native, and ships rustls TLS by default without unsafe code.
| Distribution | Namespace or import | Runtime |
|---|---|---|
| nordic-devhouse | nordic_devhouse | Current stable Rust with Tokio |
Create and reuse the client
The builder validates configuration once. Run consumes a typed request by value, borrows the reusable client, and returns a generic JobResult<T> decoded through serde.
use nordic_devhouse::{Client, Error, OutputFormat, ScrapeRequest};
#[tokio::main]
async fn main() -> Result<(), Error> {
let client = Client::builder()
.api_key(std::env::var("NORDIC_DEVHOUSE_API_KEY")?)
.max_retries(3)
.build()?;
let result = client.scrape().run(
ScrapeRequest::new("https://example.com/products")
.format(OutputFormat::Json)
.idempotency_key("catalog-2026-08-11"),
).await?;
println!("{} {}", result.job_id, result.records);
Ok(())
}Control a durable job
Start and wait are async methods that accept cancellation through future dropping and optional explicit deadlines. Dropping the future never implies cancellation of the remote job.
let job = client.scrape().start(
ScrapeRequest::new("https://example.com/products")
.format(OutputFormat::Json),
).await?;
let result = client.jobs()
.wait(&job.id)
.timeout(Duration::from_secs(600))
.await?;Public models
Names follow the language conventions, but every SDK preserves the same fields and job-state semantics.
| Concept | Contract |
|---|---|
| Client options | API key, base URL, request timeout, retry budget, and optional test transport |
| Scrape request | URL, output formats, webhook URL, metadata, and idempotency key |
| Job | Stable ID, status, timestamps, progress, and warnings |
| Job result | Job ID, record count, typed data, metadata, and request ID |
| Webhook event | Verified event ID, type, timestamp, and typed data |
Handle errors idiomatically
A non-exhaustive Error enum preserves typed recovery data and sources. Callers match only variants they can recover from and use the catch-all SDK variant for forward compatibility.
match client.scrape().run(request).await {
Ok(result) => consume(result),
Err(Error::RateLimited { retry_after, request_id, .. }) => {
tracing::warn!(?retry_after, %request_id, "rate limited");
}
Err(Error::Validation { issues, .. }) => reject(issues),
Err(error) => {
tracing::error!(request_id = error.request_id(), %error);
return Err(error);
}
}Verify the raw webhook body
Verification uses the exact request bytes, validates HMAC-SHA256 and timestamp tolerance, and returns a typed event. Persist the verified event ID to prevent duplicate processing.
let event = client.webhooks().verify(
raw_body,
headers.get("Nordic-Signature"),
&std::env::var("NORDIC_WEBHOOK_SECRET")?,
)?;
if event.kind == "scrape.completed" {
import_result(&event.data.job_id).await?;
}Runtime and lifecycle rules
- 1Use async Rust with Tokio and rustls defaults
- 2Mark public enums non-exhaustive for forward compatibility
- 3Avoid unsafe code in the crate and dependencies owned by the module
- 4Expose serde models and retain unknown fields when requested
- 5Use feature flags only for real adapters such as tracing and native-tls