Skip to content

Latest commit

 

History

History
648 lines (455 loc) · 15.6 KB

README.md

File metadata and controls

648 lines (455 loc) · 15.6 KB

wretch-logo

wretch

travis-badge npm-badge Coverage Status license-badge

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

f[ETCH] [WR]apper

Important : Wretch is in active development ! Please check out the changelog after each update for new features and breaking changes. If you want to try out the hot stuff, look at the dev branch.

Table of Contents

Motivation

Because having to write two callbacks for a simple request is awkward.

// 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
})

Because manually checking and throwing every request error code is fastidious.

// Fetch won’t reject on HTTP error status

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

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

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")
  .json({ "hello": "world" })
  .post()
  .res(response => /* ... */)

Because configuration should not rhyme with repetition.

// Wretch is immutable which means that you can configure, store and reuse objects

const corsWretch = wretch().options({ credentials: "include", mode: "cors" })

// Sends a cors request to http://mycrossdomainapi.com
corsWretch.url("http://mycrossdomainapi.com").get().res(response => /* ... */)

// Adding a specific header and a base url without modifying the original object
const corsWretch2 = corsWretch.url("http://myendpoint.com").headers({ "X-HEADER": "VALUE" })
// Post json to http://myendpoint.com/json/postdata
corsWretch2.url("/json/postdata").json({ a: 1 }).post()

// Reuse the original cors wretch object
const corsWretch3 = corsWretch.url("http://myotherendpoint.com").accept("application/json")
// Get json from http://myotherendpoint.com/data/1
corsWretch3.url("/data/1").get().json(myjson => /* ... */)
// Get json from http://myotherendpoint.com/data/2
corsWretch3.url("/data/2").get().json(myjson => /* ... */)

Installation

Npm

npm i wretch

Clone

git clone https://github.com/elbywan/wretch
cd wretch
npm install
npm start

Compatibility

Browsers

Wretch is compatible with modern browsers out of the box.

For older environment without fetch support, you should get a polyfill.

Node.js

Works with any FormData or fetch polyfills.

// The global way :

global.fetch = require("node-fetch")
global.FormData = require("form-data")
global.URLSearchParams = require("url").URLSearchParams

// Or the non-global way :

const fetch = require("node-fetch")
const FormData = require("form-data")

wretch().polyfills({
    fetch: fetch,
    FormData: FormData,
    URLSearchParams: require("url").URLSearchParams
})

Usage

Wretch is bundled using the UMD format (@dist/bundle/wretch.js) alongside es2015 modules (@dist/index.js) and typescript definitions.

Import

<!--- "wretch" will be attached to the global window object. -->
<script src="https://unpkg.com/wretch"></script>
// es2015 modules
import wretch from "wretch"

// commonjs
var wretch = require("wretch")

Code

Wretcher objects are immutable.

wretch(url, options)
      // A fresh Wretcher object
  .[helper method(s)]()
      // Optional
      // A set of helper methods to set the default options, set accept header, change the current url ...
  .[body type]()
      // Optional
      // Serialize an object to json or FormData formats and sets the body & header field if needed
  .[http method]()
      // Required
      // Performs the get/put/post/delete/patch request

  /* Fetch is called at this time */

  .[catcher(s)]()
      // Optional
      // You can chain error handlers here
  .[response type]()
      // Required
      // Specify the data type you need, which will be parsed and handed to you

  /* Wretch returns a Promise, so you can continue chaining actions afterwards. */

  .then(/* ... */)
  .catch(/* ... */)

API


wretch(url = "", opts = {})

Create a new Wretcher object with an url and vanilla fetch options.

Helper Methods

Helper methods are optional and can be chained.

defaults(opts: Object, mixin: boolean = false)

Set default fetch options which will be used for every subsequent requests.

// Interestingly enough, default options are mixed in :

wretch().defaults({ headers: { "Accept": "application/json" }})

// The fetch request is sent with both headers.
wretch("...", { headers: { "X-Custom": "Header" }}).get()
// You can mix in with the existing options instead of overriding them by passing a boolean flag :

wretch().defaults({ headers: { "Accept": "application/json" }})
wretch().defaults({ encoding: "same-origin", headers: { "X-Custom": "Header" }}, true)

/* The new options are :
{
  headers: { "Accept": "application/json", "X-Custom": "Header" },
  encoding: "same-origin"
}
*/

errorType(method: "text" | "json" = "text")

Sets the method (text, json ...) used to parse the data contained in the response body in case of an HTTP error.

Persists for every subsequent requests.

wretch().errorType("json")

wretch("http://server/which/returns/an/error/with/a/json/body")
  .get()
  .res()
  .catch(error => {
    // error[errorType] (here, json) contains the parsed body
    console.log(error.json))
  }

polyfills(polyfills: Object)

Sets the non-global polyfills which will be used for every subsequent calls.

const fetch = require("node-fetch")
const FormData = require("form-data")

wretch().polyfills({
    fetch: fetch,
    FormData: FormData,
    URLSearchParams: require("url").URLSearchParams
})

catcher(code: number, catcher: (error: WretcherError) => void)

Adds a catcher which will be called on every subsequent request error.

Very useful when you need to perform a repetitive action on a specific error code.

const w = wretcher()
  .catcher(404, err => redirect("/routes/notfound", err.message))
  .catcher(500, err => flashMessage("internal.server.error"))

// No need to catch 404 or 500 code, they are already taken care of.
w.url("http://myapi.com/get/something").get().json(json => /* ... */)

// Default catchers can be overridden if needed.
w.url("...").notFound(err => /* overrides the default 'redirect' catcher */)

options(options: Object, mixin: boolean = false)

Set the fetch options.

wretch("...").options({ credentials: "same-origin" })

Wretch being immutable, you can store the object for later use.

const corsWretch = wretch().options({ credentials: "include", mode: "cors" })

corsWretch.url("http://endpoint1").get()
corsWretch.url("http://endpoint2").get()
// You can mix in with the existing options instead of overriding them by passing a boolean flag :

wretch()
  .options({ headers: { "Accept": "application/json" }})
  .options({ encoding: "same-origin", headers: { "X-Custom": "Header" }}, true)

/* Options mixed in :
{
  headers: { "Accept": "application/json", "X-Custom": "Header" },
  encoding: "same-origin"
}
*/

url(url: string, replace: boolean = false)

Appends or replaces the url.

wretch({ credentials: "same-origin" }).url("...").get().json(/* ... */)

// Can be used to set a base url

// Subsequent requests made using the 'blogs' object will be prefixed with "http://mywebsite.org/api/blogs"
const blogs = wretch("http://mywebsite.org/api/blogs")

// Perfect for CRUD apis
const id = await blogs.json({ name: "my blog" }).post().json(_ => _.id)
const blog = await blogs.url(`/${id}`).get().json()
console.log(blog.name)

await blogs.url(`/${id}`).delete().res()

// And to replace the base url if needed :
const noMoreBlogs = blogs.url("http://mywebsite.org/", true)

query(qp: Object)

Converts a javascript object to query parameters, then appends this query string to the current url.

let w = wretch("http://example.com")
// 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?c=3&d=4&d=5

headers(headerValues: Object)

Set request headers.

wretch("...")
  .headers({ "Content-Type": "text/plain", Accept: "application/json" })
  .body("my text")
  .post()
  .json()

accept(headerValue: string)

Shortcut to set the "Accept" header.

wretch("...").accept("application/json")

content(headerValue: string)

Shortcut to set the "Content-Type" header.

wretch("...").content("application/json")

Body Types

A body type is only needed when performing put/patch/post requests with a body.

body(contents: any)

Set the request body with any content.

wretch("...").body("hello").put()

json(jsObject: Object)

Sets the content type header, stringifies an object and sets the request body.

const jsonObject = { a: 1, b: 2, c: 3 }
wretch("...").json(jsonObject).post()

formData(formObject: Object)

Converts the javascript object to a FormData and sets the request body.

const form = {
  hello: "world",
  duck: "Muscovy"
}
wretch("...").formData(form).post()

Http Methods

Required

You can pass the fetch options here if you prefer.

get(opts = {})

Perform a get request.

wretch("...").get({ credentials: "same-origin" })

delete(opts = {})

Perform a delete request.

wretch("...").delete({ credentials: "same-origin" })

put(opts = {})

Perform a put request.

wretch("...").json({...}).put({ credentials: "same-origin" })

patch(opts = {})

Perform a patch request.

wretch("...").json({...}).patch({ credentials: "same-origin" })

post(opts = {})

Perform a post request.

wretch("...").json({...}).post({ credentials: "same-origin" })

Catchers

Catchers are optional, but if you do not provide them an error will still be thrown in case of an http error code received.

Catchers can be chained.

type WretcherError = Error & { status: number, response: Response, text?: string, json?: Object }
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))
  .res()

badRequest(cb: (error: WretcherError) => any)

Syntactic sugar for error(400, cb).

unauthorized(cb: (error: WretcherError) => any)

Syntactic sugar for error(401, cb).

forbidden(cb: (error: WretcherError) => any)

Syntactic sugar for error(403, cb).

notFound(cb: (error: WretcherError) => any)

Syntactic sugar for error(404, cb).

timeout(cb: (error: WretcherError) => any)

Syntactic sugar for error(418, cb).

internalError(cb: (error: WretcherError) => any)

Syntactic sugar for error(500, cb).

error(code: number, cb: (error: WretcherError) => any)

Catch a specific error and perform the callback.

Response Types

Required

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

res(cb: (response : Response) => any)

Raw Response handler.

wretch("...").get().res(response => console.log(response.url))

json(cb: (json : Object) => any)

Json handler.

wretch("...").get().json(json => console.log(Object.keys(json)))

blob(cb: (blob : Blob) => any)

Blob handler.

wretch("...").get().blob(blob => /* ... */)

formData(cb: (fd : FormData) => any)

FormData handler.

wretch("...").get().formData(formData => /* ... */)

arrayBuffer(cb: (ab : ArrayBuffer) => any)

ArrayBuffer handler.

wretch("...").get().arrayBuffer(arrayBuffer => /* ... */)

text(cb: (text : string) => any)

Text handler.

wretch("...").get().text(txt => console.log(txt))

Performance API (experimental)

perfs(cb: (timings: PerformanceTiming) => void)

Takes advantage of the Performance API (browsers & node.js) to expose timings related to the request.

Browser timings are very accurate, node.js only contains raw measures.

// Use perfs() before the response types (text, json, ...)
wretch("...")
  .get()
  .perfs(timings => {
    /* Will be called when the timings are ready. */
    console.log(timings.startTime)
  })
  .res()
  /* ... */

For node.js, there is a little extra work to do :

// Node.js 8.5+ only
const { performance, PerformanceObserver } = require("perf_hooks")

const fetchPolyfill =
wretch().polyfills({
  fetch: function(url, opts) {
    performance.mark(url + " - begin")
    return fetch(url, opts).then(_ => {
      performance.mark(url + " - end")
      performance.measure(_.url, url + " - begin", url + " - end")
    })
  },
  /* ... */
  performance: performance,
  PerformanceObserver: PerformanceObserver
})

License

MIT