├── .gitignore ├── .vscode └── settings.json ├── typings └── domfix.d.ts ├── .github └── workflows │ ├── branches.yml │ └── tags.yml ├── scripts └── build_npm.ts ├── search-params-url.ts ├── LICENSE.md ├── test └── index.test.js ├── index.ts └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /npm -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.lint": true, 4 | "deno.unstable": true 5 | } -------------------------------------------------------------------------------- /typings/domfix.d.ts: -------------------------------------------------------------------------------- 1 | interface URLSearchParams { 2 | [Symbol.iterator](): Iterator<[string, string]> 3 | } 4 | 5 | interface Headers { 6 | [Symbol.iterator](): Iterator<[string, string]> 7 | } 8 | 9 | interface FormData { 10 | [Symbol.iterator](): Iterator<[string, FormDataEntryValue]> 11 | } -------------------------------------------------------------------------------- /.github/workflows/branches.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | push: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: denoland/setup-deno@v1 17 | with: 18 | deno-version: v1.x 19 | - run: deno test ./test 20 | -------------------------------------------------------------------------------- /scripts/build_npm.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S deno run --allow-read --allow-write=./,/Users/qwtel/Library/Caches/deno --allow-net --allow-env=HOME,DENO_AUTH_TOKENS,DENO_DIR --allow-run=git,pnpm 2 | 3 | import { basename, extname } from "https://deno.land/std@0.133.0/path/mod.ts"; 4 | import { build, emptyDir } from "https://deno.land/x/dnt/mod.ts"; 5 | 6 | import { 7 | copyMdFiles, mkPackage, 8 | } from 'https://gist.githubusercontent.com/qwtel/ecf0c3ba7069a127b3d144afc06952f5/raw/latest-version.ts' 9 | 10 | await emptyDir("./npm"); 11 | 12 | const name = basename(Deno.cwd()) 13 | 14 | await build({ 15 | entryPoints: ["./index.ts", { 16 | name: './search-params-url', 17 | path: './search-params-url.ts' 18 | }], 19 | outDir: "./npm", 20 | shims: {}, 21 | test: false, 22 | package: await mkPackage(name), 23 | declaration: true, 24 | packageManager: 'pnpm', 25 | compilerOptions: { 26 | sourceMap: true, 27 | target: 'ES2019' 28 | }, 29 | }); 30 | 31 | // post build steps 32 | await copyMdFiles(); 33 | -------------------------------------------------------------------------------- /search-params-url.ts: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore no-explicit-any 2 | export type SearchParamsInit = [string, any][] | Record | string | URLSearchParams; 3 | 4 | // This could be it's own module... 5 | /** 6 | * Like `URL`, but accepts a `params` argument that is added to the search parameters/query string. 7 | */ 8 | export class SearchParamsURL extends URL { 9 | constructor( 10 | url: string | URL, 11 | params?: SearchParamsInit | null, 12 | base?: string | URL 13 | ) { 14 | super(url as string, base); 15 | const iterable = Array.isArray(params) || params instanceof URLSearchParams 16 | ? params 17 | : typeof params === 'string' 18 | ? new URLSearchParams(params) 19 | : Object.entries(params ?? {}) 20 | for (const [k, v] of iterable) 21 | this.searchParams.append(k, '' + v); 22 | } 23 | } 24 | 25 | export { 26 | SearchParamsURL as SearchURL, 27 | SearchParamsURL as ParamsURL, 28 | } 29 | 30 | /** @deprecated Use SearchParamsURL instead */ 31 | export const urlWithParams = (...args: ConstructorParameters) => { 32 | return new SearchParamsURL(...args).href; 33 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License 2 | 3 | Copyright (c) 2020 Florian Klampfer (https://qwtel.com/) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/tags.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | push: 8 | tags: 9 | - v* 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: denoland/setup-deno@v1 17 | with: 18 | deno-version: v1.x 19 | - run: deno test ./test 20 | 21 | publish-npm: 22 | needs: test 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v2 26 | - uses: denoland/setup-deno@v1 27 | with: 28 | deno-version: v1.x 29 | - uses: actions/setup-node@v1 30 | with: 31 | node-version: 14 32 | registry-url: https://registry.npmjs.org/ 33 | - uses: pnpm/action-setup@v2 34 | with: 35 | version: 6 36 | run_install: false 37 | - run: deno run -A ./scripts/build_npm.ts 38 | - run: cd ./npm && npm publish 39 | env: 40 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 41 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | import 'https://gist.githubusercontent.com/qwtel/b14f0f81e3a96189f7771f83ee113f64/raw/TestRequest.ts' 2 | import { 3 | assert, 4 | assertExists, 5 | assertEquals, 6 | assertStrictEquals, 7 | assertStringIncludes, 8 | assertThrows, 9 | assertRejects, 10 | assertArrayIncludes, 11 | } from 'https://deno.land/std@0.133.0/testing/asserts.ts' 12 | const { test } = Deno; 13 | 14 | import { JSONRequest, JSONResponse } from '../index.ts' 15 | 16 | test('exists', () =>{ 17 | assertExists(JSONRequest) 18 | assertExists(JSONResponse) 19 | }) 20 | 21 | test('exists II', () =>{ 22 | assertExists(new JSONRequest('/')) 23 | assertExists(new JSONResponse()) 24 | }) 25 | 26 | test('instanceof request/response ', () =>{ 27 | assert(new JSONRequest('/') instanceof Request) 28 | assert(new JSONResponse('/') instanceof Response) 29 | }) 30 | 31 | test('stringify ', async () =>{ 32 | assertEquals(await new JSONRequest('/', { method: 'POST', body: { foo: 'bar' } }).json(), { foo: 'bar' }) 33 | assertEquals(await new JSONResponse({ foo: 'bar' }).json(), { foo: 'bar' }) 34 | }) 35 | 36 | test('headers', () => { 37 | assertStringIncludes(new JSONRequest('/', { method: 'POST', body: { foo: 'bar' } }).headers.get('content-type'), 'application/json') 38 | assertStringIncludes(new JSONRequest('/', { method: 'POST', body: { foo: 'bar' } }).headers.get('accept'), 'application/json') 39 | assertStringIncludes(new JSONResponse({ foo: 'bar' }).headers.get('content-type'), 'application/json') 40 | }) 41 | 42 | test('non json data', () => { 43 | assertStringIncludes(new JSONRequest('/', { method: 'POST', body: new FormData() }).headers.get('content-type'), 'multipart/form-data') 44 | assertStringIncludes(new JSONRequest('/', { method: 'POST', body: new FormData() }).headers.get('accept'), 'application/json') 45 | assertStringIncludes(new JSONResponse(new FormData()).headers.get('content-type'), 'multipart/form-data') 46 | }) 47 | 48 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore-file no-cond-assign 2 | // deno-lint-ignore no-explicit-any 3 | export type JSONBodyInit = BodyInit | any; 4 | export type JSONRequestInit = { body?: JSONBodyInit | null } & Omit; 5 | 6 | /** 7 | * Tests is the argument is a Fetch API `BodyInit`. 8 | * Assumed to be `JSONValue` otherwise. 9 | */ 10 | function isBodyInit(b?: JSONBodyInit): b is BodyInit { 11 | return ( 12 | b == null || 13 | typeof b === 'string' || 14 | (typeof Blob !== 'undefined' && b instanceof Blob) || 15 | (typeof ArrayBuffer !== 'undefined' && (b instanceof ArrayBuffer || ArrayBuffer.isView(b))) || 16 | (typeof FormData !== 'undefined' && b instanceof FormData) || 17 | (typeof URLSearchParams !== 'undefined' && b instanceof URLSearchParams) || 18 | (typeof ReadableStream !== 'undefined' && b instanceof ReadableStream) 19 | ); 20 | } 21 | 22 | export class JSONRequest extends Request { 23 | static contentType = 'application/json;charset=UTF-8'; 24 | static accept = 'application/json, text/plain, */*'; 25 | 26 | constructor( 27 | input: RequestInfo | URL, 28 | init?: JSONRequestInit, 29 | replacer?: Parameters[1], 30 | space?: Parameters[2], 31 | ) { 32 | const { headers: _headers, body: _body, ..._init } = init || {}; 33 | 34 | let isBI: boolean 35 | const body = (isBI = isBodyInit(_body)) 36 | ? _body 37 | : JSON.stringify(_body, replacer, space); 38 | 39 | const headers = new Headers(_headers); 40 | if (!headers.has('Content-Type') && !isBI) 41 | headers.set('Content-Type', JSONRequest.contentType); 42 | if (!headers.has('Accept')) 43 | headers.set('Accept', JSONRequest.accept); 44 | 45 | super(input instanceof URL ? input.href : input, { headers, body, ..._init }); 46 | } 47 | } 48 | 49 | export class JSONResponse extends Response { 50 | static contentType = 'application/json;charset=UTF-8'; 51 | 52 | constructor( 53 | body?: JSONBodyInit | null, 54 | init?: ResponseInit, 55 | replacer?: Parameters[1], 56 | space?: Parameters[2], 57 | ) { 58 | const { headers: _headers, ..._init } = init || {}; 59 | 60 | let isBI: boolean 61 | const _body = (isBI = isBodyInit(body)) 62 | ? body 63 | : JSON.stringify(body, replacer, space); 64 | 65 | const headers = new Headers(_headers); 66 | if (!headers.has('Content-Type') && !isBI) 67 | headers.set('Content-Type', JSONResponse.contentType); 68 | 69 | super(_body, { headers, ..._init }); 70 | } 71 | } 72 | 73 | export function jsonFetch(...args: ConstructorParameters) { 74 | return fetch(new JSONRequest(...args)); 75 | } 76 | 77 | export * from './search-params-url.ts' 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSON Fetch 2 | 3 | A drop-in replacements for `fetch`, `Request`, and `Response` with first class support for JSON objects. 4 | 5 | Unlike other HTTP libraries, this one stays as close as possible to the original Fetch API, 6 | while improving the ergonomics the most common use case: 7 | 8 | Before: 9 | 10 | ```ts 11 | const response = await fetch('/some', { 12 | method: 'POST', 13 | body: JSON.stringify(json), 14 | headers: { 15 | 'Content-Type': 'application/json', 16 | }, 17 | }); 18 | ``` 19 | 20 | After: 21 | 22 | ```ts 23 | import { JSONRequest } from '@worker-tools/json-fetch'; 24 | 25 | const response = await fetch(new JSONRequest('/some', { 26 | method: 'POST', 27 | body: json, 28 | })); 29 | ``` 30 | 31 | You can also use the updated `jsonFetch` function: 32 | 33 | ```ts 34 | import { jsonFetch as fetch } from '@worker-tools/json-fetch'; 35 | 36 | const response = await fetch('/some', { method: 'POST', body: data }) 37 | ``` 38 | 39 | Note that previous use cases remain intact, i.e. posting `FormData`, `ReadableStream`, etc. as body works: 40 | 41 | ```ts 42 | const response = await fetch(new JSONRequest('/some', { 43 | method: 'POST', 44 | body: new FromData(form), 45 | })) 46 | ``` 47 | 48 | This will send the body as form-data/multipart with correct content type header, as in the original Fetch API. 49 | Only difference is that the `Accept` header will be set to indicate preference for `application/json`, i.e. anticipating a JSON response from the server. 50 | 51 | -------- 52 | 53 |

54 |

This module is part of the Worker Tools collection
⁕ 55 | 56 | [Worker Tools](https://workers.tools) are a collection of TypeScript libraries for writing web servers in [Worker Runtimes](https://workers.js.org) such as Cloudflare Workers, Deno Deploy and Service Workers in the browser. 57 | 58 | If you liked this module, you might also like: 59 | 60 | - 🧭 [__Worker Router__][router] --- Complete routing solution that works across CF Workers, Deno and Service Workers 61 | - 🔋 [__Worker Middleware__][middleware] --- A suite of standalone HTTP server-side middleware with TypeScript support 62 | - 📄 [__Worker HTML__][html] --- HTML templating and streaming response library 63 | - 📦 [__Storage Area__][kv-storage] --- Key-value store abstraction across [Cloudflare KV][cloudflare-kv-storage], [Deno][deno-kv-storage] and browsers. 64 | - 🆗 [__Response Creators__][response-creators] --- Factory functions for responses with pre-filled status and status text 65 | - 🎏 [__Stream Response__][stream-response] --- Use async generators to build streaming responses for SSE, etc... 66 | - 🥏 [__JSON Fetch__][json-fetch] --- Drop-in replacements for Fetch API classes with first class support for JSON. 67 | - 🦑 [__JSON Stream__][json-stream] --- Streaming JSON parser/stingifier with first class support for web streams. 68 | 69 | Worker Tools also includes a number of polyfills that help bridge the gap between Worker Runtimes: 70 | - ✏️ [__HTML Rewriter__][html-rewriter] --- Cloudflare's HTML Rewriter for use in Deno, browsers, etc... 71 | - 📍 [__Location Polyfill__][location-polyfill] --- A `Location` polyfill for Cloudflare Workers. 72 | - 🦕 [__Deno Fetch Event Adapter__][deno-fetch-event-adapter] --- Dispatches global `fetch` events using Deno’s native HTTP server. 73 | 74 | [router]: https://workers.tools/router 75 | [middleware]: https://workers.tools/middleware 76 | [html]: https://workers.tools/html 77 | [kv-storage]: https://workers.tools/kv-storage 78 | [cloudflare-kv-storage]: https://workers.tools/cloudflare-kv-storage 79 | [deno-kv-storage]: https://workers.tools/deno-kv-storage 80 | [kv-storage-polyfill]: https://workers.tools/kv-storage-polyfill 81 | [response-creators]: https://workers.tools/response-creators 82 | [stream-response]: https://workers.tools/stream-response 83 | [json-fetch]: https://workers.tools/json-fetch 84 | [json-stream]: https://workers.tools/json-stream 85 | [request-cookie-store]: https://workers.tools/request-cookie-store 86 | [extendable-promise]: https://workers.tools/extendable-promise 87 | [html-rewriter]: https://workers.tools/html-rewriter 88 | [location-polyfill]: https://workers.tools/location-polyfill 89 | [deno-fetch-event-adapter]: https://workers.tools/deno-fetch-event-adapter 90 | 91 | Fore more visit [workers.tools](https://workers.tools). 92 | --------------------------------------------------------------------------------