visit
Before jumping into the coding part, let's discuss which language we're going to use. Remember my point about defining data models? For API client it's essential to give a user more information about data that is going be returned, so we don't have to constantly switch context between documentation and IDE. It also helps to avoid bugs and typos as you write code (we all have tests in the end, haven't we?). Keeping all that in mind, the best choice at the moment is to use Typescript.
npm i -D microbundle
Now update the
package.js
with build tasks and a path to entry point:{
"source": "src/foo.js", // Your source file (same as 1st arg to microbundle)
"main": "dist/foo.js", // output path for CommonJS/Node
"module": "dist/foo.module.js", // output path for JS Modules
"unpkg": "dist/foo.umd.js", // optional, for unpkg.com
"scripts": {
"build": "microbundle", // uses "source" and "main" as input and output paths by default
"dev": "microbundle watch"
}
}
/src
/articles
index.ts // Everything that's related to articles
...
/comments
index.ts
...
/users
...
index.ts // Imports and joins all resources together
It also useful to keep resources as separate classes, so you don't need to change root
index.ts
every time you add a new method. Then you'd need to merge them together using .function applyMixins(derivedCtor: any, baseCtors: any[]) {
baseCtors.forEach(baseCtor => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
Object.defineProperty(
derivedCtor.prototype,
name,
Object.getOwnPropertyDescriptor(baseCtor.prototype, name)
);
});
});
}
class DevTo extends Base {}
interface DevTo extends Articles, Comments, Users {}
applyMixins(DevTo, [Articles, Comments, Users])
export default DevTo
fetch
is available in the browser but missing in Node.js where you should use the http
module. will help us use fetch function everywhere and switch between browser and Node.js versions automatically.request
function which wraps fetch and append authentication header:function request<T> (endpoint: string, options?: RequestInit): Promise<T> {
const url = this.basePath + endpoint
const headers = {
'api-key': this.apiKey,
'Content-type': 'application/json'
}
const config = {
...options,
headers,
}
return fetch(url, config).then(r => {
if (r.ok) {
return r.json()
}
throw new Error(r.statusText)
})
}
We always return
Promise
, so clients can chain requests together or wait for the results.describe('Article resource', () => {
test('getArticles returns a list of articles', async () => {
// Set up the mock request
const scope = nock('//dev.to/api/')
.get('/articles')
.reply(200, [{ title: 'Article' }])
// Make the request
const DevToClient = new DevTo({ apiKey: 'XYZ' })
await DevToClient.getArticles()
// Assert that the expected request was made.
scope.done()
})
})
Together we designed an API client that is small, scalable, supports Typescript out-of-the-box, and works in browser and in Node.js.
If you want to see the final code, feel free to check out the . Don’t hesitate to ask me for the details or suggest an improvement.
Previously published at //lyamkin.com/blog/how-to-build-api-client-library-in-js/