Skip to content

Instantly share code, notes, and snippets.

@qwtel
Last active August 12, 2020 04:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save qwtel/ed6e617fb58bbce05f1e1c677605e6cd to your computer and use it in GitHub Desktop.
Save qwtel/ed6e617fb58bbce05f1e1c677605e6cd to your computer and use it in GitHub Desktop.
Subtypes of the browser's own Request and Response types adopted for JSON objects.

Main difference to regular Response and Request types is that no manual JSON.stringify and headers.set('Content-Type', ...) is required.

In my opinion, JSONRequest is the most lightweight way of fixing the biggest issue of the Fetch API when dealing with JSON APIs, without resorting to full alternatives such as superagent or axios.

Example:

const response = await fetch(new JSONRequest('/comments', {
  method: 'POST',
  body: { text: 'Usage example: ...' },
}))

const { id } = await response.json()

The JSONResponse type is mostly useful inside service workers when responding to requests with some ad-hoc JSON, or data stored in IndexedDD, but it can also be used to quickly put some data into a cache:

const cache = await caches.open('temp-comments')
cache.put('/comments/1', new JSONResponse({ text: 'Usage example: ...' }))

Note that both these classes only work with JSON as body. Use the base types when the body is a File, ArrayBuffer, etc.

export class SearchParamsURL extends URL {
/**
* @param {string | URL} url
* @param {{ [name: string]: string }} [params]
* @param {string | URL} [base]
*/
constructor(url, params = {}, base) {
super(url.toString(), base);
for (const [k, v] of Object.entries(params)) this.searchParams.append(k, v);
}
}
export {
SearchParamsURL as SearchURL,
SearchParamsURL as ParamsURL,
}
/** @typedef {BodyInit | object} JSONBodyInit */
/** @typedef {Omit<RequestInit, 'body'> & { body?: JSONBodyInit | null }} JSONRequestInit */
/**
* @param {JSONBodyInit} b
*/
function isBodyInit(b) {
return (b == null || typeof b === 'string' || b instanceof Blob || b instanceof ArrayBuffer || ArrayBuffer.isView(b) || b instanceof FormData || b instanceof URLSearchParams || b instanceof ReadableStream);
}
export class JSONRequest extends Request {
/**
* @param {RequestInfo | URL} input
* @param {JSONRequestInit} [init]
* @param {(this: any, key: string, value: any) => any} [replacer]
* @param {string | number} [space]
*/
constructor(input, init, replacer, space) {
const { headers: h, body: b, ...i } = init || {};
const bi = isBodyInit(b);
const body = bi ? b : JSON.stringify(b, replacer, space);
const headers = new Headers(h);
if (!headers.has('Content-Type') && !bi) headers.set('Content-Type', JSONRequest.contentType);
if (!headers.has('Accept')) headers.set('Accept', JSONRequest.accept);
super(input instanceof URL ? input.toString() : input, { headers, body, ...i });
}
}
JSONRequest.contentType = 'application/json;charset=UTF-8';
JSONRequest.accept = 'application/json, text/plain, */*';
export class JSONResponse extends Response {
/**
* @param {JSONBodyInit | null} body
* @param {ResponseInit} [init]
* @param {(this: any, key: string, value: any) => any} [replacer]
* @param {string | number} [space]
*/
constructor(body, init, replacer, space) {
const { headers: h, ...i } = init || {};
const bi = isBodyInit(body)
const b = bi ? body : JSON.stringify(body, replacer, space);
const headers = new Headers(h);
if (!headers.has('Content-Type') && !bi) headers.set('Content-Type', JSONResponse.contentType);
super(b, { headers, ...i });
}
}
JSONResponse.contentType = 'application/json;charset=UTF-8';
/**
* @param {JSONRequest | string | URL} input
* @param {JSONRequestInit} [init]
* @param {(this: any, key: string, value: any) => any} [replacer]
* @param {string | number} [space]
*/
export function jsonFetch(input, init, replacer, space) {
return fetch(new JSONRequest(input, init, replacer, space));
}
export class SearchParamsURL extends URL {
constructor(url: string | URL, params: { [name: string]: string } = {}, base?: string | URL) {
super(url.toString(), base);
for (const [k, v] of Object.entries(params)) this.searchParams.append(k, v);
}
}
export {
SearchParamsURL as SearchURL,
SearchParamsURL as ParamsURL,
}
type JSONBodyInit = BodyInit | object;
type JSONRequestInit = Omit<RequestInit, 'body'> & { body?: JSONBodyInit | null };
function isBodyInit(b: JSONBodyInit) {
return (b == null || typeof b === 'string' || b instanceof Blob || b instanceof ArrayBuffer || ArrayBuffer.isView(b) || b instanceof FormData || b instanceof URLSearchParams || b instanceof ReadableStream);
}
export class JSONRequest extends Request {
static contentType = 'application/json;charset=UTF-8';
static accept = 'application/json, text/plain, */*';
constructor(
input: RequestInfo | URL,
init?: JSONRequestInit,
replacer?: (this: any, key: string, value: any) => any,
space?: string | number
) {
const { headers: h, body: b, ...i } = init || {};
const bi = isBodyInit(b);
const body = bi ? (b as BodyInit) : JSON.stringify(b, replacer, space);
const headers = new Headers(h);
if (!headers.has('Content-Type') && !bi) headers.set('Content-Type', JSONRequest.contentType);
if (!headers.has('Accept')) headers.set('Accept', JSONRequest.accept);
super(input instanceof URL ? input.toString() : input, { headers, body, ...i });
}
}
export class JSONResponse extends Response {
static contentType = 'application/json;charset=UTF-8';
constructor(
body: JSONBodyInit | null,
init: ResponseInit,
replacer?: (this: any, key: string, value: any) => any,
space?: string | number
) {
const { headers: h, ...i } = init || {};
const bi = isBodyInit(body)
const b = bi ? (body as BodyInit) : JSON.stringify(body, replacer, space);
const headers = new Headers(h);
if (!headers.has('Content-Type') && !bi) headers.set('Content-Type', JSONResponse.contentType);
super(b, { headers, ...i });
}
}
export function jsonFetch(
input: JSONRequest | string | URL,
init?: JSONRequestInit,
replacer?: (this: any, key: string, value: any) => any,
space?: string | number,
) {
return fetch(new JSONRequest(input, init, replacer, space));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment