Skip to content

Latest commit

 

History

History
266 lines (203 loc) · 6.6 KB

lets-make-a-plugin.md

File metadata and controls

266 lines (203 loc) · 6.6 KB

Let's make a plugin!

Another example on how to use Got like a boss 🔌

Okay, so you already have learned some basics. That's great!

When it comes to advanced usage, custom instances are really helpful. For example, take a look at gh-got. It looks pretty complicated, but... it's really not.

Before we start, we need to find the GitHub API docs.

Let's write down the most important information:

  1. The root endpoint is https://api.github.com/.
  2. We will use version 3 of the API.
    The Accept header needs to be set to application/vnd.github.v3+json.
  3. The body is in a JSON format.
  4. We will use OAuth2 for authorization.
  5. We may receive 400 Bad Request or 422 Unprocessable Entity.
    The body contains detailed information about the error.
  6. Pagination? Not yet. This is going to be a native feature of Got. We'll update this page accordingly when the feature is available.
  7. Rate limiting. These headers are interesting:
  • X-RateLimit-Limit
  • X-RateLimit-Remaining
  • X-RateLimit-Reset
  • X-GitHub-Request-Id

Also X-GitHub-Request-Id may be useful.

  1. User-Agent is required.

When we have all the necessary info, we can start mixing 🍰

The root endpoint

Not much to do here, just extend an instance and provide the prefixUrl option:

const got = require('got');

const instance = got.extend({
	prefixUrl: 'https://api.github.com'
});

module.exports = instance;

v3 API

GitHub needs to know which version we are using. We'll use the Accept header for that:

const got = require('got');

const instance = got.extend({
	prefixUrl: 'https://api.github.com',
	headers: {
		accept: 'application/vnd.github.v3+json'
	}
});

module.exports = instance;

JSON body

We'll use options.responseType:

const got = require('got');

const instance = got.extend({
	prefixUrl: 'https://api.github.com',
	headers: {
		accept: 'application/vnd.github.v3+json'
	},
	responseType: 'json'
});

module.exports = instance;

Authorization

It's common to set some environment variables, for example, GITHUB_TOKEN. You can modify the tokens in all your apps easily, right? Cool. What about... we want to provide a unique token for each app. Then we will need to create a new option - it will default to the environment variable, but you can easily override it.

Let's use handlers instead of hooks. This will make our code more readable: having beforeRequest, beforeError and afterResponse hooks for just a few lines of code would complicate things unnecessarily.

Tip: it's a good practice to use hooks when your plugin gets complicated. Try not to overload the handler function, but don't abuse hooks either.

const got = require('got');

const instance = got.extend({
	prefixUrl: 'https://api.github.com',
	headers: {
		accept: 'application/vnd.github.v3+json'
	},
	responseType: 'json',
	token: process.env.GITHUB_TOKEN,
	handlers: [
		(options, next) => {
			// Authorization
			if (options.token && !options.headers.authorization) {
				options.headers.authorization = `token ${options.token}`;
			}

			return next(options);
		}
	]
});

module.exports = instance;

Errors

We should name our errors, just to know if the error is from the API response. Superb errors, here we come!

...
	handlers: [
		(options, next) => {
			// Authorization
			if (options.token && !options.headers.authorization) {
				options.headers.authorization = `token ${options.token}`;
			}

			// Don't touch streams
			if (options.isStream) {
				return next(options);
			}

			// Magic begins
			return (async () => {
				try {
					const response = await next(options);

					return response;
				} catch (error) {
					const {response} = error;

					// Nicer errors
					if (response && response.body) {
						error.name = 'GitHubError';
						error.message = `${response.body.message} (${response.statusCode} status code)`;
					}

					throw error;
				}
			})();
		}
	]
...

Rate limiting

Umm... response.headers['x-ratelimit-remaining'] doesn't look good. What about response.rateLimit.limit instead?
Yeah, definitely. Since response.headers is an object, we can easily parse these:

const getRateLimit = (headers) => ({
	limit: parseInt(headers['x-ratelimit-limit'], 10),
	remaining: parseInt(headers['x-ratelimit-remaining'], 10),
	reset: new Date(parseInt(headers['x-ratelimit-reset'], 10) * 1000)
});

getRateLimit({
	'x-ratelimit-limit': '60',
	'x-ratelimit-remaining': '55',
	'x-ratelimit-reset': '1562852139'
});
// => {
// 	limit: 60,
// 	remaining: 55,
// 	reset: 2019-07-11T13:35:39.000Z
// }

Let's integrate it:

const getRateLimit = (headers) => ({
	limit: parseInt(headers['x-ratelimit-limit'], 10),
	remaining: parseInt(headers['x-ratelimit-remaining'], 10),
	reset: new Date(parseInt(headers['x-ratelimit-reset'], 10) * 1000)
});

...
	handlers: [
		(options, next) => {
			// Authorization
			if (options.token && !options.headers.authorization) {
				options.headers.authorization = `token ${options.token}`;
			}

			// Don't touch streams
			if (options.isStream) {
				return next(options);
			}

			// Magic begins
			return (async () => {
				try {
					const response = await next(options);

					// Rate limit for the Response object
					response.rateLimit = getRateLimit(response.headers);

					return response;
				} catch (error) {
					const {response} = error;

					// Nicer errors
					if (response && response.body) {
						error.name = 'GitHubError';
						error.message = `${response.body.message} (${response.statusCode} status code)`;
					}

					// Rate limit for errors
					if (response) {
						error.rateLimit = getRateLimit(response.headers);
					}

					throw error;
				}
			})();
		}
	]
...

The frosting on the cake: User-Agent header.

const package = require('./package');

const instance = got.extend({
	...
	headers: {
		accept: 'application/vnd.github.v3+json',
		'user-agent': `${package.name}/${package.version}`
	}
	...
});

Woah. Is that it?

Yup. View the full source code here. Here's an example of how to use it:

const ghGot = require('gh-got');

(async () => {
	const response = await ghGot('users/sindresorhus');
	const creationDate = new Date(response.created_at);

	console.log(`Sindre's GitHub profile was created on ${creationDate.toGMTString()}`);
	// => Sindre's GitHub profile was created on Sun, 20 Dec 2009 22:57:02 GMT
})();

Did you know you can mix many instances into a bigger, more powerful one? Check out the Advanced Creation guide.