Skip to main content

better_fetch/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2
3//! # better-fetch
4//!
5//! Typed HTTP client layer on top of [reqwest](https://docs.rs/reqwest), inspired by
6//! [@better-fetch/fetch](https://better-fetch.vercel.app/docs). This crate is not affiliated
7//! with the upstream TypeScript project.
8//!
9//! ## Quick flow
10//!
11//! 1. Create a [`Client`] (or [`ClientBuilder`]) with a base URL.
12//! 2. Start a request with [`Client::get`] / [`Client::post`] (flexible [`RequestBuilder`])
13//!    or [`Client::call`] (typed [`Endpoint`] routes).
14//! 3. Configure path params, query, body, auth, retries on the builder.
15//! 4. Execute with [`RequestBuilder::send`] (buffered [`Response`]),
16//!    [`RequestBuilder::send_stream`] (incremental [`StreamingResponse`]),
17//!    [`send_json`](RequestBuilder::send_json), or [`EndpointRequestBuilder::send_json`](EndpointRequestBuilder::send_json).
18//!
19//! ## Buffered vs streaming
20//!
21//! - **`send` / `send_json`** — full body in memory; hooks and retry predicates can read the body.
22//! - **`send_stream`** — `bytes_stream()` from reqwest; use [`StreamingResponse::collect`] to buffer when needed.
23//!   See the [`streaming`] module for limits (hooks, custom retry predicates, Tower backend).
24//!
25//! Use [`.get()`](Client::get) when you want string paths and a typed JSON response (`send_json::<T>()`).
26//! Use [`Client::call`] when method, path, params, query, and response are bound to an [`Endpoint`] type.
27//!
28//! ## Cargo features
29//!
30//! The client always uses [reqwest](https://docs.rs/reqwest) as the default HTTP backend.
31//! Enable crate features to turn on reqwest capabilities and optional APIs.
32//!
33//! | Feature | Description |
34//! |---------|-------------|
35//! | `json` (default) | JSON bodies, `send_json`, custom [`JsonParserFn`] |
36//! | `rustls-tls` (default) | TLS via rustls (enable `native-tls` instead, not both) |
37//! | `native-tls` | TLS via the platform stack (do not combine with `rustls-tls`) |
38//! | `multipart` | [`RequestBuilder::multipart`] |
39//! | `tower` | Tower transport stack via [`ClientBuilder::transport_stack`] (implies `rustls-tls`) |
40//! | `schema` | [`SchemaRegistry`] route metadata |
41//! | `openapi` | OpenAPI 3.0 export from schema registry |
42//! | `validate` | Garde validation on JSON responses |
43//! | `blocking`, `cookies` | Passed through to reqwest |
44//! | `macros` | `#[derive(EndpointParamsDerive)]`, `#[derive(EndpointQueryDerive)]` proc-macros |
45//!
46//! See the [repository README](https://github.com/sebasxsala/better-fetch-rs) for full examples.
47//!
48//! ## Example (`.get()` — flexible path, typed response)
49//!
50//! ```no_run
51//! # use better_fetch::{Client, Result};
52//! # use serde::Deserialize;
53//! # #[derive(Debug, Deserialize)]
54//! # #[serde(rename_all = "camelCase")]
55//! # struct Todo { user_id: u64, id: u64, title: String, completed: bool }
56//! # #[tokio::main]
57//! # async fn main() -> Result<()> {
58//! let client = Client::new("https://jsonplaceholder.typicode.com")?;
59//!
60//! // send() returns Response for any status; json() fails on non-2xx
61//! let todo: Todo = client
62//!     .get("/todos/:id")
63//!     .param("id", 1)
64//!     .send()
65//!     .await?
66//!     .json()
67//!     .await?;
68//!
69//! // Or in one step:
70//! let todo: Todo = client.get("/todos/:id").param("id", 1).send_json().await?;
71//! # Ok(())
72//! # }
73//! ```
74//!
75//! ## Example (typed endpoint — method, path, params, response)
76//!
77//! ```no_run
78//! # use better_fetch::{Client, Endpoint, Result, define_params};
79//! # use http::Method;
80//! # use serde::Deserialize;
81//! define_params!(GetTodoParams for "/todos/:id" { id: u64 });
82//!
83//! struct GetTodo;
84//! impl Endpoint for GetTodo {
85//!     const METHOD: Method = Method::GET;
86//!     const PATH: &'static str = "/todos/:id";
87//!     type Response = Todo;
88//!     type Params = GetTodoParams;
89//!     type Query = ();
90//! }
91//!
92//! # #[derive(Deserialize)]
93//! # struct Todo { id: u64, title: String }
94//! # #[tokio::main]
95//! # async fn main() -> Result<()> {
96//! let client = Client::new("https://jsonplaceholder.typicode.com")?;
97//! let todo = client
98//!     .call::<GetTodo>()
99//!     .params(GetTodoParams { id: 1 })
100//!     .send_json()
101//!     .await?;
102//! # Ok(())
103//! # }
104//! ```
105
106mod url_build;
107
108pub mod auth;
109pub mod backend;
110pub mod cancel;
111pub mod client;
112pub mod endpoint;
113pub mod error;
114pub mod hooks;
115#[cfg(feature = "json")]
116mod json_parser;
117
118pub mod plugin;
119pub mod plugins;
120pub mod request;
121pub mod response;
122pub mod retry;
123pub mod streaming;
124#[cfg(feature = "validate")]
125mod validate_json;
126
127#[cfg(feature = "schema")]
128pub mod schema;
129
130#[cfg(feature = "openapi")]
131pub mod openapi;
132
133#[cfg(feature = "tower")]
134pub mod tower;
135
136pub use auth::{AsyncTokenProvider, Auth, TokenSource};
137pub use backend::{
138    HttpBackend, HttpBody, HttpRequest, HttpResponse, HttpStreamingResponse, ReqwestBackend,
139};
140pub use cancel::CancellationToken;
141pub use client::{Client, ClientBuilder, ClientConfig};
142pub use endpoint::{
143    Endpoint, EndpointParams, EndpointParamsInitial, EndpointQuery, EndpointRequestBuilder,
144    NeedsParams, ParamsBuilderState, Ready,
145};
146
147#[cfg(feature = "macros")]
148pub use better_fetch_macros::{
149    EndpointParams as EndpointParamsDerive, EndpointQuery as EndpointQueryDerive,
150};
151pub use error::{Error, TransportKind};
152pub use hooks::{
153    ErrorContext, Hooks, RequestContext, ResponseContext, StreamingResponseContext,
154    StreamingResponseMeta, StreamingSuccessContext, SuccessContext,
155};
156#[cfg(feature = "json")]
157pub use json_parser::{json_parser, serde_json_parser, JsonParserFn};
158pub use plugin::{Plugin, PluginRegistry, PreparedRequest};
159pub use plugins::LoggerPlugin;
160pub use request::RequestBuilder;
161#[cfg(feature = "multipart")]
162/// Re-export of [reqwest multipart](https://docs.rs/reqwest/latest/reqwest/multipart/) types (feature `multipart`).
163pub use reqwest::multipart;
164pub use response::Response;
165pub use retry::{default_should_retry, parse_retry_after, RetryPolicy, ShouldRetryFn};
166pub use streaming::{BodyStream, StreamingResponse};
167pub use url_build::QueryValue;
168
169#[cfg(feature = "schema")]
170pub use schema::{EndpointSchema, SchemaRegistry};
171
172#[cfg(feature = "openapi")]
173pub use openapi::{
174    OpenApiBuilder, OpenApiComponents, OpenApiDocument, OpenApiInfo, OpenApiOperation,
175    OpenApiSchemaRef, OpenApiServer,
176};
177
178#[cfg(feature = "tower")]
179pub use tower::{BoxHttpService, ReqwestHttpService, ServiceBackend};
180
181/// Result alias using [`Error`].
182pub type Result<T> = std::result::Result<T, Error>;