Typed Rust Client Library for consuming ServiceStack APIs.
- Typed Request/Response DTOs, generated from any ServiceStack API
- Response Types inferred from the Request DTO — no turbofish, no
serde_json::Value - Structured
ResponseStatuserrors with field validation errors - Auth with Basic Auth, API Keys, JWT Bearer Tokens, Refresh Tokens and Session Cookies
- Batched Requests, one-way Requests and multipart file uploads
- Async by default, with an optional sync client in
servicestack::blocking
cargo add servicestackOr in Cargo.toml:
[dependencies]
servicestack = "0.1"
serde = { version = "1", features = ["derive"] }Requires Rust 1.88+.
| Feature | Description |
|---|---|
blocking |
Sync client in servicestack::blocking for CLIs and scripts |
multipart |
multipart/form-data file uploads |
servicestack = { version = "0.1", features = ["blocking"] }Generate the Rust DTOs of any ServiceStack API with the get-dtos tool:
npx get-dtos rust https://blazor-vue.web-templates.ioWhich downloads a dtos.rs containing the typed DTOs of the remote API:
use servicestack::*;
use serde::{Serialize, Deserialize};
// @Route("/hello/{Name}")
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
#[serde(default)]
pub struct Hello {
pub name: Option<String>,
}
impl IRequest for Hello {
const NAME: &'static str = "Hello";
const VERB: &'static str = "GET";
}
impl IReturn for Hello { type Response = HelloResponse; }The generated IRequest/IReturn impls are what let the client infer each API's
Response Type and the HTTP Method it should be sent with.
use servicestack::JsonServiceClient;
mod dtos; use dtos::*;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = JsonServiceClient::new("https://blazor-vue.web-templates.io");
let res = client.send(&Hello { name: Some("World".into()) }).await?; // res is a HelloResponse
println!("{}", res.result);
Ok(())
}send uses the HTTP Method the API is annotated with, use get, post, put,
patch or delete to send a Request DTO with a specific HTTP Method:
let res = client.post(&Hello { name: Some("World".into()) }).await?;APIs that don't return a Response Body are sent with send_void:
client.send_void(&DeleteBooking { id: 1 }).await?;Enable the blocking feature for the same API without async/await:
use servicestack::blocking::JsonServiceClient;
let client = JsonServiceClient::new("https://blazor-vue.web-templates.io");
let res = client.send(&Hello { name: Some("World".into()) })?;AutoQuery APIs return a typed QueryResponse<T>, with the query params of their
base type flattened into the Request DTO:
use servicestack::QueryDb;
let res = client.send(&QueryBookings {
query_db: QueryDb { take: Some(5), order_by_desc: Some("id".into()), ..Default::default() },
..Default::default()
}).await?;
for booking in res.results { // booking is a Booking
println!("{} {}", booking.id, booking.name);
}Failed API Requests return an Error containing the HTTP Status Code and the
API's structured ResponseStatus:
match client.send(&CreateBooking::default()).await {
Ok(res) => println!("{}", res.id),
Err(err) => {
println!("{:?}", err.status_code()); // Some(400)
println!("{:?}", err.error_code()); // Some("NotEmpty")
println!("{:?}", err.error_message()); // Some("'Name' must not be empty.")
println!("{:?}", err.field_error("Name")); // Some("'Name' must not be empty.")
println!("{}", err.is_unauthorized()); // false
}
}Alternatively api returns errors in its result instead of a separate Err:
let api = client.api(&CreateBooking::default()).await;
if api.failed() {
println!("{:?} {:?}", api.error_code(), api.field_error("Name"));
} else {
println!("{}", api.response().unwrap().id);
}API Keys and JWTs are sent in the Bearer Token Authorization header:
let mut client = JsonServiceClient::new("https://example.org");
client.set_bearer_token("ak-87949de37e894627a9f6173154e7cafa");HTTP Basic Auth credentials:
client.set_credentials("username", "password");Sign in with ServiceStack's Authenticate API, which maintains the authenticated Session in the client's cookie jar and uses any Bearer Token the Server returns:
let auth = client.authenticate("username", "password").await?;When a Refresh Token is configured, expired Bearer Tokens are transparently refreshed and the failed Request retried:
client.set_refresh_token(&refresh_token);If the Server returns a 401 Unauthorized Response either because the client was
unauthenticated or its Bearer Token or API Key had expired, use
set_on_authentication_required to re-configure the client before the original
Request is automatically retried:
client.set_on_authentication_required(|client| {
Box::pin(async move {
client.authenticate("username", "password").await?;
Ok(())
})
});
// Automatically retries Requests returning 401 Responses
let res = client.send(&Secured::default()).await?;A configured Refresh Token takes precedence over the callback, which is only used when no Refresh Token is set or refreshing it failed.
let responses = client.send_all(&[
Hello { name: Some("A".into()) },
Hello { name: Some("B".into()) },
]).await?;Or send a Request to a one-way endpoint that ignores its Response:
client.publish(&Hello { name: Some("World".into()) }).await?;Enable the multipart feature:
use servicestack::UploadFile;
let res = client.post_files_with_request(&UploadPhoto { album: "Holiday".into() }, vec![
UploadFile {
field_name: "file".into(),
file_name: "photo.png".into(),
content_type: Some("image/png".into()),
data: std::fs::read("photo.png")?,
},
]).await?;let res: HelloResponse = client.get_url("/hello/World").await?;
let res: HelloResponse = client.post_url("/hello", &Hello { name: Some("World".into()) }).await?;
let csv: String = client.get_url_string("/api/QueryBookings.csv").await?;let mut client = JsonServiceClient::new("https://example.org");
client.set_header("X-Custom", "Value");
client.set_user_agent("my-app/1.0");
// Or supply a pre-configured reqwest Client for timeouts, proxies and TLS
let http = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()?;
let client = JsonServiceClient::with_client("https://example.org", http);JsonServiceClient::new sends Requests to ServiceStack's pre-defined /api
route. Use JsonServiceClient::new_json_service_client for older ServiceStack
instances that only have the /json/reply routes enabled, or set_base_path
for a custom base path.
- examples/hello.rs — async: typed APIs, batched Requests, validation errors and authentication
- examples/hello_blocking.rs — the sync client
cargo run --example hello
cargo run --example hello_blocking --features blockingcargo test --all-features # unit tests
cargo test --all-features -- --ignored # integration tests against test.servicestack.netReleases are cut with npm scripts and published by the release GitHub Action:
npm run bump # 0.1.0 -> 0.1.1 (also `-- minor`, `-- major`, `-- 1.2.3`)
# describe the release in CHANGELOG.md, then
npm run releaseOr in a single step:
npm run release -- patchnpm run release tags the version, pushes it and creates the GitHub Release,
which triggers the workflow that runs the tests and publishes it to crates.io.
BSD-3-Clause. See LICENSE.