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