- Notifications
You must be signed in to change notification settings - Fork100
A tiny wrapper built around fetch with an intuitive syntax. 🍬
License
elbywan/wretch
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
A tiny (~2KB g-zipped) wrapper built aroundfetch with an intuitive syntax.
Wretch 2.11 is now live 🎉 ! Please have a look at thereleases and thechangelog after each update for new features and breaking changes. If you want to try out the hot stuff, please look into thedev branch.
And if you like the library please consider becoming asponsor ❤️.
wretch
is a small wrapper aroundfetch 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
Fetch needs a second callback to process the response body.
fetch("examples/example.json").then(response=>response.json()).then(json=>{//Do stuff with the parsed json});
Wretch does it for you.
// 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});
Fetch won’t reject on HTTP error status.
fetch("anything").then(response=>{if(!response.ok){if(response.status===404)thrownewError("Not found")elseif(response.status===401)thrownewError("Unauthorized")elseif(response.status===418)thrownewError("I'm a teapot !")elsethrownewError("Other error")}else// ...}).then(data=>/* ... */).catch(error=>{/* ... */})
Wretch throws when the response is not successful and contains helper methods to handle common codes.
wretch("anything").get().notFound(error=>{/* ... */}).unauthorized(error=>{/* ... */}).error(418,error=>{/* ... */}).res(response=>/* ... */).catch(error=>{/* uncaught errors */})
With fetch you have to set the header, the method and the body manually.
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.
wretch("endpoint").post({"hello":"world"}).res(response=>/* ... */)
A Wretch object is immutable which means that you can reuse previous instances safely.
// Cross origin authenticated requests on an external APIconstexternalApi=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 resourceconstresource=awaitexternalApi// 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 resourceexternalApi.url("/resource").post({"Shiny new":"resource object"}).json(handleNewResourceResult);
npm i wretch# or yarn/pnpm add wretch
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 |
<!-- 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 - …--><!-- UMD import as window.wretch --><scriptsrc="https://unpkg.com/wretch"></script><!-- Modern import --><scripttype="module">importwretchfrom'https://cdn.skypack.dev/wretch/dist/bundle/wretch.all.min.mjs'// ... //</script>
wretch@^2
is compatible with modern browsers only. For older browsers please usewretch@^1
.
Wretch is compatible with and tested inNode.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.
For older versions, the Node.js standard library does not provide a native implementation of fetch (and other Browsers-only APIs) and polyfilling is mandatory.
The non-global way (preferred):
importfetch,{FormData}from"node-fetch"// w is a reusable wretch instanceconstw=wretch().polyfills({ fetch, FormData,});
Globally:
importfetch,{FormData}from"node-fetch";// Either mutate the global object…global.fetch=fetch;global.FormData=FormData;// …or use the static wretch.polyfills method to impact every wretch instance created afterwards.wretch.polyfills({ fetch, FormData,});
Works withDeno >=0.41.0 out of the box.
Types should be imported from/dist/types.d.ts
.
// You can import wretch from any CDN that serve ESModules.importwretchfrom"https://cdn.skypack.dev/wretch";consttext=awaitwretch("https://httpstat.us").get("/200").text();console.log(text);// -> 200 OK
// ECMAScript modulesimportwretchfrom"wretch"// CommonJSconstwretch=require("wretch")// Global variable (script tag)window.wretch
importwretchfrom"wretch"// Instantiate and configure wretchconstapi=wretch("https://jsonplaceholder.typicode.com",{mode:"cors"}).errorType("json").resolve(r=>r.json())try{// Fetch usersconstusers=awaitapi.get("/users")// Find all posts from a given userconstuser=users.find(({ name})=>name==="Nicholas Runolfsdottir V")constpostsByUser=awaitapi.get(`/posts?userId=${user.id}`)// Create a new postconstnewPost=awaitapi.url("/posts").post({title:"New Post",body:"My shiny new post"})// Patch itawaitapi.url("/posts/"+newPost.id).patch({title:"Updated Post",body:"Edited body"})// Fetch itawaitapi.get("/posts/"+newPost.id)}catch(error){// The API could return an empty object - in which case the status text is logged instead.constmessage=typeoferror.message==="object"&&Object.keys(error.message).length>0 ?JSON.stringify(error.message) :error.response.statusTextconsole.error(`${error.status}:${message}`)}
A high level overview of the successive steps that can be chained to perform a request and parse the result.
// First, instantiate wretchwretch(baseUrl,baseOptions)
The "request" chain starts here.
// Optional - A set of helper methods to set the default options, set accept header, change the current url….<helpermethod(s)>()// Optional - Serialize an object to json or FormData formats and sets the body & header field if needed.<bodytype>()// Required - Sends the get/put/post/delete/patch request..<httpmethod>()
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.
// Optional - You can chain error handlers here.<catcher(s)>()// Required - Specify the data type you need, which will be parsed and handed to you.<responsetype>()// >> Ends the response chain.
From this point on, wretch returns a standard Promise.
.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.
importwretchfrom"wretch"wretch.options({mode:"cors"})letw=wretch("http://domain.com/",{cache:"default"})
Helper Methods are used to configure the request and program actions.
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.
w=w.body("<html><body><div/></body></html>")
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.
// 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 anObject
it is assumed that it is a JSON payload and it will have the same behaviour as calling.json(body)
unless theContent-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.
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.
typeWretchError=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.
wretch("/resource").get().unauthorized(async(error,req)=>{// Renew credentialsconsttoken=awaitwretch("/renewtoken").get().text();storeToken(token);// Replay the original request with new credentialsreturnreq.auth(token).get().unauthorized((err)=>{throwerr;}).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.
// Without a callbackwretch("...").get().json().then(json=>/* json is the parsed json of the response body */)// Without a callback using awaitconstjson=awaitwretch("...").get().json()// With a callback the value returned is passed to the Promisewretch("...").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 are separate pieces of code that you can import and plug intowretch
to add new features.
importFormDataAddonfrom"wretch/addons/formData"importQueryStringAddonfrom"wretch/addons/queryString"// Add both addonsconstw=wretch().addon(FormDataAddon).addon(QueryStringAddon)// Additional features are now availablew.formData({hello:"world"}).query({check:true})
Typescript should also be fully supported and will provide completions.
typesafe.mov
Used to construct and append the query string part of the URL from an object.
importQueryStringAddonfrom"wretch/addons/queryString"letw=wretch("http://example.com").addon(QueryStringAddon);// url is http://example.comw=w.query({a:1,b:2});// url is now http://example.com?a=1&b=2w=w.query({c:3,d:[4,5]});// url is now http://example.com?a=1&b=2c=3&d=4&d=5w=w.query("five&six&seven=eight");// url is now http://example.com?a=1&b=2c=3&d=4&d=5&five&six&seven=eightw=w.query({reset:true},true);// url is now http://example.com?reset=true
Adds a helper method to serialize amultipart/form-data
body from an object.
importFormDataAddonfrom"wretch/addons/formData"constform={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("...").addon(FormDataAddon).formData(form,["ignored"]).post();
Adds a method to serialize aapplication/x-www-form-urlencoded
body from an object.
importFormUrlAddonfrom"wretch/addons/formUrl"constform={a:1,b:{c:2}};constalreadyEncodedForm="a=1&b=%7B%22c%22%3A2%7D";// Automatically sets the content-type header to "application/x-www-form-urlencoded"wretch("...").addon(FormUrlAddon).formUrl(form).post();wretch("...").addon(FormUrlAddon).formUrl(alreadyEncodedForm).post();
Adds the ability to abort requests and set timeouts using AbortController and signals under the hood.
importAbortAddonfrom"wretch/addons/abort"
Only compatible with browsers that supportAbortControllers.Otherwise, you could use a (partial)polyfill.
Use cases :
const[c,w]=wretch("...").addon(AbortAddon()).get().onAbort((_)=>console.log("Aborted !")).controller();w.text((_)=>console.log("should never be called"));c.abort();// Or :constcontroller=newAbortController();wretch("...").addon(AbortAddon()).signal(controller).get().onAbort((_)=>console.log("Aborted !")).text((_)=>console.log("should never be called"));controller.abort();
// 1 second timeoutwretch("...").addon(AbortAddon()).get().setTimeout(1000).json(_=>// will not be called if the request timeouts)
Adds the ability to set theAuthorization
header for thebasic authentication scheme without the need to manually encode the username/password.
Also, allows using URLs withwretch
that contain credentials, which would otherwise throw an error.
importBasicAuthAddonfrom"wretch/addons/basicAuth"constuser="user"constpass="pass"// Automatically sets the Authorization header to "Basic " + <base64 encoded credentials>wretch("...").addon(BasicAuthAddon).basicAuth(user,pass).get()// Allows using URLs with credentials in themwretch(`https://${user}:${pass}@...`).addon(BasicAuthAddon).get()
Adds the ability to monitor progress when downloading a response.
Compatible with all platforms implementing theTransformStream WebAPI.
importProgressAddonfrom"wretch/addons/progress"wretch("some_url").addon(ProgressAddon()).get()// Called with the number of bytes loaded and the total number of bytes to load.progress((loaded,total)=>{console.log(`${(loaded/total*100).toFixed(0)}%`)}).text()
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 are functions that can intercept requests before being processed byFetch. Wretch includes a helper to help replicate themiddleware style.
importwretchfrom"wretch"import{retry,dedupe}from"wretch/middlewares"constw=wretch().middlewares([retry(),dedupe()])
💡 The following middlewares were previously provided by the
wretch-middlewares
package.
Retries a request multiple times in case of an error (or until a custom condition is true).
💡 By default, the request will be retried if the response status is not in the 2xx range.
// Replace the default condition with a custom one to avoid retrying on 4xx errors:until:(response,error)=>!!response&&(response.ok||(response.status>=400&&response.status<500))
importwretchfrom'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: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})])
Prevents having multiple identical requests on the fly at the same time.
importwretchfrom'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.
importwretchfrom'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.
importwretchfrom'wretch'import{delay}from'wretch/middlewares'wretch().middlewares([delay(1000)])
Basically a Middleware is a function having the following signature :
// A middleware accepts options and returns a configured versiontypeMiddleware=(options?:{[key:string]:any})=>ConfiguredMiddleware;// A configured middleware (with options curried)typeConfiguredMiddleware=(next:FetchLike)=>FetchLike;// A "fetch like" function, accepting an url and fetch options and returning a response promisetypeFetchLike=(url:string,opts:WretchOptions,)=>Promise<WretchResponse>;
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.
constcontextMiddleware=(next)=>(url,opts)=>{if(opts.context){// Mutate "context"opts.context.property="anything";}returnnext(url,opts);};// Provide the reference to a "context" objectconstcontext={};constres=awaitwretch("...")// Pass "context" by reference as an option.options({ context}).middlewares([contextMiddleware]).get().res();console.log(context.property);// prints "anything"
👀 Show me the code
/* A simple delay middleware. */constdelayMiddleware=delay=>next=>(url,opts)=>{returnnewPromise(res=>setTimeout(()=>res(next(url,opts)),delay))}/* Returns the url and method without performing an actual request. */constshortCircuitMiddleware=()=>next=>(url,opts)=>{// We create a new Response object to comply because wretch expects that from fetch.constresponse=newResponse()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.returnPromise.resolve(response)}/* Logs all requests passing through. */constlogMiddleware=()=>next=>(url,opts)=>{console.log(opts.method+"@"+url)returnnext(url,opts)}/* A throttling cache. */constcacheMiddleware=(throttle=0)=>{constcache=newMap()constinflight=newMap()constthrottling=newSet()returnnext=>(url,opts)=>{constkey=opts.method+"@"+urlif(!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))returnPromise.resolve(cache.get(key).clone())// If the request in already in-flight, wait until it is resolvedelseif(inflight.has(key)){returnnewPromise((resolve,reject)=>{inflight.get(key).push([resolve,reject])})}}// Init. the pending promises Mapif(!inflight.has(key))inflight.set(key,[])// If we are not throttling, activate the throttle for X millisecondsif(throttle&&!throttling.has(key)){throttling.add(key)setTimeout(()=>{throttling.delete(key)},throttle)}// We call the next middleware in the chain.returnnext(url,opts).then(_=>{// Add a cloned response to the cachecache.set(key,_.clone())// Resolve pending promisesinflight.get(key).forEach((([resolve,reject])=>resolve(_.clone()))// Remove the inflight pending promisesinflight.delete(key)// Return the original responsereturn_}).catch(_=>{// Reject pending promises on errorinflight.get(key).forEach(([resolve,reject])=>reject(_))inflight.delete(key)throw_})}}// To call a single middlewareconstcache=cacheMiddleware(1000)wretch("...").middlewares([cache]).get()// To chain middlewareswretch("...").middlewares([logMiddleware(),delayMiddleware(1000),shortCircuitMiddleware()}).get().text(_=>console.log(text))// To test the cache middleware more thoroughlyconstwretchCache=wretch().middlewares([cacheMiddleware(1000)])constprintResource=(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 handlingconstresourceUrl="/"// Only two actual requests are made here even though there are 30 callsfor(leti=0;i<10;i++){printResource(resourceUrl)printResource(resourceUrl,500)printResource(resourceUrl,1500)}
It seems like usingwretch
in a Cloudflare Worker environment is not possible out of the box, as the CloudflareResponse
implementation does not implement thetype
property and throws an error when trying to access it.
Please check the issue#159 for more information.
The following middleware should fix the issue (thanks @jimmed 🙇):
wretch().middlewares([(next)=>async(url,opts)=>{constresponse=awaitnext(url,opts);try{Reflect.get(response,"type",response);}catch(error){Object.defineProperty(response,"type",{get:()=>"default",});}returnresponse;},])
TheRequest
object from the Fetch API uses theHeaders
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:
constheaders=newHeaders();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 usingwretch
, please be mindful of this limitation and avoid setting the same header multiple times with a different case:
wretch(url).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.
You can use the following middleware to deduplicate headers (thanks @jimmed 🙇):
exportconstmanipulateHeaders=callback=>next=>(url,{ headers, ...opts})=>{constnextHeaders=callback(newHeaders(headers))returnnext(url,{ ...opts,headers:nextHeaders})}exportconstdedupeHeaders=(dedupeHeaderLogic={})=>{constdeduperMap=newMap(Object.entries(dedupeHeaderLogic).map(([k,v])=>[k.toLowerCase(),v]),)constdedupe=key=>deduperMap.get(key.toLowerCase())??(values=>newSet(values))returnmanipulateHeaders((headers)=>{Object.entries(headers.raw()).forEach(([key,values])=>{constdeduped=Array.from(dedupe(key)(values))headers.delete(key)deduped.forEach((value,index)=>headers[index ?'append' :'set'](key.toLowerCase(),value),)})returnheaders})}// 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!=='*/*')})])
Wretch has beencompletely 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
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.
Some features that were part ofwretch
v1 are now split apart and must be imported through addons.It is now needed to pass the Addon to the.addon
method to register it.
Please refer to theAddons documentation.
/* Previously (wretch@1) */importwretchfrom"wretch"wretch.formData({hello:"world"}).query({check:true})/* Now (wretch@2) */importFormDataAddonfrom"wretch/addons/formData"importQueryStringAddonfrom"wretch/addons/queryString"importbaseWretchfrom"wretch"// Add both addonsconstwretch=baseWretch().addon(FormDataAddon).addon(QueryStringAddon)// Additional features are now availablewretch.formData({hello:"world"}).query({check:true})
Types have been renamed and refactored, please update your imports accordingly and refer to thetypescript api documentation.
Some functions used to have amixin = true
argument that could be used to merge the value, others areplace = false
argument performing the opposite.In v2 there are onlyreplace = false
arguments but the default behaviour should be preserved.
/* Previously (wretch@1) */wretch.options({credentials:"same-origin"},false)// false: do not merge the valuewretch.options({credentials:"same-origin"})// Default behaviour stays the same/* Now (wretch@2) */wretch.options({credentials:"same-origin"},true)// true: replace the existing valuewretch.options({credentials:"same-origin"})// Default behaviour stays the same
In v1 it was possible to set fetch options while calling the http methods to end the request chain.
/* 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.
/* Now (wretch@2) */wretch("https://base.com").get("/resource/1")
The.replay
function has been renamed to.fetch
.
MIT
About
A tiny wrapper built around fetch with an intuitive syntax. 🍬