npm explorer

wretch

v3.0.6TypeScript

A tiny wrapper built around fetch with an intuitive syntax.

wretchfetchajaxrequest
0/weekUpdated 1 months agoMITUnpacked: 927.8 KB
Published by Julien Elbaz
npm install wretch
RepositoryHomepagenpm


wretch-logo



Wretch



github-badge
npm-badge
npm-downloads-badge
Coverage Status

license-badge



A tiny (~1.8KB g-zipped) wrapper built around fetch with an intuitive syntax.



f[ETCH] [WR]apper


##### Wretch 3.0 is now live πŸŽ‰ ! Check out the Migration Guide for upgrading from v2, and please have a look at the releases and the changelog after each update for new features and breaking changes.

##### And if you like the library please consider becoming a sponsor ❀️.

Features

#### wretch is a small wrapper around fetch designed to simplify the way to perform network requests and handle responses.

- πŸͺΆ Small - core is less than 1.8KB g-zipped
- πŸ’‘ Intuitive - lean API, handles errors, headers and (de)serialization
- 🧊 Immutable - every call creates a cloned instance that can then be reused safely
- πŸ”Œ Modular - plug addons to add new features, and middlewares to intercept requests
- 🧩 Isomorphic - compatible with modern browsers, Node.js 22+, Deno and Bun
- 🦺 Type safe - strongly typed, written in TypeScript
- βœ… Proven - fully covered by unit tests and widely used
- πŸ’“ Maintained - alive and well for many years

Table of Contents

- Quick Start
- Motivation
- Installation
- Compatibility
- Usage
- Recipes
- Api
- Addons
- Middlewares
- Migration Guides
- License

Quick Start

``bash

1️⃣ Install


npm i wretch
`


`javascript
// 2️⃣ Import and create a reusable API client
import wretch from "wretch"

const api = wretch("https://jsonplaceholder.typicode.com")
.options({ mode: "cors" })

// 3️⃣ Make requests with automatic JSON handling
const post = await api.get("/posts/1").json()
console.log(post.title)

// 4️⃣ POST with automatic serialization
const created = await api
.post({ title: "New Post", body: "Content", userId: 1 }, "/posts")
.json()

// 5️⃣ Handle errors elegantly
await api
.get("/posts/999")
.notFound(() => console.log("Post not found!"))
.json()

// 6️⃣ Different response types
const text = await api.get("/posts/1").text() // Raw text
const response = await api.get("/posts/1").res() // Raw Response object
const blob = await api.get("/photos/1").blob() // Binary data
`

Motivation

#### Because having to write a second callback to process a response body feels awkward.

Fetch needs a second callback to process the response body.


`javascript
fetch("https://jsonplaceholder.typicode.com/posts/1")
.then(response => response.json())
.then(json => {
//Do stuff with the parsed json
});
`

Wretch does it for you.


`javascript
// Use .res for the raw response, .text for raw text, .json for json, .blob for a blob …
wretch("https://jsonplaceholder.typicode.com/posts/1")
.get()
.json(json => {
// Do stuff with the parsed json
return json
});
`

#### Because manually checking and throwing every request error code is tedious.

Fetch won't reject on HTTP error status.


`javascript
fetch("https://jsonplaceholder.typicode.com/posts/1")
.then(response => {
if(!response.ok) {
if(response.status === 404) throw new Error("Not found")
else if(response.status === 401) throw new Error("Unauthorized")
else if(response.status === 418) throw new Error("I'm a teapot !")
else throw new Error("Other error")
}
else {/ … /}
})
.then(data => {/ … /})
.catch(error => { / … / })
`

Wretch throws when the response is not successful and contains helper methods to handle common codes.


`javascript
wretch("https://jsonplaceholder.typicode.com/posts/1")
.get()
.notFound(error => { / … / })
.unauthorized(error => { / … / })
.error(418, error => { / … / })
.res(response => {/ … / })
.catch(error => { / uncaught errors / })
`

#### Because sending a json object should be easy.

With fetch you have to set the header, the method and the body manually.


`javascript
fetch("https://jsonplaceholder.typicode.com/posts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ "hello": "world" })
}).then(response => {/ … /})
// Omitting the data retrieval and error management parts…
`

With wretch, you have shorthands at your disposal.


`javascript
wretch("https://jsonplaceholder.typicode.com/posts")
.post({ "hello": "world" })
.res(response => { / … / })
`

#### Because configuration should not rhyme with repetition.

A Wretch object is immutable which means that you can reuse previous instances safely.


`javascript
const token = "MY_SECRET_TOKEN"

// Cross origin authenticated requests on an external API
const externalApi = wretch("https://jsonplaceholder.typicode.com") // Base url
// Authorization header
.auth(
Bearer ${token})
// Cors fetch options
.options({ credentials: "include", mode: "cors" })
// Handle 403 errors
.resolve((w) => w.forbidden(error => { / Handle all 403 errors / }));

// Fetch a resource
const resource = await externalApi
// Add a custom header for this request
.headers({ "If-Unmodified-Since": "Wed, 21 Oct 2015 07:28:00 GMT" })
.get("/posts/1")
.json(() => {/ do something with the resource /});

// Post a resource
externalApi
.url("/posts")
.post({ "Shiny new": "resource object" })
.json(() => {/ do something with the created resource /});
`

Installation

Package Manager

`sh
npm i wretch # or yarn/pnpm add wretch
`

<script> tag

The package contains multiple bundles depending on the format and feature set located under the /dist/bundle folder.

Bundle variants

> πŸ’‘ If you pick the core bundle, then to plug addons you must import them separately from /dist/bundle/addons/[addonName].min.js

| Feature set | File Name |
| ------------------ | ------------------- |
| Core features only |
wretch.min.js |
| Core + all addons |
wretch.all.min.js |

| Format | Extension |
| -------- | ---------- |
| ESM |
.min.mjs |
| CommonJS |
.min.cjs |
| UMD |
.min.js |

`html




`

Compatibility

Browsers

wretch@^3 is compatible with modern browsers only. For older browsers please use wretch@^1.

Node.js

Wretch is compatible with and tested in _Node.js >= 22_.

For older Node.js versions, please use wretch@^2.

Node.js 22+ includes native fetch support and all required Web APIs (FormData, URLSearchParams, AbortController, etc.) out of the box, so no polyfills are needed.

Deno

Works with Deno out of the box.

`bash
deno add npm:wretch
`


`ts
import wretch from "wretch";

const text = await wretch("https://httpbingo.org").get("/status/200").text();
console.log(text); // -> { "code": 200, "description": "OK" }
`

Bun

Works with Bun out of the box.

`bash
bun add wretch
`


`ts
import wretch from "wretch";

const text = await wretch("https://httpbingo.org").get("/status/200").text();
console.log(text); // -> { "code": 200, "description": "OK" }
`

Usage

Import


`typescript
// ECMAScript modules
import wretch from "wretch"
// CommonJS
const wretch = require("wretch")
// Global variable (script tag)
window.wretch
`

Common Use Cases


`javascript
// 🌐 REST API Client
const api = wretch("https://jsonplaceholder.typicode.com")
.auth("Bearer token")
.resolve(r => r.json())

const users = await api.get("/users")
const user = await api.post({ name: "John" }, "/users")
users
`


`javascript
// πŸ“€ File Upload with Progress
import ProgressAddon from "wretch/addons/progress"
import FormDataAddon from "wretch/addons/formData"

await wretch("https://httpbingo.org/post")
.addon([FormDataAddon, ProgressAddon()])
.formData({ file: file })
.post()
.progress((loaded, total) => console.log(
${(loaded/total*100).toFixed()}%))
.json()
`


`javascript
// πŸ”„ Auto-retry on Network Failure
import { retry } from "wretch/middlewares"

const resilientApi = wretch()
.middlewares([retry({ maxAttempts: 3, retryOnNetworkError: true })])
`


`typescript
// 🎯 Type-safe TypeScript
interface User { id: number; name: string; email: string }

const user = await wretch("https://jsonplaceholder.typicode.com")
.get("/users/1")
.json() // Fully typed!

user
`


`javascript
// πŸ” Automatic Token Refresh
const api = wretch("https://httpbingo.org/basic-auth/user/pass")
.addon(BasicAuthAddon)
.resolve(w => w.unauthorized(async (error, req) => {
const newToken = await refreshToken()
return req
.basicAuth("user", "pass")
.unauthorized(e => {
console.log("Still unauthorized after token refresh");
throw e
})
.fetch()
.json()
}))
`

Custom Fetch Implementation

You can provide a custom fetch implementation using the .fetchPolyfill() method. This is useful for for a variety of use cases including mocking, adding logging, timing, or other custom behavior to all requests made through a wretch instance.


`js
import wretch from "wretch"

// Per-instance custom fetch
const api = wretch("https://jsonplaceholder.typicode.com")
.fetchPolyfill((url, opts) => {
console.log('Fetching:', url)
console.time(url)
return fetch(url, opts).finally(() => {
console.timeEnd(url)
})
})

await api.get("/posts").json()
`

$3

Converts a wretch instance into a fetch-like function, preserving all configuration (middlewares, catchers, headers, etc.). Useful for integrating wretch with libraries that expect a fetch signature.


`js
const myFetch = wretch("https://jsonplaceholder.typicode.com")
.auth("Bearer token")
.catcher(401, (err) => console.log("Unauthorized"))
.toFetch()

// Use like regular fetch
const response = await myFetch("/users", { method: "GET" })
response
`


`js
// Pass to libraries expecting fetch
import { createClient } from "@apollo/client"

const client = createClient({
fetch: wretch().auth("Bearer token").toFetch()
})
`

Chaining

A high level overview of the successive steps that can be chained to perform a request and parse the result.

`
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Request Chain β”‚
β”‚ β”‚
β”‚ wretch(url) ──> .helper() ──> .body() ──> .httpMethod() β”‚
β”‚ ↓ ↓ ↓ β”‚
β”‚ .headers() .json() .get() β”‚
β”‚ .auth() .body() .post() β”‚
β”‚ .options() .put() β”‚
β”‚ .delete() β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
↓
[ fetch() is called ]
↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Response Chain β”‚
β”‚ β”‚
β”‚ .catcher() ──> .responseType() ──> Promise ──> .then()/.catch()β”‚
β”‚ ↓ ↓ β”‚
β”‚ .notFound() .json() β”‚
β”‚ .unauthorized() .text() β”‚
β”‚ .error() .blob() β”‚
β”‚ .res() β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
`

Step-by-step breakdown:


`ts
// First, instantiate wretch
wretch(baseUrl, baseOptions)
`

_The "request" chain starts here._


`ts
// Optional - A set of helper methods to set the default options, set accept header, change the current url…
.()
// Optional - Serialize an object to json or FormData formats and sets the body & header field if needed
.()
// Required - Sends the get/put/post/delete/patch request.
.()
`

_The "response" chain starts here._

_Fetch is called after the request chain ends and before the response chain starts._

_The request is on the fly and now it is time to chain catchers and finally call a response type handler._


`ts
// Optional - You can chain error handlers here
.()
// Required - Specify the data type you need, which will be parsed and handed to you
.()
// >> Ends the response chain.
`

_From this point on, wretch returns a standard Promise._


`ts
.then(…)
.catch(…)
`

$3

Here's how the chaining works in practice:


`js
await wretch("https://api.example.com") // Base URL
.headers({ "X-Api-Key": "secret" }) // Helper method
.query({ limit: 10 }) // Helper method
.json({ name: "Alice", role: "admin" }) // Body type
.post("/users") // HTTP method (starts request)
.badRequest(err => console.log("Invalid")) // Catcher
.unauthorized(err => console.log("No auth")) // Catcher
.json(user => console.log(user)) // Response type
`

Recipes

Looking for common patterns and solutions? Check out the Recipes Guide for practical examples covering:

- Error Handling - Parsing error response bodies, custom error types, global handlers
- TypeScript Patterns - Typing precomposed instances, reusable catchers
- File Uploads - Progress tracking, FormData handling
- Query Strings - Filtering undefined values
- Request Control - Combining timeouts with AbortControllers, aborting on errors
- Advanced Patterns - Token refresh & replay, schema validation, async polling

API πŸ”—

> πŸ’‘ The API documentation is now autogenerated and hosted separately, click the links access it.

$3

The default export is a factory function used to instantiate wretch.


`js
import wretch from "wretch"

const api = wretch("http://domain.com/", { cache: "default" })
`

$3

Helper Methods are used to configure the request and program actions.

Available methods: .url() Β· .options() Β· .headers() Β· .auth() Β· .accept() Β· .content() Β· .signal() Β· .toFetch() Β· .fetchPolyfill() Β· .catcher() Β· .catcherFallback() Β· .customError() Β· .defer() Β· .resolve() Β· .middlewares() Β· .addon() Β· .polyfills()


`js
let api = wretch("http://domain.com/")

api = api
.url("/posts/1")
.headers({ "Cache-Control": "no-cache" })
.content("text/html")
`

$3

Specify a body type if uploading data. Can also be added through the HTTP Method argument.

Available methods: .body() Β· .json()


`js
let api = wretch("http://domain.com/")

api = api.body("

")
api
`

$3

Sets the HTTP method and sends the request.

Calling an HTTP method ends the request chain and returns a response chain.
You can pass optional url and body arguments to these methods.

Available methods: .get() Β· .post() Β· .put() Β· .patch() Β· .delete() Β· .head() Β· .opts()


`js
const api = wretch("http://jsonplaceholder.typicode.com")

// These shorthands:
api.get("/posts");
api.post({ json: "body" }, "/posts");
// Are equivalent to:
api.url("/posts").get();
api.json({ json: "body" }).url("/posts").post();
`

NOTE: if the body argument is an Object it is assumed that it is a JSON payload and it will have the same behaviour as calling .json(body) unless the Content-Type header has been set to something else beforehand.

$3

Catchers are optional, but if none are provided an error will still be thrown for http error codes and it will be up to you to catch it.

Available methods: .badRequest() Β· .unauthorized() Β· .forbidden() Β· .notFound() Β· .timeout() Β· .internalError() Β· .error() Β· .fetchError()


`js
wretch("http://domain.com/resource")
.get()
.badRequest((err) => console.log(err.status))
.unauthorized((err) => console.log(err.status))
.forbidden((err) => console.log(err.status))
.notFound((err) => console.log(err.status))
.timeout((err) => console.log(err.status))
.internalError((err) => console.log(err.status))
.error(418, (err) => console.log(err.status))
.fetchError((err) => console.log(err))
.res();
`

The error passed to catchers is enhanced with additional properties.


`ts
type WretchError = Error & {
status: number;
response: WretchResponse;
url: string;
};
`

By default, error.message is set to the response body text (or statusText if body parsing fails).

#### Request Replay

The original request is passed along the error and can be used in order to
perform an additional request.


`js
await wretch("https://httpbingo.org/basic-auth/user/pass")
.addon(BasicAuthAddon)
.basicAuth("user", "wrongpass")
.get()
.unauthorized(async (error, req) => {
// Renew credentials
const password = await wretch("https://httpbingo.org/base64/decode/cGFzcw==").get().text();
// Replay the original request with new credentials
return req
.basicAuth("user", password)
.fetch()
.unauthorized((err) => { throw err })
.json();
})
.json()
// The promise chain is preserved as expected
// ".then" will be performed on the result of the original request
// or the replayed one (if a 401 error was thrown)
.then(() => { / … / });
`

$3

Setting the final response body type ends the chain and returns a regular promise.

All these methods accept an optional callback, and will return a Promise
resolved with either the return value of the provided callback or the expected
type.

Available methods: .res() Β· .json() Β· .text() Β· .blob() Β· .arrayBuffer() Β· .formData()


`js
const ENDPOINT = "https://jsonplaceholder.typicode.com/posts/1"

// Without a callback
wretch(ENDPOINT)
.get()
.json()
.then(json => {
/ the json argument is the parsed json of the response body /
})
// Without a callback using await
const json = await wretch(ENDPOINT).get().json()
// With a callback the value returned is passed to the Promise
wretch(ENDPOINT).get().json(json => "Hello world!").then(console.log) // => Hello world!
`

_If an error is caught by catchers, the response type handler will not be
called._

Addons

Addons are separate pieces of code that you can import and plug into wretch to add new features.


`js
import FormDataAddon from "wretch/addons/formData"
import QueryStringAddon from "wretch/addons/queryString"

// Add both addons
const w = wretch().addon([FormDataAddon, QueryStringAddon])

// Additional features are now available
w.formData({ hello: "world" }).query({ check: true })
`

Typescript should also be fully supported and will provide completions.

https://user-images.githubusercontent.com/3428394/182319457-504a0856-abdd-4c1d-bd04-df5a061e515d.mov

$3

Used to construct and append the query string part of the URL from an object.


`js
import QueryStringAddon from "wretch/addons/queryString"

let w = wretch("http://example.com").addon(QueryStringAddon);
// url is http://example.com
w = w.query({ a: 1, b: 2 });
// url is now http://example.com?a=1&b=2
w = w.query({ c: 3, d: [4, 5] });
// url is now http://example.com?a=1&b=2c=3&d=4&d=5
w = w.query("five&six&seven=eight");
// url is now http://example.com?a=1&b=2c=3&d=4&d=5&five&six&seven=eight
w = w.query({ reset: true }, { replace: true });
// url is now http://example.com?reset=true
`

$3

Adds a helper method to serialize a multipart/form-data body from an object.


`js
import FormDataAddon from "wretch/addons/formData"

const form = {
duck: "Muscovy",
duckProperties: {
beak: {
color: "yellow",
},
legs: 2,
},
ignored: {
key: 0,
},
};

// Will append the following keys to the FormData payload:
// "duck", "duckProperties[beak][color]", "duckProperties[legs]"
wretch("https://httpbingo.org/post").addon(FormDataAddon).formData(form, { recursive: ["ignored"] }).post();
`

$3

Adds a method to serialize a application/x-www-form-urlencoded body from an object.


`js
import FormUrlAddon from "wretch/addons/formUrl"

const form = { a: 1, b: { c: 2 } };
const alreadyEncodedForm = "a=1&b=%7B%22c%22%3A2%7D";

// Automatically sets the content-type header to "application/x-www-form-urlencoded"
wretch("https://httpbingo.org/post").addon(FormUrlAddon).formUrl(form).post();
wretch("https://httpbingo.org/post").addon(FormUrlAddon).formUrl(alreadyEncodedForm).post();
`

$3

Adds the ability to abort requests and set timeouts using AbortController and signals under the hood.


`js
import AbortAddon from "wretch/addons/abort"
`

Use cases :


`js
const [c, w] = wretch("https://httpbingo.org/get")
.addon(AbortAddon())
.get()
.onAbort((_) => console.log("Aborted !"))
.controller();

w.text((_) => console.log("should never be called"));
c.abort();

// Or :

const controller = new AbortController();

wretch("https://httpbingo.org/get")
.addon(AbortAddon())
.signal(controller)
.get()
.onAbort((_) => console.log("Aborted !"))
.text((_) => console.log("should never be called"));

controller.abort();
`


`js
await wretch("https://httpbingo.org/delay/2")
.addon(AbortAddon())
.get()
// 1 second timeout
.setTimeout(1000)
.onAbort(_ => {
console.log("Request timed out")
})
.json(_ => {
console.log("Response received in time")
})
`

$3

Adds the ability to set the Authorization header for the basic authentication scheme without the need to manually encode the username/password.

Also, allows using URLs with wretch that contain credentials, which would otherwise throw an error.


`js
import BasicAuthAddon from "wretch/addons/basicAuth"

const user = "user"
const pass = "pass"

// Automatically sets the Authorization header to "Basic " +
wretch("https://httpbingo.org/get")
.addon(BasicAuthAddon)
.basicAuth(user, pass)
.get()

// Allows using URLs with credentials in them
wretch(
https://${user}:${pass}@httpbingo.org/basic-auth/${user}/${pass})
.addon(BasicAuthAddon)
.get()
`

$3

Adds the ability to monitor progress when downloading or uploading.

_Compatible with all platforms implementing the TransformStream WebAPI._

Download progress:


`js
import ProgressAddon from "wretch/addons/progress"

await wretch("https://httpbingo.org/bytes/5000")
.addon(ProgressAddon())
.get()
// Called with the number of bytes loaded and the total number of bytes to load
.progress((loaded, total) => {
console.log(
Download: ${(loaded / total * 100).toFixed(0)}%)
})
.blob()
`

Upload progress:


`js
import ProgressAddon from "wretch/addons/progress"
import FormDataAddon from "wretch/addons/formData"

const formData = new FormData()
formData.append('file', file)

wretch("https://httpbingo.org/post")
.addon([ProgressAddon(), FormDataAddon])
.onUpload((loaded, total) => {
console.log(
Upload: ${(loaded / total * 100).toFixed(0)}%)
})
.post(formData)
.res()
`

> Note for browsers: Upload progress requires HTTPS (HTTP/2) in Chrome/Chromium and doesn't work in Firefox due to streaming limitations. Works fully in Node.js.

$3

Adds the ability to measure requests using the Performance Timings API.

Uses the Performance API (browsers & Node.js) to expose timings related to the underlying request.

> πŸ’‘ Make sure to follow the additional instructions in the documentation to setup Node.js if necessary.


Middlewares

Middlewares are functions that can intercept requests before being processed by
Fetch. Wretch includes a helper to help replicate the
middleware style.


`js
import wretch from "wretch"
import { retry, dedupe } from "wretch/middlewares"

const w = wretch().middlewares([retry(), dedupe()])
`

> πŸ’‘ The following middlewares were previously provided by the wretch-middlewares package.

$3

Retries a request multiple times in case of an error (or until a custom condition is true).

> πŸ’‘ By default, the request will be retried only for server errors (5xx) and other non-successful responses, but not for client errors (4xx).
>
>
`js
> // To retry on all non-2xx responses (including 4xx):
> until: (response, error) => !!response && response.ok
>
`


`js
import wretch from 'wretch'
import { retry } from 'wretch/middlewares'

wretch().middlewares([
retry({
/ Options - defaults below /
delayTimer: 500,
delayRamp: (delay, nbOfAttempts) => delay * nbOfAttempts,
maxAttempts: 10,
until: (response, error) => !!response && (response.ok || (response.status >= 400 && response.status < 500)),
onRetry: undefined,
retryOnNetworkError: false,
resolveWithLatestResponse: false
})
])

// You can also return a Promise, which is useful if you want to inspect the body:
wretch().middlewares([
retry({
until: response =>
response?.clone().json().then(body =>
body.field === 'something'
) || false
})
])
`

$3

Prevents having multiple identical requests on the fly at the same time.


`js
import wretch from 'wretch'
import { dedupe } from 'wretch/middlewares'

wretch().middlewares([
dedupe({
/ Options - defaults below /
skip: (url, opts) => opts.skipDedupe || opts.method !== 'GET',
key: (url, opts) => opts.method + '@' + url,
resolver: response => response.clone()
})
])
`

$3

A throttling cache which stores and serves server responses for a certain amount of time.


`js
import wretch from 'wretch'
import { throttlingCache } from 'wretch/middlewares'

wretch().middlewares([
throttlingCache({
/ Options - defaults below /
throttle: 1000,
skip: (url, opts) => opts.skipCache || opts.method !== 'GET',
key: (url, opts) => opts.method + '@' + url,
clear: (url, opts) => false,
invalidate: (url, opts) => null,
condition: response => response.ok,
flagResponseOnCacheHit: '__cached'
})
])
`

$3

Delays the request by a specific amount of time.


`js
import wretch from 'wretch'
import { delay } from 'wretch/middlewares'

wretch().middlewares([
delay(1000)
])
`

Writing a Middleware

Basically a Middleware is a function having the following signature :


`ts
// A middleware accepts options and returns a configured version
type Middleware = (options?: { [key: string]: any }) => ConfiguredMiddleware;
// A configured middleware (with options curried)
type ConfiguredMiddleware = (next: FetchLike) => FetchLike;
// A "fetch like" function, accepting an url and fetch options and returning a response promise
type FetchLike = (
url: string,
opts: WretchOptions,
) => Promise;
`

$3

If you need to manipulate data within your middleware and expose it for later
consumption, a solution could be to pass a named property to the wretch options
(_suggested name:
context_).

Your middleware can then take advantage of that by mutating the object
reference.


`js
const contextMiddleware = (next) =>
(url, opts) => {
if (opts.context) {
// Mutate "context"
opts.context.property = "anything";
}
return next(url, opts);
};

// Provide the reference to a "context" object
const context = {};
const res = await wretch("https://httpbingo.org/get")
// Pass "context" by reference as an option
.options({ context })
.middlewares([contextMiddleware])
.get()
.res();

console.log(context.property); // prints "anything"
`

$3


 πŸ‘€ Show me the code


`javascript
/ A simple delay middleware. /
const delayMiddleware = delay => next => (url, opts) => {
return new Promise(res => setTimeout(() => res(next(url, opts)), delay))
}

/ Returns the url and method without performing an actual request. /
const shortCircuitMiddleware = () => next => (url, opts) => {
// We create a new Response object to comply because wretch expects that from fetch.
const response = new Response(url)
// Instead of calling next(), returning a Response Promise bypasses the rest of the chain.
return Promise.resolve(response)
}

/ Logs all requests passing through. /
const logMiddleware = () => next => (url, opts) => {
console.log(opts.method + "@" + url)
return next(url, opts)
}

/ A throttling cache. /
const cacheMiddleware = (throttle = 0) => {

const cache = new Map()
const inflight = new Map()
const throttling = new Set()

return next => (url, opts) => {
const key = opts.method + "@" + url

if(!opts.noCache && throttling.has(key)) {
// If the cache contains a previous response and we are throttling, serve it and bypass the chain.
if(cache.has(key))
return Promise.resolve(cache.get(key).clone())
// If the request in already in-flight, wait until it is resolved
else if(inflight.has(key)) {
return new Promise((resolve, reject) => {
inflight.get(key).push([resolve, reject])
})
}
}

// Init. the pending promises Map
if(!inflight.has(key))
inflight.set(key, [])

// If we are not throttling, activate the throttle for X milliseconds
if(throttle && !throttling.has(key)) {
throttling.add(key)
setTimeout(() => { throttling.delete(key) }, throttle)
}

// We call the next middleware in the chain.
return next(url, opts)
.then(_ => {
// Add a cloned response to the cache
cache.set(key, _.clone())
// Resolve pending promises
inflight.get(key)?.forEach((([resolve, reject]) => resolve(_.clone())))
// Remove the inflight pending promises
inflight.delete(key)
// Return the original response
return _
})
.catch(_ => {
// Reject pending promises on error
inflight.get(key)?.forEach(([resolve, reject]) => reject(_))
inflight.delete(key)
throw _
})
}
}

// To call a single middleware
const cache = cacheMiddleware(1000)
wretch("https://httpbingo.org/get").middlewares([cache]).get()

// To chain middlewares
wretch("https://httpbingo.org/get").middlewares([
logMiddleware(),
delayMiddleware(1000),
shortCircuitMiddleware()
]).get().text(text => console.log(text))

// To test the cache middleware more thoroughly
const wretchCache = wretch("https://httpbingo.org").middlewares([cacheMiddleware(500)])
const printResource = (url, timeout = 0) => {
return new Promise(resolve => setTimeout(async () => {
wretchCache.url(url).get().notFound(console.error).text(resource => {
console.log(resource)
resolve(resource)
})
}, timeout))
}
// The resource url, change it to an invalid route to check the error handling
const resourceUrl = "/base64/decode/YWVhY2YyYWYtODhlNi00ZjgxLWEwYjAtNzdhMTIxNTA0Y2E4"
// Only two actual requests are made here even though there are 30 calls
await Promise.all(Array.from({ length: 10 }).flatMap(() =>
[
printResource(resourceUrl),
printResource(resourceUrl, 200),
printResource(resourceUrl, 700)
]
))
`

Limitations

Cloudflare Workers

It seems like using wretch in a Cloudflare Worker environment is not possible out of the box, as the Cloudflare Response implementation does not implement the type property and throws an error when trying to access it.

#### Please check the issue #159 for more information.

$3

The following middleware should fix the issue (thanks @jimmed πŸ™‡):


`js
wretch().middlewares([
(next) => async (url, opts) => {
const response = await next(url, opts);
try {
Reflect.get(response, "type", response);
} catch (error) {
Object.defineProperty(response, "type", {
get: () => "default",
});
}
return response;
},
])
`

Headers Case Sensitivity

The Request object from the Fetch API uses the Headers class to store headers under the hood.
This class is case-insensitive, meaning that setting both will actually appends the value to the same key:


`js
const headers = new Headers();
headers.append("Accept", "application/json");
headers.append("accept", "application/json");
headers.forEach((value, key) => console.log(key, value));
// prints: accept application/json, application/json
`

When using wretch, please be mindful of this limitation and avoid setting the same header multiple times with a different case:


`js
wretch("https://httpbingo.org/post")
.headers({ "content-type": "application/json" })
// .json is a shortcut for .headers("Content-Type": "application/json").post().json()
.json({ foo: "bar" })
// Wretch stores the headers inside a plain javascript object and will not deduplicate them.
// Later on when fetch builds the Headers object the content type header will be set twice
// and its value will be "application/json, application/json".
// Ultimately this is certainly not what you want.
`

#### Please check the issue #80 for more information.

$3

You can use the following middleware to deduplicate headers (thanks @jimmed πŸ™‡):


`js
export const manipulateHeaders =
callback => next => (url, { headers, ...opts }) => {
const nextHeaders = callback(new Headers(headers))
return next(url, { ...opts, headers: nextHeaders })
}

export const dedupeHeaders = (dedupeHeaderLogic = {}) => {
const deduperMap = new Map(
Object.entries(dedupeHeaderLogic).map(([k, v]) => [k.toLowerCase(), v]),
)
const dedupe = key =>
deduperMap.get(key.toLowerCase()) ?? (values => new Set(values))

return manipulateHeaders((headers) => {
Object.entries(headers.raw()).forEach(([key, values]) => {
const deduped = Array.from(dedupe(key)(values))
headers.delete(key)
deduped.forEach((value, index) =>
headersindex ? 'append' : 'set', value),
)
})
return headers
})
}

// By default, it will deduplicate identical values for a given header. This can be used as follows:
wretch().middlewares([dedupeHeaders()])
// If there is a specific header for which the defaults cause problems, then you can provide a callback to handle deduplication yourself:
wretch().middlewares([
dedupeHeaders({
Accept: (values) => values.filter(v => v !== '/')
})
])
``

Migration Guides

Comprehensive migration guides are available for upgrading between major versions:

- Migration Guide: v2 to v3
- Migration Guide: v1 to v2

License

MIT

wretch - npm explorer

Dist Tags

next2.0.0-next.1
latest3.0.6