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