Repository: sindresorhus/got
Pull request #495 (2018-06-29 07:53:55)

Support discrete client configuration via `got.create([options])`
Add first-class support for discrete client configuration via new public `got.create([options])` method.

Feature is 100% backwards compatible and internal defaults have been refactored ever so slightly to dogfood the interface.

I did my best to adhere to existing code standards-- linted w/ `xo`, added tests to maintain coverage (statements + lines are slightly up, branches are slightly down), documented the method in `readme.md`, and tailored the commit message style to match the predominant pattern.

My first time contributing here so please let me know if there's anything else I can do to help! If y'all prefer to keep this out of core I'll understand and can publish elsewhere-- no hard feelings either way.

Thanks!

knksmith57: /cc @jstewmon in case you wanted to take a pass at this
ewolfe: I just pulled this down and it works great :) 

I found myself wanting to create an instance with a `hostname` option and using only the path as the argument to my instance. ie:

```js
const client = got.create({
  hostname: 'https://example.com',
  path: '/api',
  headers: {
    Authorization: `Bearer ${TOKEN}`,
  },
});

const response = await client('/hello-world'); // GET https://example.com/api/hello-world
```

What are your thoughts on adding that functionality? I don't think it's _too_ opinionated and is in the spirit of the tagline "Simplified HTTP requests"
jstewmon: @ewolfe that feature is usually implemented with a discrete option `baseUrl` to disambiguate the caller's intent. I'd say that's a complementary feature to being able to create an instance, but deserving of its own PR, since it would add a config param.
knksmith57: @ewolfe thanks for taking a look! Super glad you find the feature useful.

re simplified interface: I would love to get there with `got` and have a few ideas on how to support that sort of functionality without polluting the public API-- @jstewmon's suggestion to support something like `baseUrl` is definitely one way to do it.

For what it's worth, everything from your example snippet _except_ the URI prefixing is made possible by this feature (protocol + hostname + default headers).

If there's interest, I'd be happy to put together a proposal PR. Thanks again for taking a look!
knksmith57: how do y'all feel about this @sindresorhus / @lukechilds / @brandon93s?
lukechilds: Apologies, I'm very busy on another project right now.

I haven't reviewed the code in depth but I really like the idea. Much nicer than having to manually create a wrapper function and should make writing API clients with Got a simpler too.

Patch:
diff --git a/index.js b/index.js
index 248fdb8..c001449 100644
--- a/index.js
+++ b/index.js
@@ -7,6 +7,7 @@ const {PassThrough, Transform} = require('stream');
 const urlLib = require('url');
 const fs = require('fs');
 const querystring = require('querystring');
+const extend = require('extend');
 const CacheableRequest = require('cacheable-request');
 const duplexer3 = require('duplexer3');
 const toReadableStream = require('to-readable-stream');
@@ -28,6 +29,19 @@ const errors = require('./errors');
 const getMethodRedirectCodes = new Set([300, 301, 302, 303, 304, 305, 307, 308]);
 const allMethodRedirectCodes = new Set([300, 303, 307, 308]);
 
+const defaults = {
+	retries: 2,
+	cache: false,
+	decompress: true,
+	useElectronNet: false,
+	throwHttpErrors: true,
+	headers: {
+		'user-agent': `${pkg.name}/${pkg.version} (https://github.com/sindresorhus/got)`
+	}
+};
+
+const got = create(defaults);
+
 const isFormData = body => is.nodeStream(body) && is.function(body.getBoundary);
 
 const getBodySize = async opts => {
@@ -203,7 +217,7 @@ function requestAsEventEmitter(opts) {
 							}
 
 							const lastUploaded = uploaded;
-							const headersSize = Buffer.byteLength(req._header);
+							const headersSize = req._header ? Buffer.byteLength(req._header) : 0;
 							uploaded = socket.bytesWritten - headersSize;
 
 							// Prevent the known issue of `bytesWritten` being larger than body size
@@ -511,17 +525,8 @@ function normalizeArguments(url, opts) {
 		url = urlToOptions(url);
 	}
 
-	const defaults = {
-		path: '',
-		retries: 2,
-		cache: false,
-		decompress: true,
-		useElectronNet: false,
-		throwHttpErrors: true
-	};
-
 	opts = {
-		...defaults,
+		path: '',
 		...url,
 		protocol: url.protocol || 'http:', // Override both null/undefined with default protocol
 		...opts
@@ -534,10 +539,7 @@ function normalizeArguments(url, opts) {
 		}
 	}
 
-	opts.headers = {
-		'user-agent': `${pkg.name}/${pkg.version} (https://github.com/sindresorhus/got)`,
-		...headers
-	};
+	opts.headers = headers;
 
 	if (opts.decompress && is.undefined(opts.headers['accept-encoding'])) {
 		opts.headers['accept-encoding'] = 'gzip, deflate';
@@ -641,36 +643,46 @@ function normalizeArguments(url, opts) {
 	return opts;
 }
 
-function got(url, opts) {
-	try {
-		const normalizedArgs = normalizeArguments(url, opts);
+function create(defaults = {}) {
+	function got(url, opts) {
+		try {
+			opts = extend(true, {}, defaults, opts);
+			const normalizedArgs = normalizeArguments(url, opts);
+
+			if (normalizedArgs.stream) {
+				return asStream(normalizedArgs);
+			}
 
-		if (normalizedArgs.stream) {
-			return asStream(normalizedArgs);
+			return asPromise(normalizedArgs);
+		} catch (err) {
+			return Promise.reject(err);
 		}
-
-		return asPromise(normalizedArgs);
-	} catch (err) {
-		return Promise.reject(err);
 	}
-}
 
-got.stream = (url, opts) => asStream(normalizeArguments(url, opts));
+	got.create = (opts = {}) => create(extend(true, {}, defaults, opts));
 
-const methods = [
-	'get',
-	'post',
-	'put',
-	'patch',
-	'head',
-	'delete'
-];
+	got.stream = (url, opts) => {
+		opts = extend(true, {}, defaults, opts);
+		return asStream(normalizeArguments(url, opts));
+	};
 
-for (const method of methods) {
-	got[method] = (url, options) => got(url, {...options, method});
-	got.stream[method] = (url, options) => got.stream(url, {...options, method});
-}
+	const methods = [
+		'get',
+		'post',
+		'put',
+		'patch',
+		'head',
+		'delete'
+	];
+
+	for (const method of methods) {
+		got[method] = (url, options) => got(url, {...options, method});
+		got.stream[method] = (url, options) => got.stream(url, {...options, method});
+	}
 
-Object.assign(got, errors);
+	Object.assign(got, errors);
+
+	return got;
+}
 
 module.exports = got;
diff --git a/package.json b/package.json
index 88f2aa8..5b2f73a 100644
--- a/package.json
+++ b/package.json
@@ -38,6 +38,7 @@
 		"cacheable-request": "^2.1.1",
 		"decompress-response": "^3.3.0",
 		"duplexer3": "^0.1.4",
+		"extend": "^3.0.1",
 		"get-stream": "^3.0.0",
 		"is-retry-allowed": "^1.1.0",
 		"isurl": "^1.0.0-alpha5",
diff --git a/readme.md b/readme.md
index ce9fe85..5401674 100644
--- a/readme.md
+++ b/readme.md
@@ -291,6 +291,25 @@ 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
+
+#### got.create([options])
+
+Configure a new `got` instance with default `options`:
+
+```js
+(async () => {
+	const client = got.create({headers: {'x-foo': 'bar'}});
+	const {headers} = await client.get('httpbin.org/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;
+	//=> headers2['x-foo'] === 'bar'
+	//=> headers2['x-baz'] === 'qux'
+})();
+```
+
 
 ## Errors
 


Test patch:
diff --git a/test/create.js b/test/create.js
new file mode 100644
index 0000000..5bc98be
--- /dev/null
+++ b/test/create.js
@@ -0,0 +1,67 @@
+import test from 'ava';
+import got from '..';
+import {createServer} from './helpers/server';
+
+let s;
+
+test.before('setup', async () => {
+	s = await createServer();
+
+	s.on('/', (req, res) => {
+		req.resume();
+		res.end(JSON.stringify(req.headers));
+	});
+
+	await s.listen(s.port);
+});
+
+test.after('cleanup', async () => {
+	await s.close();
+});
+
+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;
+	t.deepEqual(instanceHeaders, globalHeaders);
+});
+
+test('support instance defaults', async t => {
+	const instance = got.create({
+		headers: {
+			'user-agent': 'custom-ua-string'
+		}
+	});
+	const headers = (await instance(s.url, {json: true})).body;
+	t.is(headers['user-agent'], 'custom-ua-string');
+});
+
+test('support invocation overrides', async t => {
+	const instance = got.create({
+		headers: {
+			'user-agent': 'custom-ua-string'
+		}
+	});
+	const headers = (await instance(s.url, {
+		json: true,
+		headers: {
+			'user-agent': 'different-ua-string'
+		}
+	})).body;
+	t.is(headers['user-agent'], 'different-ua-string');
+});
+
+test('curry previous instance defaults', async t => {
+	const instanceA = got.create({
+		headers: {
+			'x-foo': 'foo'
+		}
+	});
+	const instanceB = instanceA.create({
+		headers: {
+			'x-bar': 'bar'
+		}
+	});
+	const headers = (await instanceB(s.url, {json: true})).body;
+	t.is(headers['x-foo'], 'foo');
+	t.is(headers['x-bar'], 'bar');
+});