Wretch
A tiny wrapper built around fetch with an intuitive syntax.
README
Wretch
A tiny (~1.9KB g-zipped) wrapper built around fetch with an intuitive syntax.
f[ETCH] [WR]apper
Wretch 2.1 is now live 🎉 ! Please have a look at the releases and the changelog after each update for new features and breaking changes. If you want to try out the hot stuff, please look into the dev branch.
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 2KB 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 14+ and Deno- 🦺 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
- [Motivation](#motivation)- [Installation](#installation)- [Compatibility](#compatibility)- [Usage](#usage)- [Api](#api-)- [Addons](#addons)- [Middlewares](#middlewares)- [Migration from v1](#migration-from-v1)- [License](#license)
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.
- ``` js
- fetch("examples/example.json")
- .then(response => response.json())
- .then(json => {
- //Do stuff with the parsed json
- });
- ```
Wretch does it for you.
- ``` js
- // Use .res for the raw response, .text for raw text, .json for json, .blob for a blob ...
- wretch("examples/example.json")
- .get()
- .json(json => {
- // Do stuff with the parsed json
- });
- ```
Because manually checking and throwing every request error code is tedious.
Fetch won’t reject on HTTP error status.
- ``` js
- fetch("anything")
- .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.
- ``` js
- wretch("anything")
- .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.
- ``` js
- fetch("endpoint", {
- 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.
- ``` js
- wretch("endpoint")
- .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.
- ``` js
- // Cross origin authenticated requests on an external API
- const externalApi = wretch("http://external.api") // Base url
- // Authorization header
- .auth(`Bearer ${token}`)
- // Cors fetch options
- .options({ credentials: "include", mode: "cors" })
- // Handle 403 errors
- .resolve((_) => _.forbidden(handle403));
- // 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("/resource/1")
- .json(handleResource);
- // Post a resource
- externalApi
- .url("/resource")
- .post({ "Shiny new": "resource object" })
- .json(handleNewResourceResult);
- ```
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 File ------------------ ------------------- Core `wretch.min.js` Core `wretch.all.min.js`
Format Extension -------- ---------- ESM `.min.mjs` CommonJS `.min.cjs` UMD `.min.js`
- ``` html
- <!--
- Pick your favourite CDN:
- - https://unpkg.com/wretch
- - https://cdn.jsdelivr.net/npm/wretch/
- - https://www.skypack.dev/view/wretch
- - https://cdnjs.com/libraries/wretch
- - …
- -->
- <script src="https://unpkg.com/wretch"></script>
- <script type="module">
- import wretch from 'https://cdn.skypack.dev/wretch/dist/bundle/wretch.all.min.mjs'
- // ... //
- </script>
- ```
Compatibility
Browsers
wretch@^2 is compatible with modern browsers only. For older browsers please use wretch@^1.
Node.js
Wretch is compatible with and tested in _Node.js >= 14_. Older versions of node may workbut it is not guaranteed.
🥳 Starting from Node.js 18, node includes experimental fetch support. Wretch will work without installing any polyfill.
>
Polyfills
Since the Node.js standard library does not provide a native implementation of fetch (and other Browsers-only APIs), polyfilling is mandatory.
_The non-global way (preferred):_
- ``` js
- // w is a reusable wretch instance
- const w = wretch().polyfills({
- fetch: require("node-fetch"),
- FormData: require("form-data"),
- URLSearchParams: require("url").URLSearchParams,
- });
- ```
_Globally:_
- ``` js
- // Either mutate the global object…
- global.fetch = require("node-fetch");
- global.FormData = require("form-data");
- global.URLSearchParams = require("url").URLSearchParams;
- // …or use the static wretch.polyfills method to impact every wretch instance created afterwards.
- wretch.polyfills({
- fetch: require("node-fetch"),
- FormData: require("form-data"),
- URLSearchParams: require("url").URLSearchParams,
- });
- ```
Deno
Works with Deno >=0.41.0 out of the box.
Types should be imported from /dist/types.d.ts.
- ```ts
- // You can import wretch from any CDN that serve ESModules.
- import wretch from "https://cdn.skypack.dev/wretch";
- const text = await wretch("https://httpstat.us").get("/200").text();
- console.log(text); // -> 200 OK
- ```
Usage
Import
- ```typescript
- // ECMAScript modules
- import wretch from "wretch"
- // CommonJS
- const wretch = require("wretch")
- // Global variable (script tag)
- window.wretch
- ```
Minimal Example
- ``` js
- import wretch from "wretch"
- // Instantiate and configure wretch
- const api =
- wretch("https://jsonplaceholder.typicode.com", { mode: "cors" })
- .errorType("json")
- .resolve(r => r.json())
- try {
- // Fetch users
- const users = await api.get("/users")
- // Find all posts from a given user
- const user = users.find(({ name }) => name === "Nicholas Runolfsdottir V")
- const postsByUser = await api.get(`/posts?userId=${user.id}`)
- // Create a new post
- const newPost = await api.url("/posts").post({
- title: "New Post",
- body: "My shiny new post"
- })
- // Patch it
- await api.url("/posts/" + newPost.id).patch({
- title: "Updated Post",
- body: "Edited body"
- })
- // Fetch it
- await api.get("/posts/" + newPost.id)
- } catch (error) {
- // The API could return an empty object - in which case the status text is logged instead.
- const message =
- typeof error.message === "object" && Object.keys(error.message).length > 0
- ? JSON.stringify(error.message)
- : error.response.statusText
- console.error(`${error.status}: ${message}`)
- }
- ```
Chaining
A high level overview of the successive steps that can be chained to perform a request and parse the result.
- ```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…
- .<helper method(s)>()
- // Optional - Serialize an object to json or FormData formats and sets the body & header field if needed
- .<body type>()
- // Required - Sends the get/put/post/delete/patch request.
- .<http method>()
- ```
_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
- .<catcher(s)>()
- // Required - Specify the data type you need, which will be parsed and handed to you
- .<response type>()
- // >> Ends the response chain.
- ```
_From this point on, wretch returns a standard Promise._
- ```ts
- .then(…)
- .catch(…)
- ```
💡 The API documentation is now autogenerated and hosted separately, click the links access it.
These methods are available from the main default export and can be used to instantiate wretch and configure it globally.
- ``` js
- import wretch from "wretch"
- wretch.options({ mode: "cors" })
- let w = wretch("http://domain.com/", { cache: "default" })
- ```
Helper Methods are used to configure the request and program actions.
- ``` js
- w = w
- .url("/resource/1")
- .headers({ "Cache-Control": no-cache })
- .content("text/html")
- ```
Specify a body type if uploading data. Can also be added through the HTTP Method argument.
- ``` js
- w = w.body("<html><body><div/></body></html>")
- ```
Setus 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.
- ``` js
- // These shorthands:
- wretch().get("/url");
- wretch().post({ json: "body" }, "/url");
- // Are equivalent to:
- wretch().url("/url").get();
- wretch().json({ json: "body" }).url("/url").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.
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.
- ``` js
- wretch("...")
- .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;
- text?: string;
- json?: Object;
- };
- ```
The original request is passed along the error and can be used in order toperform an additional request.
- ``` js
- wretch("/resource")
- .get()
- .unauthorized(async (error, req) => {
- // Renew credentials
- const token = await wretch("/renewtoken").get().text();
- storeToken(token);
- // Replay the original request with new credentials
- return req.auth(token).get().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(callback);
- ```
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 Promiseresolved with either the return value of the provided callback or the expectedtype.
- ``` js
- // Without a callback
- wretch("...").get().json().then(json => /* json is the parsed json of the response body */)
- // Without a callback using await
- const json = await wretch("...").get().json()
- // With a callback the value returned is passed to the Promise
- wretch("...").get().json(json => "Hello world!").then(console.log) // => Hello world!
- ```
_If an error is caught by catchers, the response type handler will not becalled._
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).addon(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
Used to construct and append the query string part of the URL from an object.
- ``` js
- import QueryStringAddon from "wretch/addons/queryString"
- ```
Adds a helper method to serialize a multipart/form-data body from an object.
- ``` js
- import FormDataAddon from "wretch/addons/formData"
- ```
Adds a method to serialize a application/x-www-form-urlencoded body from an object.
- ``` js
- import FormUrlAddon from "wretch/addons/formUrl"
- ```
Adds the ability to abort requests and set timeouts using AbortController and signals under the hood.
- ``` js
- import AbortAddon from "wretch/addons/abort"
- ```
_Only compatible with browsers that supportOtherwise, you could use a (partial)polyfill._
Use cases :
- ``` js
- const [c, w] = wretch("...")
- .addon(AbortAddon())
- .get()
- .onAbort((_) => console.log("Aborted !"))
- .controller();
- w.text((_) => console.log("should never be called"));
- c.abort();
- // Or :
- const controller = new AbortController();
- wretch("...")
- .addon(AbortAddon())
- .signal(controller)
- .get()
- .onAbort((_) => console.log("Aborted !"))
- .text((_) => console.log("should never be called"));
- controller.abort();
- ```
- ``` js
- // 1 second timeout
- wretch("...").addon(AbortAddon()).get().setTimeout(1000).json(_ =>
- // will not be called if the request timeouts
- )
- ```
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 byFetch. Wretch includes a helper to help replicate themiddleware 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](https://github.com/elbywan/wretch-middlewares/) package.
Retries a request multiple times in case of an error (or until a custom condition is true).
- ``` 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,
- onRetry: null,
- 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'
- )
- })
- ])
- ```
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()
- })
- ])./* ... */
- ```
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'
- })
- ])./* ... */
- ```
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<WretchResponse>;
- ```
Context
If you need to manipulate data within your middleware and expose it for laterconsumption, 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 objectreference.
- ``` 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("...")
- // Pass "context" by reference as an option
- .options({ context })
- .middlewares([contextMiddleware])
- .get()
- .res();
- console.log(context.property); // prints "anything"
- ```
Advanced examples
👀 Show me the code
- ``` js
- /* 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()
- response.text = () => Promise.resolve(opts.method + "@" + url)
- response.json = () => Promise.resolve({ url, method: opts.method })
- // 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("...").middlewares([cache]).get()
- // To chain middlewares
- wretch("...").middlewares([
- logMiddleware(),
- delayMiddleware(1000),
- shortCircuitMiddleware()
- }).get().text(_ => console.log(text))
- // To test the cache middleware more thoroughly
- const wretchCache = wretch().middlewares([cacheMiddleware(1000)])
- const printResource = (url, timeout = 0) =>
- setTimeout(_ => wretchCache.url(url).get().notFound(console.error).text(console.log), timeout)
- // The resource url, change it to an invalid route to check the error handling
- const resourceUrl = "/"
- // Only two actual requests are made here even though there are 30 calls
- for(let i = 0; i < 10; i++) {
- printResource(resourceUrl)
- printResource(resourceUrl, 500)
- printResource(resourceUrl, 1500)
- }
- ```
Migration from v1
Philosophy
Wretch has been completely rewritten with the following goals in mind:
- reduce its size by making it modular- preserve the typescript type coverage- improve the API by removing several awkward choices
Compatibility
wretch@1 was transpiled to es5, wretch@2 is now transpiled to es2018.Any "modern" browser and Node.js versions >= 14 should parse the library without issues.
If you need compatibility with older browsers/nodejs versions then either stick with v1, use poyfillsor configure @babel to make it transpile wretch.
Addons
Some features that were part of wretch v1 are now split apart and must be imported through addons.It is now needed to pass the Addon to the [.addon](#addonaddon-wretchaddon) method to register it.
Please refer to the Addons documentation.
- ``` js
- /* Previously (wretch@1) */
- import wretch from "wretch"
- wretch.formData({ hello: "world" }).query({ check: true })
- /* Now (wretch@2) */
- import FormDataAddon from "wretch/addons/formData"
- import QueryStringAddon from "wretch/addons/queryString"
- import wretch as baseWretch from "wretch"
- // Add both addons
- const wretch = baseWretch().addon(FormDataAddon).addon(QueryStringAddon)
- // Additional features are now available
- wretch.formData({ hello: "world" }).query({ check: true })
- ```
Typescript
Types have been renamed and refactored, please update your imports accordingly and refer to the typescript api documentation.
API Changes
Replace / Mixin arguments
Some functions used to have a mixin = true argument that could be used to merge the value, others a replace = false argument performing the opposite.In v2 there are only replace = false arguments but the default behaviour should be preserved.
- ``` js
- /* Previously (wretch@1) */
- wretch.options({ credentials: "same-origin" }, false) // false: do not merge the value
- wretch.options({ credentials: "same-origin" }) // Default behaviour stays the same
- /* Now (wretch@2) */
- wretch.options({ credentials: "same-origin" }, true) // true: replace the existing value
- wretch.options({ credentials: "same-origin" }) // Default behaviour stays the same
- ```
HTTP methods extra argument
In v1 it was possible to set fetch options while calling the http methods to end the request chain.
- ``` js
- /* Previously (wretch@1) */
- wretch("...").get({ my: "option" })
- ```
This was a rarely used feature and the extra argument now appends a string to the base url.
- ``` js
- /* Now (wretch@2) */
- wretch("https://base.com").get("/resource/1")
- ```
Replay function
The .replay function has been renamed to [.fetch](https://elbywan.github.io/wretch/api/interfaces/index.Wretch.html#fetch).
License
MIT
Wretch 2.1 is now live 🎉 ! Please have a look at the releases and the changelog after each update for new features and breaking changes. If you want to try out the hot stuff, please look into the dev branch.
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.
Table of Contents
Motivation
Because having to write a second callback to process a response body feels awkward.
- ``` js
- fetch("examples/example.json")
- .then(response => response.json())
- .then(json => {
- //Do stuff with the parsed json
- });
- ```
- ``` js
- // Use .res for the raw response, .text for raw text, .json for json, .blob for a blob ...
- wretch("examples/example.json")
- .get()
- .json(json => {
- // Do stuff with the parsed json
- });
- ```
Because manually checking and throwing every request error code is tedious.
- ``` js
- fetch("anything")
- .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 => { /* ... */ })
- ```
- ``` js
- wretch("anything")
- .get()
- .notFound(error => { /* ... */ })
- .unauthorized(error => { /* ... */ })
- .error(418, error => { /* ... */ })
- .res(response => /* ... */)
- .catch(error => { /* uncaught errors */ })
- ```
Because sending a json object should be easy.
- ``` js
- fetch("endpoint", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ "hello": "world" })
- }).then(response => /* ... */)
- // Omitting the data retrieval and error management parts…
- ```
- ``` js
- wretch("endpoint")
- .post({ "hello": "world" })
- .res(response => /* ... */)
- ```
Because configuration should not rhyme with repetition.
- ``` js
- // Cross origin authenticated requests on an external API
- const externalApi = wretch("http://external.api") // Base url
- // Authorization header
- .auth(`Bearer ${token}`)
- // Cors fetch options
- .options({ credentials: "include", mode: "cors" })
- // Handle 403 errors
- .resolve((_) => _.forbidden(handle403));
- // 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("/resource/1")
- .json(handleResource);
- // Post a resource
- externalApi
- .url("/resource")
- .post({ "Shiny new": "resource object" })
- .json(handleNewResourceResult);
- ```
Installation
Package Manager
- ```sh
- npm i wretch # or yarn/pnpm add wretch
- ```
<script> tag
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 | File |
---|---|
------------------ | ------------------- |
Core | `wretch.min.js` |
Core | `wretch.all.min.js` |
Format | Extension |
---|---|
-------- | ---------- |
ESM | `.min.mjs` |
CommonJS | `.min.cjs` |
UMD | `.min.js` |
- ``` html
- <!--
- Pick your favourite CDN:
- - https://unpkg.com/wretch
- - https://cdn.jsdelivr.net/npm/wretch/
- - https://www.skypack.dev/view/wretch
- - https://cdnjs.com/libraries/wretch
- - …
- -->
- <script src="https://unpkg.com/wretch"></script>
- <script type="module">
- import wretch from 'https://cdn.skypack.dev/wretch/dist/bundle/wretch.all.min.mjs'
- // ... //
- </script>
- ```
Compatibility
Browsers
Node.js
🥳 Starting from Node.js 18, node includes experimental fetch support. Wretch will work without installing any polyfill.
Polyfills
- ``` js
- // w is a reusable wretch instance
- const w = wretch().polyfills({
- fetch: require("node-fetch"),
- FormData: require("form-data"),
- URLSearchParams: require("url").URLSearchParams,
- });
- ```
- ``` js
- // Either mutate the global object…
- global.fetch = require("node-fetch");
- global.FormData = require("form-data");
- global.URLSearchParams = require("url").URLSearchParams;
- // …or use the static wretch.polyfills method to impact every wretch instance created afterwards.
- wretch.polyfills({
- fetch: require("node-fetch"),
- FormData: require("form-data"),
- URLSearchParams: require("url").URLSearchParams,
- });
- ```
Deno
- ```ts
- // You can import wretch from any CDN that serve ESModules.
- import wretch from "https://cdn.skypack.dev/wretch";
- const text = await wretch("https://httpstat.us").get("/200").text();
- console.log(text); // -> 200 OK
- ```
Usage
Import
- ```typescript
- // ECMAScript modules
- import wretch from "wretch"
- // CommonJS
- const wretch = require("wretch")
- // Global variable (script tag)
- window.wretch
- ```
Minimal Example
- ``` js
- import wretch from "wretch"
- // Instantiate and configure wretch
- const api =
- wretch("https://jsonplaceholder.typicode.com", { mode: "cors" })
- .errorType("json")
- .resolve(r => r.json())
- try {
- // Fetch users
- const users = await api.get("/users")
- // Find all posts from a given user
- const user = users.find(({ name }) => name === "Nicholas Runolfsdottir V")
- const postsByUser = await api.get(`/posts?userId=${user.id}`)
- // Create a new post
- const newPost = await api.url("/posts").post({
- title: "New Post",
- body: "My shiny new post"
- })
- // Patch it
- await api.url("/posts/" + newPost.id).patch({
- title: "Updated Post",
- body: "Edited body"
- })
- // Fetch it
- await api.get("/posts/" + newPost.id)
- } catch (error) {
- // The API could return an empty object - in which case the status text is logged instead.
- const message =
- typeof error.message === "object" && Object.keys(error.message).length > 0
- ? JSON.stringify(error.message)
- : error.response.statusText
- console.error(`${error.status}: ${message}`)
- }
- ```
Chaining
- ```ts
- // First, instantiate wretch
- wretch(baseUrl, baseOptions)
- ```
- ```ts
- // Optional - A set of helper methods to set the default options, set accept header, change the current url…
- .<helper method(s)>()
- // Optional - Serialize an object to json or FormData formats and sets the body & header field if needed
- .<body type>()
- // Required - Sends the get/put/post/delete/patch request.
- .<http method>()
- ```
- ```ts
- // Optional - You can chain error handlers here
- .<catcher(s)>()
- // Required - Specify the data type you need, which will be parsed and handed to you
- .<response type>()
- // >> Ends the response chain.
- ```
- ```ts
- .then(…)
- .catch(…)
- ```
💡 The API documentation is now autogenerated and hosted separately, click the links access it.
- ``` js
- import wretch from "wretch"
- wretch.options({ mode: "cors" })
- let w = wretch("http://domain.com/", { cache: "default" })
- ```
- ``` js
- w = w
- .url("/resource/1")
- .headers({ "Cache-Control": no-cache })
- .content("text/html")
- ```
- ``` js
- w = w.body("<html><body><div/></body></html>")
- ```
- ``` js
- // These shorthands:
- wretch().get("/url");
- wretch().post({ json: "body" }, "/url");
- // Are equivalent to:
- wretch().url("/url").get();
- wretch().json({ json: "body" }).url("/url").post();
- ```
- ``` js
- wretch("...")
- .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();
- ```
- ```ts
- type WretchError = Error & {
- status: number;
- response: WretchResponse;
- text?: string;
- json?: Object;
- };
- ```
- ``` js
- wretch("/resource")
- .get()
- .unauthorized(async (error, req) => {
- // Renew credentials
- const token = await wretch("/renewtoken").get().text();
- storeToken(token);
- // Replay the original request with new credentials
- return req.auth(token).get().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(callback);
- ```
- ``` js
- // Without a callback
- wretch("...").get().json().then(json => /* json is the parsed json of the response body */)
- // Without a callback using await
- const json = await wretch("...").get().json()
- // With a callback the value returned is passed to the Promise
- wretch("...").get().json(json => "Hello world!").then(console.log) // => Hello world!
- ```
Addons
- ``` js
- import FormDataAddon from "wretch/addons/formData"
- import QueryStringAddon from "wretch/addons/queryString"
- // Add both addons
- const w = wretch().addon(FormDataAddon).addon(QueryStringAddon)
- // Additional features are now available
- w.formData({ hello: "world" }).query({ check: true })
- ```
- ``` js
- import QueryStringAddon from "wretch/addons/queryString"
- ```
- ``` js
- import FormDataAddon from "wretch/addons/formData"
- ```
- ``` js
- import FormUrlAddon from "wretch/addons/formUrl"
- ```
- ``` js
- import AbortAddon from "wretch/addons/abort"
- ```
- ``` js
- const [c, w] = wretch("...")
- .addon(AbortAddon())
- .get()
- .onAbort((_) => console.log("Aborted !"))
- .controller();
- w.text((_) => console.log("should never be called"));
- c.abort();
- // Or :
- const controller = new AbortController();
- wretch("...")
- .addon(AbortAddon())
- .signal(controller)
- .get()
- .onAbort((_) => console.log("Aborted !"))
- .text((_) => console.log("should never be called"));
- controller.abort();
- ```
- ``` js
- // 1 second timeout
- wretch("...").addon(AbortAddon()).get().setTimeout(1000).json(_ =>
- // will not be called if the request timeouts
- )
- ```
💡 Make sure to follow the additional instructions in the documentation to setup Node.js if necessary.
Middlewares
- ``` 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](https://github.com/elbywan/wretch-middlewares/) package.
- ``` 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,
- onRetry: null,
- 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'
- )
- })
- ])
- ```
- ``` 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()
- })
- ])./* ... */
- ```
- ``` 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'
- })
- ])./* ... */
- ```
- ``` js
- import wretch from 'wretch'
- import { delay } from 'wretch/middlewares'
- wretch().middlewares([
- delay(1000)
- ])./* ... */
- ```
Writing a Middleware
- ```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<WretchResponse>;
- ```
Context
- ``` 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("...")
- // Pass "context" by reference as an option
- .options({ context })
- .middlewares([contextMiddleware])
- .get()
- .res();
- console.log(context.property); // prints "anything"
- ```
Advanced examples
👀 Show me the code
- ``` js
- /* 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()
- response.text = () => Promise.resolve(opts.method + "@" + url)
- response.json = () => Promise.resolve({ url, method: opts.method })
- // 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("...").middlewares([cache]).get()
- // To chain middlewares
- wretch("...").middlewares([
- logMiddleware(),
- delayMiddleware(1000),
- shortCircuitMiddleware()
- }).get().text(_ => console.log(text))
- // To test the cache middleware more thoroughly
- const wretchCache = wretch().middlewares([cacheMiddleware(1000)])
- const printResource = (url, timeout = 0) =>
- setTimeout(_ => wretchCache.url(url).get().notFound(console.error).text(console.log), timeout)
- // The resource url, change it to an invalid route to check the error handling
- const resourceUrl = "/"
- // Only two actual requests are made here even though there are 30 calls
- for(let i = 0; i < 10; i++) {
- printResource(resourceUrl)
- printResource(resourceUrl, 500)
- printResource(resourceUrl, 1500)
- }
- ```
Migration from v1
Philosophy
Compatibility
Addons
- ``` js
- /* Previously (wretch@1) */
- import wretch from "wretch"
- wretch.formData({ hello: "world" }).query({ check: true })
- /* Now (wretch@2) */
- import FormDataAddon from "wretch/addons/formData"
- import QueryStringAddon from "wretch/addons/queryString"
- import wretch as baseWretch from "wretch"
- // Add both addons
- const wretch = baseWretch().addon(FormDataAddon).addon(QueryStringAddon)
- // Additional features are now available
- wretch.formData({ hello: "world" }).query({ check: true })
- ```
Typescript
API Changes
Replace / Mixin arguments
- ``` js
- /* Previously (wretch@1) */
- wretch.options({ credentials: "same-origin" }, false) // false: do not merge the value
- wretch.options({ credentials: "same-origin" }) // Default behaviour stays the same
- /* Now (wretch@2) */
- wretch.options({ credentials: "same-origin" }, true) // true: replace the existing value
- wretch.options({ credentials: "same-origin" }) // Default behaviour stays the same
- ```
HTTP methods extra argument
- ``` js
- /* Previously (wretch@1) */
- wretch("...").get({ my: "option" })
- ```
- ``` js
- /* Now (wretch@2) */
- wretch("https://base.com").get("/resource/1")
- ```