You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

README.md 11KB

1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. <div align="center">
  2. # portal
  3. > HTTP API clients ... simplified.
  4. </div>
  5. ## Motivation
  6. > Inspired when developing an internal API client in my company [Yamsafer](https://github.com/Yamsafer) :heart:, and by the design of the [Cloudflare NodeJS client](https://github.com/cloudflare/node-cloudflare) :heart:.
  7. This library aims to simplify the creation of HTTP API clients by providing a declarative abstraction of HTTP requests.
  8. Instead of worrying about how to consume some HTTP API, we should focus on the buisness logic behind this API call, so instead of worrying about whether the HTTP request library uses promises, or callbacks, or how the response object is formated, you simply declare what you want, and move on with your life.
  9. So instead of having the focus be centered around how you make the request like the following,
  10. ```js
  11. import request from 'request' // or who knows what you wanna use ¯\_(ツ)_/¯
  12. function myApiWrapper (arg1, arg2) {
  13. return new Promise((resolve, reject) => {
  14. request(
  15. `https://some.api/some/url/${arg1}/some-resource/${arg2}`,
  16. /*other options who knows,*/
  17. (error, response, data) => {
  18. if (error) reject(error)
  19. resolve(response) // or data ¯\_(ツ)_/¯
  20. }
  21. )
  22. })
  23. }
  24. /// TOO MUCH BOILERPLATE !!
  25. ```
  26. The above boilerplate where you worry about whether you're using `request` or `request-promise` or whatnot, and you worry about how to resolve your response and what it looks like, get completely abstracted and unified into,
  27. ```js
  28. import portal from '@ojizero/portal'
  29. const portalBase = portal({ baseUrl: 'https://some.api' })
  30. const myApiWrapper = portalBase.route({ path: '/some/url/:arg1/some-resource/:arg2' })
  31. /// Woosh done :D
  32. ```
  33. And you gain the consistent response structure (placed inside a promise). This can be extended even further into building entire clients for you APIs!
  34. It also adds support for standardized validation for request arguments, query strings, and payload (provided using [Joi](https://github.com/hapijs/joi) :heart:)
  35. ## Installation
  36. With NPM
  37. ```
  38. npm i -S @ojizero/portal
  39. ```
  40. Or if you're into Yarn
  41. ```
  42. yarn add @ojizero/portal
  43. ```
  44. <!-- We separate installation of `got` and `portal` to prepare for later support of multiple internal clients, mainly to support browsers using `ky` without introducing breaking changes. -->
  45. ## Usage
  46. Aimed to be used as a building block for API client libraries
  47. ```typescript
  48. /// In your library or definition file
  49. import portal from '@ojizero/portal'
  50. const client = portal({ baseUrl: 'some.base.url' }) // Initial configuration can be passed here
  51. // Get method without path variables
  52. export const someGetMethod = client.route({ path: '/some/path' })
  53. // Get method with path variables
  54. export const someGetMethodWithParam = client.route({ path: '/some/path/:withInnerVariable' })
  55. // Post method with path variables
  56. export const somePostMethodWithParam = client.route({ path: '/some/path/:withInnerVariable', method: 'POST' })
  57. /// NOTE: ideally this wouldn't be a module level instance but this is to simplify this example 😬
  58. /* ******************* */
  59. /// In your application
  60. import * as YourAPIClient from 'your-client-module'
  61. const someGetMethodPromise = YourAPIClient.someGetMethod() // GET http://some.base.url/some/path
  62. const someGetMethodWithParamPromise = YourAPIClient.someGetMethodWithParam(5) // GET http://some.base.url/some/path/5
  63. const somePostMethodWithParamPromise = YourAPIClient.someGetMethodWithParam(5, { payload: { some: 'payload' } }) // POST http://some.base.url/some/path/5 { some: 'payload' }
  64. ```
  65. Examples can be found in the [`examples`](./examples) folder
  66. ## API Documentation
  67. ### (default export) createPortalClient(config)
  68. Used to create a portal base client. Returns a [portal object](####portal-object)
  69. #### config
  70. > All config options are required unless otherwise stated.
  71. ##### baseUrl
  72. Type: `string` (required).
  73. The base URL used by the client, all related URIs are prepended by it.
  74. ##### headers
  75. Type: `OutgoingHttpHeaders`
  76. The default headers to be attached to all requests.
  77. ##### authentication
  78. Type: `Authentication`
  79. ###### Authentication
  80. **type**
  81. Type: `AuthenticationTypes` (required)
  82. Valid `AuthenticationTypes` are:
  83. - `None` = 'none'
  84. - No authentication required. (same as not providing the `authentication` option)
  85. - `BasicAuth` = 'basic',
  86. - Use `username` and `password` to generate a token.
  87. - Token is added to the `Authorization` header prepended with `basic`.
  88. - `BearerAuth` = 'bearer',
  89. - Use `authToken` as is.
  90. - Token is added to the `Authorization` header prepended with `bearer`.
  91. **username**
  92. Type: `string`
  93. Required if using `BasicAuth` type.
  94. **password**
  95. Type: `string`
  96. Required if using `BasicAuth` type.
  97. **authToken**
  98. Type: `string`
  99. Required if using `BearerAuth` type.
  100. ##### retries
  101. Type: `number`
  102. Number of retries to execute on failures, default is `0`.
  103. ##### timeout
  104. Type: `number`
  105. Request timeout in seconds, default is `30`
  106. ##### onError
  107. Type: `'reject'` | `'resolve'`
  108. > TODO: I think it's not working currently 🤔
  109. If set to `resolve` error won't throw, instead will return a normal response, default is `reject`.
  110. #### Portal Object
  111. The returns object has three attributes,
  112. ##### route
  113. A function (representing a MethodFactory), used to generate client routes. It takes for an input a [MethodSpec](######methodspec) and returns a function representing the API call.
  114. ###### MethodSpec
  115. An object with the following attributes
  116. **`path`** (required) `string`
  117. The URL path for the route, it should be a relateive path given the [baseUrl](#####baseurl) defined when initiating the portal client.
  118. **`method`** (optional) `string`
  119. The method for the HTTP call, defaults to `GET`.
  120. **`params`** (optional) [`ValdiationSpec`]()
  121. An optional validation specification for the path arguments in of the route.
  122. **`body`** (optional) [`ValdiationSpec`]()
  123. An optional validation specification for the payload arguments of the route.
  124. **`queryString`** (optional) [`ValdiationSpec`]()
  125. An optional validation specification for the query string arguments in of the route.
  126. **`contentType`** (optional) `string`
  127. The Content-Type header value of the request, defaults to `application/json`.
  128. **`accept`** (optional) `string`
  129. The Accept header value of the request, defaults to `application/json`.
  130. **`headers`** (optional) `OutgoingHttpHeaders`
  131. Additional headers to always be added to requests to the given route, defaults to `{}`.
  132. ###### Usage example
  133. ```js
  134. /* --- snip --- */
  135. const getRoute = portalObject.route({ path: '/some/base/path' })
  136. await getRoute() // GET /some/base/path
  137. await getRoute({ queryString: { a: 10 } }) // GET /some/base/path?a=10
  138. /* --- snip --- */
  139. const postRoute = portalObject.route({ path: '/some/base/path', method: 'POST' })
  140. await postRoute({ payload: { some: 'payload' } }) // POST /some/base/path { some: 'payload' }
  141. /* --- snip --- */
  142. ```
  143. ###### RouteFunction
  144. Type: `(...args: any[]): Promise<Response>`
  145. A function that takes any number of arguments and performs the HTTP request.
  146. The only special argument is the last one, which can be an object hodling any request options that can be used by the underlying client, as well as the payload (under the key `payload`) and query string (under the key `queryString`) objects if needed.
  147. Example calls would be
  148. ```js
  149. getMethod(arg1, arg2, /*...,*/ argn)
  150. getMethod(arg1, arg2, /*...,*/ argn, { queryString: { q: 'hello' } })
  151. postMethod(arg1, arg2, /*...,*/ argn, { payload: { q: 'hello' } })
  152. ```
  153. All calls to a RouteFunction produce a promise that resolves to an oject of [`Response`](######response) type.
  154. ###### Response
  155. An object with the following attributes,
  156. - `status`, which is an object holding the HTTP status both as a message and a code.
  157. - `body`, any response body returns from the HTTP request
  158. - `headers`, HTTP headers returned for the response
  159. - `_rawResponse`, the raw underlying HTTP response object (from the underlying client library)
  160. ```ts
  161. export interface Response {
  162. status: {
  163. code?: number,
  164. word?: string,
  165. },
  166. body: any,
  167. headers: IncomingHttpHeaders,
  168. [Symbol.for('portal:symbols:raw-response')]: RawResponse,
  169. }
  170. ```
  171. ##### resource
  172. A function (representing a ResourceFactory), and can be used to generate client resources. A resource is a basic CRUD API for a given route, providing a default set of APIs for `list`, `get`, `edit`, `add`, and `delete`, those APIs correspond to the following HTTP calls,
  173. - list -> `GET /some-uri`
  174. - get -> `GET /some-uri/:id`
  175. - edit -> `PUT /some-uri/:id { some payload to update the ID with }`
  176. - add (aliased as set) `POST /some-uri { some payload to create a new reource with }`
  177. - del (aliased as delete) `DELETE /some-uri/:id`
  178. It takes a [`ResourceConfig`](######resourceconfig) object as input and returns a ResourceFactory function. When calling the ResourceFactory, the result is an object with the list of CRUD operations (and any additional ones defined in the resource config) as function defined on it.
  179. The result from using the resource factory, is an object with the enabled default resource method, and any other extra ones as its keys, and each of those keys poiting to the `RouteFunction` used to execute the API call.
  180. ###### ResourceConfig
  181. An object with the following attributes,
  182. **`baseRoute`** (required) `string`
  183. The base API route to generate the CRUD around.
  184. **`enabledRoutes`** (optional) `Array<string>`
  185. The list of routes to enable, by default all basic CRUD APIs are enabled.
  186. **`extraMethods`** (optional) `{ [k:string]: MethodSpec }`
  187. A JSON with string keys mapping to corresponding [MethodSpec](######methodspec). Each key will be added an added method defined by it's corresponding spec. Defaults to `{}`.
  188. ###### Usage example
  189. ```js
  190. /* --- snip --- */
  191. const apiResource = portalObject.resource({ baseRoute: '/some/base/path' })
  192. /**
  193. * apiResource has the following methods on it
  194. * list, get, edit, add, set, del, delete
  195. * corresponding the default set of APIs in an HTTP resource
  196. */
  197. await apiResource.list() // GET /some/base/path
  198. await apiResource.get(someId) // GET /some/base/path/:someId
  199. // ... etc
  200. /* --- snip --- */
  201. ```
  202. Each method defined on the resource object follows the type [RouteFunction](######routefunction).
  203. ##### _client
  204. The underlying client instance used by the two previous factories. This is exposed only for transparncy but is not intended to be used ideally by anyone external.
  205. ##### Usage of the portal object
  206. RouteFunction
  207. ## Status
  208. This is still a work in progress :D any help is appreciated
  209. ### TODO
  210. - [x] Finish documentations
  211. - [x] Why?
  212. - [x] More on external API
  213. - [x] Support non JSON payload
  214. - [x] Get `onError: resolve` to work
  215. - [x] Support simplified form for validation
  216. - [x] Finalize behvaiour of genreator method functions
  217. - [x] What to do with their arguments (ambiguity of options/payload/querystring in args)
  218. - [x] Document RouteFunction behvaiour
  219. - [ ] Browser support through Ky ?
  220. ## License
  221. [MIT licensed](LICENSE).