Repository: sindresorhus/got
Pull request #503 (2018-07-06 13:34:00)

Simplify wrapping `got`
Fixes https://github.com/sindresorhus/got/issues/203.

Real example:

https://gist.github.com/szmarczak/6255d5fa9e2f0f0e15ef2264be867478

sindresorhus: I'm a little bit worried that we're exposing too many internals now, especially `got.normalizeArguments`.
sindresorhus: What's the difference between `.fork()` and `.create()`?
sindresorhus: I really like how much it simplifies `gh-got` though. I'm sure it will enable more useful niche versions of Got.
szmarczak: > What's the difference between .fork() and .create()?

Fork - forks the options from parent instance (squashes them into a new piece, performs `assignOptions` again), replaces `methods` and `handler` if provided.

Create - imagine there are no options, no methods, no handler. You set it up from scratch. Like another got, not inherited from any instance.
sindresorhus: We should list `fork()` first then, as that's what most end-users would want. They just want to tweak some of the defaults.
sindresorhus: A common use-case is just overriding a few defaults when creating a new instance. I don't like how you now need to specify a nested object to set a few options.

I think `got.fork()` should be optimized for the common-case and only allow overriding options, not handler/methods. If users need more advanced control, they can use `got.create()` and pass in the exposed `got.defaults`.

So `got.fork()`:

```js
const client = got.fork({headers: {'x-foo': 'bar'});
```

Also, is `fork` the best word for this? Some alternatives would be `extend()` or `defaults()`. Not saying it's not. Just putting it out there.
sindresorhus: Can you add a small tip to the Tips section (https://github.com/sindresorhus/got#tips) on how to use the `endpoint` option with `got.extend()` to quickly get an endpoint specific Got instance? I think that's a common need, so would be good to have a clear example for that specific thing.
brandon93s: I'm a bit concerned that a user is going to be confused as to whether they should be using `get.extend()` or `got.create()` for their use case. Most users are just going to want to provide new defaults, but `extend` has a small blurb in the docs compared to the in-depth `create` section which could mislead them. `create` seems to cater primarily to library / module creation (e.g. gh-got) and not everyday use. It may be time to consider breaking out some advanced sections of the docs into separate files and linking to them to avoid information overload:

> Need more control over the behavior of got? Check out the [advanced creation](#) options.

Looking elsewhere, axios has `axios.create()` and request has `request.defaults()` which are functionally equivalent to `got.extend()`. Users coming from axios may reach for `create` and have a very different experience. 
sindresorhus: @brandon93s Good point. Let's move it out into a separate file like you've described.
szmarczak: @brandon93s I would leave the names unchanged. Previously `got.extend()` was named `got.fork()`. I would just note in `readme.md`/`xxx.md` the difference between `got.create()` and `got.extend()`. I can rename it anytime if it's needed.
sindresorhus: @szmarczak I don't think @brandon93s was suggesting we rename it, but rather just pointing out the benefit of moving `got.create()` out of the readme.
szmarczak: @sindresorhus So I'll do that tomorrow then :unicorn: 

Patch:
diff --git a/advanced-creation.md b/advanced-creation.md
new file mode 100644
index 0000000..59b14c9
--- /dev/null
+++ b/advanced-creation.md
@@ -0,0 +1,116 @@
+# Advanced creation
+
+> Make calling REST APIs easier by creating niche-specific `got` instances.
+
+#### got.create(settings)
+
+Example: [gh-got](https://github.com/sindresorhus/gh-got/blob/master/index.js)
+
+Configure a new `got` instance with the provided settings.<br>
+**Note:** In contrast to `got.extend()`, this method has no defaults.
+
+##### [options](readme.md#options)
+
+To inherit from parent, set it as `got.defaults.options` or use [object spread](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax#Spread_in_object_literals).
+
+##### methods
+
+Type: `Object`
+
+Array of supported request methods.
+
+To inherit from parent, set it as `got.defaults.methods`.
+
+##### handler
+
+Type: `Function`<br>
+Default: `undefined`
+
+Function making additional changes to the request.
+
+To inherit from parent, set it as `got.defaults.handler`.<br>
+To use the default handler, just omit specifying this.
+
+###### [url](readme.md#url)
+
+###### [options](readme.md#options)
+
+###### next()
+
+Normalizes arguments and returns a `Promise` or a `Stream` depending on [`options.stream`](readme.md#stream).
+
+```js
+const settings = {
+	handler: (url, options, next) => {
+		if (options.stream) {
+			// It's a Stream
+			// We can perform stream-specific actions on it
+			return next(url, options)
+				.on('request', request => setTimeout(() => request.abort(), 50));
+		}
+
+		// It's a Promise
+		return next(url, options);
+	},
+	methods: got.defaults.methods,
+	options: {
+		...got.defaults.options,
+		json: true
+	}
+};
+
+const jsonGot = got.create(settings);
+```
+
+```js
+const defaults = {
+	handler: (url, options, next) => {
+		return next(url, options);
+	},
+	methods: [
+		'get',
+		'post',
+		'put',
+		'patch',
+		'head',
+		'delete'
+	],
+	options: {
+		retries: 2,
+		cache: false,
+		decompress: true,
+		useElectronNet: false,
+		throwHttpErrors: true,
+		headers: {
+			'user-agent': `${pkg.name}/${pkg.version} (https://github.com/sindresorhus/got)`
+		}
+	}
+};
+
+// Same as:
+const defaults = {
+	handler: got.defaults.handler,
+	methods: got.defaults.methods,
+	options: got.defaults.options
+};
+
+const unchangedGot = got.create(defaults);
+```
+
+```js
+const settings = {
+	handler: got.defaults.handler,
+	methods: got.defaults.methods,
+	options: {
+		...got.defaults.options,
+		headers: {
+			unicorn: 'rainbow'
+		}
+	}
+};
+
+const unicorn = got.create(settings);
+
+// Same as:
+const unicorn = got.extend({headers: {unicorn: 'rainbow'}});
+```
diff --git a/appveyor.yml b/appveyor.yml
new file mode 100644
index 0000000..c53c0a5
--- /dev/null
+++ b/appveyor.yml
@@ -0,0 +1,19 @@
+environment:
+  matrix:
+    - nodejs_version: '10'
+    - nodejs_version: '8'
+install:
+  - ps: Install-Product node $env:nodejs_version
+  - npm install --global npm@latest
+  - set PATH=%APPDATA%\npm;%PATH%
+  - npm install
+matrix:
+  fast_finish: true
+build: off
+shallow_clone: true
+test_script:
+  - node --version
+  - npm --version
+  - npm test
+cache:
+  - '%APPDATA%\npm-cache'
diff --git a/readme.md b/readme.md
index 9b26cf9..e543b23 100644
--- a/readme.md
+++ b/readme.md
@@ -13,7 +13,7 @@
 
 > Simplified HTTP requests
 
-[![Build Status](https://travis-ci.org/sindresorhus/got.svg?branch=master)](https://travis-ci.org/sindresorhus/got) [![Coverage Status](https://coveralls.io/repos/github/sindresorhus/got/badge.svg?branch=master)](https://coveralls.io/github/sindresorhus/got?branch=master) [![Downloads](https://img.shields.io/npm/dm/got.svg)](https://npmjs.com/got)
+[![Build Status: Linux](https://travis-ci.org/sindresorhus/got.svg?branch=master)](https://travis-ci.org/sindresorhus/got) [![Build status: Windows](https://ci.appveyor.com/api/projects/status/a9fgfqojj8mf5upf/branch/master?svg=true)](https://ci.appveyor.com/project/sindresorhus/got/branch/master) [![Coverage Status](https://coveralls.io/repos/github/sindresorhus/got/badge.svg?branch=master)](https://coveralls.io/github/sindresorhus/got?branch=master) [![Downloads](https://img.shields.io/npm/dm/got.svg)](https://npmjs.com/got)
 
 A nicer interface to the built-in [`http`](http://nodejs.org/api/http.html) module.
 
@@ -34,6 +34,7 @@ Created because [`request`](https://github.com/request/request) is bloated *(sev
 - [JSON mode](#json)
 - [WHATWG URL support](#url)
 - [Electron support](#useelectronnet)
+- [Advanced creation](advanced-creation.md)
 
 
 ## Install
@@ -105,6 +106,17 @@ Type: `Object`
 
 Any of the [`https.request`](https://nodejs.org/api/https.html#https_https_request_options_callback) options.
 
+###### baseUrl
+
+Type: `string` `Object`
+
+When specified, `url` will be prepended by `baseUrl`.<br>
+If you specify an absolute URL, it will skip the `baseUrl`.
+
+Very useful when used with `got.extend()` to create niche-specific `got` instances.
+
+Can be a string or a [WHATWG `URL`](https://nodejs.org/api/url.html#url_class_url).
+
 ###### headers
 
 Type: `Object`<br>
@@ -174,7 +186,7 @@ Query string object that will be added to the request URL. This will override th
 
 Type: `number` `Object`
 
-Milliseconds to wait for the server to end the response before aborting request with `ETIMEDOUT` error.
+Milliseconds to wait for the server to end the response before aborting request with `ETIMEDOUT` error (a.k.a. `request` property). By default there's no timeout.
 
 This also accepts an object with separate `connect`, `socket`, and `request` fields for connection, socket, and entire request timeouts.
 
@@ -235,6 +247,8 @@ If this is disabled, requests that encounter an error status code will be resolv
 
 #### got.stream(url, [options])
 
+Sets `options.stream` to `true`.
+
 `stream` method will return Duplex stream with additional events:
 
 ##### .on('request', request)
@@ -300,25 +314,56 @@ If it's not possible to retrieve the body size (can happen when streaming), `tot
 
 Sets `options.method` to the method name and makes a request.
 
-#### Instances
+### Instances
+
+#### got.extend([options])
+
+Configure a new `got` instance with default `options` and (optionally) a custom `baseUrl`:
 
-#### got.create([options])
+**Note:** You can extend another extended instance. `got.defaults` provides settings used by that instance.<br>
+Check out the [unchanged default values](source/index.js).
 
-Configure a new `got` instance with default `options`:
+```js
+const client = got.extend({
+	baseUrl: 'https://example.com',
+	headers: {
+		'x-unicorn': 'rainbow'
+	}
+});
+
+client.get('/demo');
+
+/* HTTP Request =>
+ * GET /demo HTTP/1.1
+ * Host: example.com
+ * x-unicorn: rainbow
+ */
+ ```
 
 ```js
 (async () => {
-	const client = got.create({headers: {'x-foo': 'bar'}});
-	const {headers} = await client.get('httpbin.org/headers', {json: true}).body;
+	const client = got.extend({
+		baseUrl: 'httpbin.org',
+		headers: {
+			'x-foo': 'bar'
+		}
+	});
+	const {headers} = (await client.get('/headers', {json: true})).body;
 	//=> headers['x-foo'] === 'bar'
 
-	const jsonClient = client.create({json: true, headers: {'x-baz': 'qux'}});
-	const {headers: headers2} = await jsonClient.get('httpbin.org/headers').body;
+	const jsonClient = client.extend({
+		json: true,
+		headers: {
+			'x-baz': 'qux'
+		}
+	});
+	const {headers: headers2} = (await jsonClient.get('/headers')).body;
 	//=> headers2['x-foo'] === 'bar'
 	//=> headers2['x-baz'] === 'qux'
 })();
 ```
 
+*Need more control over the behavior of Got? Check out the [`got.create()`](advanced-creation.md).*
 
 ## Errors
 
@@ -676,6 +721,30 @@ got('sindresorhus.com', {
 
 Bear in mind, if you send an `if-modified-since` header and receive a `304 Not Modified` response, the body will be empty. It's your responsibility to cache and retrieve the body contents.
 
+### Custom endpoints
+
+Use `got.extend()` to make it nicer to work with REST APIs. Especially if you use the `baseUrl` option.
+
+**Note:** Not to be confused with [`got.create()`](advanced-creation.md), which has no defaults.
+
+```js
+const got = require('got');
+const pkg = require('./package.json');
+
+const custom = got.extend({
+	baseUrl: 'example.com',
+	json: true,
+	headers: {
+		'user-agent': `my-module/${pkg.version} (https://github.com/username/my-module)`
+	}
+});
+
+// Use `custom` exactly how you use `got`
+(async () => {
+	const list = await custom('/v1/users/list');
+})();
+```
+
 
 ## Related
 
diff --git a/source/as-stream.js b/source/as-stream.js
index a77cdbe..0118272 100644
--- a/source/as-stream.js
+++ b/source/as-stream.js
@@ -6,8 +6,6 @@ const requestAsEventEmitter = require('./request-as-event-emitter');
 const {HTTPError, ReadError} = require('./errors');
 
 module.exports = options => {
-	options.stream = true;
-
 	const input = new PassThrough();
 	const output = new PassThrough();
 	const proxy = duplexer3(input, output);
diff --git a/source/assign-options.js b/source/assign-options.js
new file mode 100644
index 0000000..efe18d8
--- /dev/null
+++ b/source/assign-options.js
@@ -0,0 +1,17 @@
+const extend = require('extend');
+const is = require('@sindresorhus/is');
+
+module.exports = (defaults, options = {}) => {
+	const opts = extend(true, {}, defaults, options);
+
+	if (Reflect.has(options, 'headers')) {
+		for (const [key, value] of Object.entries(options.headers)) {
+			if (is.nullOrUndefined(value)) {
+				delete opts.headers[key];
+				continue;
+			}
+		}
+	}
+
+	return opts;
+};
diff --git a/source/create.js b/source/create.js
index bfd8f92..f61bfb5 100644
--- a/source/create.js
+++ b/source/create.js
@@ -1,64 +1,67 @@
 'use strict';
-const extend = require('extend');
-const is = require('@sindresorhus/is');
+const URLGlobal = typeof URL === 'undefined' ? require('url').URL : URL; // TODO: Use the `URL` global when targeting Node.js 10
+const errors = require('./errors');
+const assignOptions = require('./assign-options');
 const asStream = require('./as-stream');
 const asPromise = require('./as-promise');
-const errors = require('./errors');
 const normalizeArguments = require('./normalize-arguments');
+const deepFreeze = require('./deep-freeze');
 
-const assignOptions = (defaults, options = {}) => {
-	const opts = extend(true, {}, defaults, options);
+const next = (path, options) => {
+	let url = path;
 
-	if (Reflect.has(options, 'headers')) {
-		for (const [key, value] of Object.entries(options.headers)) {
-			if (is.nullOrUndefined(value)) {
-				delete opts.headers[key];
-				continue;
-			}
-		}
+	if (options.baseUrl) {
+		url = new URLGlobal(path, options.baseUrl);
+	}
+
+	options = normalizeArguments(url, options);
+
+	if (options.stream) {
+		return asStream(options);
 	}
 
-	return opts;
+	return asPromise(options);
 };
 
-const create = (defaults = {}) => {
+const create = defaults => {
+	if (!defaults.handler) {
+		defaults.handler = next;
+	}
+
 	function got(url, options) {
 		try {
-			options = assignOptions(defaults, options);
-			const normalizedArgs = normalizeArguments(url, options);
-
-			if (normalizedArgs.stream) {
-				return asStream(normalizedArgs);
-			}
-
-			return asPromise(normalizedArgs);
+			options = assignOptions(defaults.options, options);
+			return defaults.handler(url, options, next);
 		} catch (error) {
 			return Promise.reject(error);
 		}
 	}
 
-	got.create = (options = {}) => create(assignOptions(defaults, options));
+	got.create = create;
+	got.extend = (options = {}) => create({
+		options: assignOptions(defaults.options, options),
+		methods: defaults.methods,
+		handler: defaults.handler
+	});
 
 	got.stream = (url, options) => {
-		options = assignOptions(defaults, options);
-		return asStream(normalizeArguments(url, options));
+		options = assignOptions(defaults.options, options);
+		options.stream = true;
+		return defaults.handler(url, options, next);
 	};
 
-	const methods = [
-		'get',
-		'post',
-		'put',
-		'patch',
-		'head',
-		'delete'
-	];
-
-	for (const method of methods) {
+	for (const method of defaults.methods) {
 		got[method] = (url, options) => got(url, {...options, method});
 		got.stream[method] = (url, options) => got.stream(url, {...options, method});
 	}
 
 	Object.assign(got, errors);
+	Object.defineProperty(got, 'defaults', {
+		value: deepFreeze(defaults),
+		writable: false,
+		enumerable: true,
+		configurable: true
+	});
 
 	return got;
 };
diff --git a/source/deep-freeze.js b/source/deep-freeze.js
new file mode 100644
index 0000000..1d3d8f0
--- /dev/null
+++ b/source/deep-freeze.js
@@ -0,0 +1,12 @@
+'use strict';
+const is = require('@sindresorhus/is');
+
+module.exports = function deepFreeze(obj) {
+	for (const [key, value] of Object.entries(obj)) {
+		if (is.object(value)) {
+			deepFreeze(obj[key]);
+		}
+	}
+
+	return Object.freeze(obj);
+};
diff --git a/source/index.js b/source/index.js
index 3687013..0c188c6 100644
--- a/source/index.js
+++ b/source/index.js
@@ -3,13 +3,23 @@ const pkg = require('../package.json');
 const create = require('./create');
 
 const defaults = {
-	retries: 2,
-	cache: false,
-	decompress: true,
-	useElectronNet: false,
-	throwHttpErrors: true,
-	headers: {
-		'user-agent': `${pkg.name}/${pkg.version} (https://github.com/sindresorhus/got)`
+	methods: [
+		'get',
+		'post',
+		'put',
+		'patch',
+		'head',
+		'delete'
+	],
+	options: {
+		retries: 2,
+		cache: false,
+		decompress: true,
+		useElectronNet: false,
+		throwHttpErrors: true,
+		headers: {
+			'user-agent': `${pkg.name}/${pkg.version} (https://github.com/sindresorhus/got)`
+		}
 	}
 };
 


Test patch:
diff --git a/test/create.js b/test/create.js
index 5681ddf..c5ad429 100644
--- a/test/create.js
+++ b/test/create.js
@@ -21,12 +21,12 @@ test.after('cleanup', async () => {
 
 test('preserve global defaults', async t => {
 	const globalHeaders = (await got(s.url, {json: true})).body;
-	const instanceHeaders = (await got.create()(s.url, {json: true})).body;
+	const instanceHeaders = (await got.extend()(s.url, {json: true})).body;
 	t.deepEqual(instanceHeaders, globalHeaders);
 });
 
 test('support instance defaults', async t => {
-	const instance = got.create({
+	const instance = got.extend({
 		headers: {
 			'user-agent': 'custom-ua-string'
 		}
@@ -36,7 +36,7 @@ test('support instance defaults', async t => {
 });
 
 test('support invocation overrides', async t => {
-	const instance = got.create({
+	const instance = got.extend({
 		headers: {
 			'user-agent': 'custom-ua-string'
 		}
@@ -51,12 +51,12 @@ test('support invocation overrides', async t => {
 });
 
 test('curry previous instance defaults', async t => {
-	const instanceA = got.create({
+	const instanceA = got.extend({
 		headers: {
 			'x-foo': 'foo'
 		}
 	});
-	const instanceB = instanceA.create({
+	const instanceB = instanceA.extend({
 		headers: {
 			'x-bar': 'bar'
 		}
@@ -65,3 +65,64 @@ test('curry previous instance defaults', async t => {
 	t.is(headers['x-foo'], 'foo');
 	t.is(headers['x-bar'], 'bar');
 });
+
+test('custom headers (extend)', async t => {
+	const options = {headers: {unicorn: 'rainbow'}};
+
+	const instance = got.extend(options);
+	const headers = (await instance(`${s.url}/`, {
+		json: true
+	})).body;
+	t.is(headers.unicorn, 'rainbow');
+});
+
+test('custom endpoint with custom headers (create)', async t => {
+	const options = {headers: {unicorn: 'rainbow'}};
+	const handler = (url, options, next) => {
+		url = `${s.url}` + url;
+
+		return next(url, options);
+	};
+	const methods = ['get'];
+
+	const instance = got.create({options, methods, handler});
+	const headers = (await instance('/', {
+		json: true
+	})).body;
+	t.is(headers.unicorn, 'rainbow');
+	t.is(headers['user-agent'], undefined);
+});
+
+test('custom endpoint with custom headers (extend)', async t => {
+	const instance = got.extend({headers: {unicorn: 'rainbow'}, baseUrl: s.url});
+	const headers = (await instance(`/`, {
+		json: true
+	})).body;
+	t.is(headers.unicorn, 'rainbow');
+	t.is(headers['user-agent'] === undefined, false);
+});
+
+test('no tampering with defaults', t => {
+	const instance = got.create({
+		handler: got.defaults.handler,
+		methods: got.defaults.methods,
+		options: {
+			...got.defaults.options,
+			baseUrl: 'example'
+		}
+	});
+
+	const instance2 = instance.create({
+		handler: instance.defaults.handler,
+		methods: instance.defaults.methods,
+		options: instance.defaults.options
+	});
+
+	// Tamper Time
+	t.throws(() => {
+		instance.defaults.options.baseUrl = 'http://google.com';
+	});
+
+	t.is(instance.defaults.options.baseUrl, 'example');
+	t.is(instance2.defaults.options.baseUrl, 'example');
+});
diff --git a/test/headers.js b/test/headers.js
index 687b8c6..d07c23d 100644
--- a/test/headers.js
+++ b/test/headers.js
@@ -1,4 +1,5 @@
 import fs from 'fs';
+import util from 'util';
 import path from 'path';
 import test from 'ava';
 import FormData from 'form-data';
@@ -118,11 +119,13 @@ test('form-data sets content-length', async t => {
 });
 
 test('stream as options.body sets content-length', async t => {
+	const fixture = path.join(__dirname, 'fixtures/stream-content-length');
+	const {size} = await util.promisify(fs.stat)(fixture);
 	const {body} = await got(s.url, {
-		body: fs.createReadStream(path.join(__dirname, 'fixtures/stream-content-length'))
+		body: fs.createReadStream(fixture)
 	});
 	const headers = JSON.parse(body);
-	t.is(headers['content-length'], '9');
+	t.is(Number(headers['content-length']), size);
 });
 
 test('remove null value headers', async t => {
diff --git a/test/timeout.js b/test/timeout.js
index f45420e..9c40f39 100644
--- a/test/timeout.js
+++ b/test/timeout.js
@@ -28,7 +28,7 @@ test('timeout option', async t => {
 });
 
 test('timeout option as object', async t => {
-	const err = await t.throws(got(`${s.url}/404`, {
+	const err = await t.throws(got(`${s.url}`, {
 		timeout: {socket: 50, request: 1},
 		retries: 0
 	}));
@@ -37,7 +37,7 @@ test('timeout option as object', async t => {
 });
 
 test('socket timeout', async t => {
-	const err = await t.throws(got(`${s.url}/404`, {
+	const err = await t.throws(got(`${s.url}`, {
 		timeout: {socket: 1},
 		retries: 0
 	}));
@@ -46,7 +46,7 @@ test('socket timeout', async t => {
 });
 
 test('connection, request timeout', async t => {
-	const err = await t.throws(got(`${s.url}/404`, {
+	const err = await t.throws(got(`${s.url}`, {
 		timeout: {socket: 50, request: 1},
 		retries: 0
 	}));
@@ -57,7 +57,7 @@ test('connection, request timeout', async t => {
 test('retries on timeout, ESOCKETTIMEDOUT', async t => {
 	let tried = false;
 
-	const err = await t.throws(got(`${s.url}/404`, {
+	const err = await t.throws(got(`${s.url}`, {
 		timeout: 1,
 		retries: () => {
 			if (tried) {
@@ -76,7 +76,7 @@ test('retries on timeout, ESOCKETTIMEDOUT', async t => {
 test('retries on timeout, ETIMEDOUT', async t => {
 	let tried = false;
 
-	const err = await t.throws(got(`${s.url}/404`, {
+	const err = await t.throws(got(`${s.url}`, {
 		timeout: 15,
 		retries: () => {
 			if (tried) {