项目作者: vitorsalgado

项目描述 :
HTTP Clients with Decorators for Typescript and Javascript
高级语言: TypeScript
项目地址: git://github.com/vitorsalgado/drizzle-http.git
创建时间: 2021-02-14T23:01:01Z
项目社区:https://github.com/vitorsalgado/drizzle-http

开源协议:MIT License

下载


Drizzle HTTP


Repository Logo


Create API Clients with Decorators for Typescript and Javascript.


NPM Packages

Deno



GitHub Action Status


Deno GitHub Action Status


Codecov


NPM Package


Deno Package


Conventional Commits


What it is

Drizzle-HTTP is library inspired by Retrofit
and Feign, that let you create API clients using decorators.

Table of Contents

Installation

Drizzle-HTTP is a monorepo with several packages. You will need to install at least the core module, @drizzle-http/core,
along with one or more extensions.
For a basic usage in a Node.js Backend Environment, you can install:

  1. npm i @drizzle-http/core
  2. npm i @drizzle-http/undici

For browser environments:

  1. npm i @drizzle-http/core
  2. npm i @drizzle-http/fetch

Getting Started

By default, request and response bodies will be handled as JSON. Will can change this with the appropriate decorators.
It will not set the content type by default.

Overview

Usage typically looks like the example below:

  1. import { newAPI, Timeout } from '@drizzle-http/core'
  2. import { GET } from "@drizzle-http/core";
  3. import { Path } from "@drizzle-http/core";
  4. import { Param } from "@drizzle-http/core";
  5. import { POST } from "@drizzle-http/core";
  6. import { Body } from "@drizzle-http/core";
  7. import { HttpResponse } from "@drizzle-http/core";
  8. import { PUT } from "@drizzle-http/core";
  9. import { DELETE } from "@drizzle-http/core";
  10. import { ParseErrorBody } from "@drizzle-http/core";
  11. import { Query } from "@drizzle-http/core";
  12. import { HeaderMap } from "@drizzle-http/core";
  13. import { UndiciCallFactory } from "@drizzle-http/undici";
  14. import { ContentType } from "@drizzle-http/core";
  15. import { MediaTypes } from "@drizzle-http/core";
  16. import { RawResponse } from "@drizzle-http/core";
  17. @Timeout(15e30)
  18. @Path('/customers')
  19. @HeaderMap({ 'x-app-id': 'example-app' })
  20. @ContentType(MediaTypes.APPLICATION_JSON)
  21. class CustomerAPI {
  22. @GET()
  23. search (@Query('filter') filter: string, @Query('sort') sort: string): Promise<Customer[]> {
  24. }
  25. @GET('/{id}')
  26. @ParseErrorBody()
  27. byId (@Param('id') id: string): Promise<Customer> {
  28. }
  29. @POST()
  30. @RawResponse()
  31. add (@Body() customer: Customer): Promise<HttpResponse> {
  32. }
  33. @PUT('/{id}')
  34. @RawResponse()
  35. update (@Param('id') id: string, @Body() customer: Customer): Promise<HttpResponse> {
  36. }
  37. @DELETE('/{id}')
  38. @RawResponse()
  39. remove (@Param('id') id: string): Promise<HttpResponse> {
  40. }
  41. }
  42. const api = newAPI()
  43. .baseUrl('https://example.com')
  44. .callFactory(new UndiciCallFactory())
  45. .createAPI(CustomerAPI)
  46. const customer = await api.byId('100')

Basic Decorators

Decorator Description Target
@GET() Define a HTTP GET request. Method
@POST() Define a HTTP POST request. Method
@PUT() Define a HTTP PUT request. Method
@DELETE() Define a HTTP DELETE request. Method
@PATCH() Define a HTTP PATCH request. Method
@OPTIONS() Define a HTTP OPTIONS request. Method
@HEAD() Define a HTTP HEAD request. Method
@HTTP() Define a custom HTTP method for a request. Method
@Body() Mark the parameter that will be the request body. Parameter
@Param() Define a path parameter that will replace a {PARAM} url template value Parameter
@Query() Define a querystring parameter Parameter
@QueryName() Define a querystring name parameter Parameter
@Field() Define a form-urlencoded field parameter Parameter
@Header() Define a header parameter Parameter
@HeaderMap() Define fixed headers Class, Method
@FormUrlEncoded() Define a form-urlencoded request Class, Method
@Multipart() Create a multipart/form-data request (Fetch Only) Class, Method
@Part() Mark a parameter as a part of multipart/form-data request body (Fetch Only) Parameter
@BodyKey() Change the name of part in a multipart/form-data request (Fetch Only) Parameter
@Accept() Define Accept header. Class, Method
@ContentType() Define Content-Type header. Class, Method
@Path() Define an additional url path. The value accepts template parameters. Class
@Abort() Configure request cancellation. Pass a Event Emitter instance. Cancel with an abort event. Class, Method or Parameter
@Timeout() Define the timeouts of a request Class, Method
@ParseErrorBody() Parse error body. Can use a custom body converter Class, Method
@NoDrizzleUserAgent() Remove Drizzle-HTTP custom user-agent header Class
@JsonRequest() Use JSON request body converter (default) Class, Method
@JsonResponse() Use JSON response converter (default) Class, Method
@UseJsonConv() Use JSON request/response converters (default) Class, Method
@PlainTextRequest() Use plain text request body converter Class, Method
@PlainTextResponse() Use plain text response converter Class, Method
@UsePlainTextConv() Use plain text request/response converters Class, Method
@RequestType() Define a custom request body converter Class, Method
@ResponseType() Define a custom response converter Class, Method
@Model() Define a parameter that will hold the request definition. Used along with @To() decorator Class, Method
@To() Map @Model() class properties and methods to a request Class, Method

Defaults

Default values that Drizzle starts with. All values can be overridden using decorators.

  • Timeout: 30 seconds
  • Request Body Converter: JSON
  • Response Body Converter: JSON

Error Handling

When methods are not decorated with @RawResponse(), Drizzle throws an HttpError with the following structure:

  1. {
  2. message: 'Request failed with status code: 400',
  3. code: 'DZ_ERR_HTTP',
  4. request: {
  5. url: 'https://example.com/test,
  6. method: 'GET',
  7. headers: Headers,
  8. body: ''
  9. },
  10. response: {
  11. headers: Headers,
  12. status: 400,
  13. statusText: ''
  14. body: 'error from server'
  15. }
  16. }

When you want to parse the error response body to, for example a JSON object, use @ParseErrorBody(). By default,
@ParseErrorBody() use the same response converter used by the success scenario. If you need a different converter for
the error body, pass the name of the converter to the decorator. E.g.: @ParseErrorBody(BuiltInConv.TEXT).

Features

  • Define HTTP requests with decorators, including path parameters, querystring, headers, body and so on.
  • Extensible
  • Custom response adapters
  • Request interceptors
  • Abort requests
  • Timeouts
  • Parse responses to objects or get the raw response in a fetch like format
  • Parse error response bodies
  • RxJs support with RxJs Adapter
  • Map responses with Response Mapper Adapter
  • Circuit Breaker with Opossum with this adapter
  • Deno support

Browser

For Browser usage, take a look on this implementation. It uses fetch to make HTTP
requests.

Deno

A version for Deno is available on https://deno.land/x/drizzle_http.
The Deno version is simpler than the one available for Node.js. It contains the core module and a fetch client
implementation specific for Deno.
More details and usage example here.

Interceptors

You can intercept requests and responses using Interceptors.
You can a simple function, chain => {}, an Intepcetor interface implementation or an InterceptorFactory
implementation, if you need more configurations.
Take a look on the examples below:

  1. class CustomerAPI {
  2. @GET('/{id}')
  3. getById (@Param('id') id: string): Promise<Customer> {
  4. return noop(id)
  5. }
  6. }
  7. const api = newAPI()
  8. .addInterceptor(async chain => {
  9. console.log('before request')
  10. const response = await chain.proceed(chain.request())
  11. console.log('after request')
  12. return response
  13. })
  14. .baseUrl('https://example.com')
  15. .callFactory(new UndiciCallFactory())
  16. .createAPI(CustomerAPI)

Circuit Breaker

With the package @drizzle-http/opossum-circuit-breaker, you can protect your
endpoints with circuit breakers.
It uses Opossum circuit breaker implementation.
See a basic demonstration below. More details here.

  1. import { CircuitBreaker } from "@drizzle-http/opossum-circuit-breaker";
  2. import { Fallback } from "@drizzle-http/opossum-circuit-breaker";
  3. @Timeout(15e30)
  4. @Path('/customers')
  5. @HeaderMap({ 'x-app-id': 'example-app' })
  6. class CustomerAPI {
  7. @GET()
  8. @CircuitBreaker()
  9. search (@Query('filter') filter: string, @Query('sort') sort: string): Promise<Customer[]> {
  10. }
  11. @GET('/{id}')
  12. @CircuitBreaker()
  13. @Fallback((id: string, error: Error) => { /** fallback logic **/
  14. })
  15. byId (@Param('id') id: string): Promise<Customer> {
  16. }
  17. }

Raw HTTP Response

By the default, HTTP success responses you be parsed and resolved and http errors will be rejected. If you want the raw
HTTP response, including headers, status codes, body stream, decorate your method with @RawResponse() and the return
will be a Promise<HttpResponse>, similar to Fetch. In this case, HTTP errors will not be rejected.

Form URL Encoded

To make application/x-www-form-urlencoded request, decorate your class or method with @FormUrlEncoded().
Use @Field() to define a parameter as a form field entry.
If using @Body(), object keys will be converted to url form encoded format.

Common Issues

ESLint and TS Check Problems

The API class methods doesn’t need to have a body, but TS and some ESLint configurations will complain with the empty
body and maybe the unused parameters. To solve this, you can:

  • Use the helper function noop() in all method bodies. This function does nothing, but it will have the same return
    type as your method, and you can pass all method arguments removing any lint issues.
  • Disable TS check for the API class file with the comment: // @ts-nocheck
  • Disable or relax ESLint checks for the file or class

Example:

  1. import { noop } from "@drizzle-http/core";
  2. class CustomerAPI {
  3. @GET('/{id}')
  4. byId (@Param('id') id: string): Promise<Customer> {
  5. return noop(id)
  6. }
  7. @POST()
  8. @ParseErrorBody()
  9. @RawResponse()
  10. add (@Body() customer: Customer): Promise<HttpResponse> {
  11. return noop(customer)
  12. }
  13. }

Request/Response Mismatch

You need to be very explicitly regarding the API class and method configurations as Drizzle is unable to detect stuff
like the generic return type of methods. For example, if you want the raw response, to gain access to all the details of
a http response, you need to explicitly decorate your method with @RawResponse().

Benchmarks

Run

  1. npm run benchmark

Results

  1. Machine: MacBook Pro (13-inch, 2019)
  2. Processor: 2,8 GHz Quad-Core Intel Core i7
  3. Memory: 16 GB 2133 MHz LPDDR3
  4. Node: 15
Tests Samples Result Tolerance Difference with slowest
got 10 360.02 req/sec ± 1.99 % -
axios 10 622.72 req/sec ± 2.14 % + 72.97 %
http 10 749.67 req/sec ± 2.19 % + 108.23 %
drizzle-http - (undici) - (circuit breaker) 10 762.81 req/sec ± 2.95 % + 111.88 %
drizzle-http - (undici) 10 781.68 req/sec ± 2.22 % + 117.12 %
undici 10 799.53 req/sec ± 2.05 % + 122.08 %

This benchmark consists in a client with multiple connections performing calls to a server that responds a 80kb JSON.

Contributing

Conventional Commits
code style: prettier
lerna
jest

See CONTRIBUTING for more details.

License

Drizzle HTTP is MIT Licensed.