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 request/response bodies |
43//! | `schema-validate` | Runtime JSON Schema validation (strict registry: request/response body, query, params) |
44//! | `miette` | [`DiagnosticError`](crate::miette_diagnostic::DiagnosticError) for labeled error reports |
45//! | `otel` | `opentelemetry`, `opentelemetry_sdk`, `tracing_opentelemetry` re-exports |
46//! | `blocking`, `cookies` | Passed through to reqwest |
47//! | `macros` | `#[derive(Endpoint)]`, `EndpointParamsDerive`, `EndpointQueryDerive` |
48//! | `full` | Common optional features bundled for internal apps |
49//!
50//! See the [repository README](https://github.com/sebasxsala/better-fetch-rs) for full examples.
51//!
52//! ## Example (`.get()` — flexible path, typed response)
53//!
54//! ```no_run
55//! # use better_fetch::{Client, Result};
56//! # use serde::Deserialize;
57//! # #[derive(Debug, Deserialize)]
58//! # #[serde(rename_all = "camelCase")]
59//! # struct Todo { user_id: u64, id: u64, title: String, completed: bool }
60//! # #[tokio::main]
61//! # async fn main() -> Result<()> {
62//! let client = Client::new("https://jsonplaceholder.typicode.com")?;
63//!
64//! // send() returns Response for any status; json() fails on non-2xx
65//! let todo: Todo = client
66//! .get("/todos/:id")
67//! .param("id", 1)
68//! .send()
69//! .await?
70//! .json()
71//! .await?;
72//!
73//! // Or in one step:
74//! let todo: Todo = client.get("/todos/:id").param("id", 1).send_json().await?;
75//! # Ok(())
76//! # }
77//! ```
78//!
79//! ## Example (typed endpoint — method, path, params, response)
80//!
81//! ```no_run
82//! # use better_fetch::{Client, Endpoint, Result, define_params};
83//! # use http::Method;
84//! # use serde::Deserialize;
85//! define_params!(GetTodoParams for "/todos/:id" { id: u64 });
86//!
87//! struct GetTodo;
88//! impl Endpoint for GetTodo {
89//! const METHOD: Method = Method::GET;
90//! const PATH: &'static str = "/todos/:id";
91//! type Response = Todo;
92//! type Params = GetTodoParams;
93//! type Query = ();
94//! type Body = ();
95//! type Headers = ();
96//! }
97//!
98//! # #[derive(Deserialize)]
99//! # struct Todo { id: u64, title: String }
100//! # #[tokio::main]
101//! # async fn main() -> Result<()> {
102//! let client = Client::new("https://jsonplaceholder.typicode.com")?;
103//! let todo = client
104//! .call::<GetTodo>()
105//! .params(GetTodoParams { id: 1 })
106//! .send_json()
107//! .await?;
108//! # Ok(())
109//! # }
110//! ```
111
112mod path_params;
113mod url_build;
114
115pub mod api_response;
116pub mod prelude;
117pub mod sse;
118
119pub mod auth;
120pub mod backend;
121pub mod cancel;
122pub mod client;
123mod client_builder;
124pub mod endpoint;
125pub mod error;
126pub mod hooks;
127#[cfg(feature = "json")]
128mod json_parser;
129mod request_pipeline;
130
131pub mod plugin;
132pub mod plugins;
133pub mod request;
134pub mod response;
135pub mod retry;
136pub mod streaming;
137#[cfg(feature = "validate")]
138mod validate_json;
139
140#[cfg(feature = "schema")]
141pub mod schema;
142#[cfg(feature = "schema-validate")]
143pub mod schema_validate;
144
145#[cfg(feature = "miette")]
146pub mod miette_diagnostic;
147
148#[cfg(feature = "otel")]
149pub mod otel;
150
151#[cfg(feature = "openapi")]
152pub mod openapi;
153
154#[cfg(feature = "tower")]
155pub mod tower;
156
157#[cfg(feature = "json")]
158pub use api_response::{into_api_result, ApiResponseExt};
159pub use auth::{AsyncTokenProvider, Auth, TokenSource};
160pub use backend::{
161 HttpBackend, HttpBody, HttpRequest, HttpResponse, HttpStreamingResponse, RecordedBodyKind,
162 RecordedRequest, RecordingBackend, ReqwestBackend,
163};
164pub use cancel::CancellationToken;
165pub use client::{Client, ClientBuilder, ClientConfig};
166pub use endpoint::{
167 DefaultParamsInitial, Endpoint, EndpointBody, EndpointHeaders, EndpointParams,
168 EndpointParamsInitial, EndpointQuery, EndpointRequestBuilder, NeedsBody, NeedsParams,
169 ParamsBuilderState, Ready,
170};
171
172#[cfg(feature = "macros")]
173pub use better_fetch_macros::{
174 Endpoint as EndpointDerive, EndpointParams as EndpointParamsDerive,
175 EndpointQuery as EndpointQueryDerive,
176};
177pub use error::{Error, TransportKind};
178pub use hooks::{
179 ErrorContext, Hooks, RequestContext, ResponseContext, StreamingResponseContext,
180 StreamingResponseMeta, StreamingSuccessContext, SuccessContext,
181};
182#[cfg(feature = "json")]
183pub use json_parser::{json_parser, serde_json_parser, JsonParserFn};
184pub use plugin::{Plugin, PluginRegistry, PreparedRequest};
185pub use plugins::LoggerPlugin;
186pub use request::RequestBuilder;
187#[cfg(feature = "multipart")]
188/// Re-export of [reqwest multipart](https://docs.rs/reqwest/latest/reqwest/multipart/) types (feature `multipart`).
189pub use reqwest::multipart;
190pub use response::{Response, ResponseBodyKind};
191pub use retry::{default_should_retry, parse_retry_after, RetryPolicy, ShouldRetryFn};
192#[cfg(feature = "schema")]
193pub use schema::{EndpointSchema, SchemaRegistry};
194pub use sse::{parse_sse_events, SseDecoder, SseEvent, SseEventStream};
195pub use streaming::{BodyStream, StreamingResponse};
196pub use url_build::{path_param_names, QueryValue};
197
198#[cfg(feature = "openapi")]
199pub use openapi::{
200 OpenApiBuilder, OpenApiComponents, OpenApiDocument, OpenApiInfo, OpenApiOperation,
201 OpenApiSchemaRef, OpenApiServer,
202};
203
204#[cfg(feature = "tower")]
205pub use tower::{
206 BoxHttpService, BoxStreamingHttpService, ReqwestHttpService, ReqwestStreamingHttpService,
207 ServiceBackend,
208};
209
210#[cfg(feature = "miette")]
211pub use miette_diagnostic::DiagnosticError;
212
213#[cfg(feature = "otel")]
214pub use otel::{opentelemetry, opentelemetry_sdk, tracing_opentelemetry};
215
216/// Result alias using [`Error`].
217pub type Result<T> = std::result::Result<T, Error>;