UNPKG

retrofit-axios-ts

Version:

A declarative and axios based retrofit implementation for JavaScript and TypeScript.

897 lines (720 loc) 31.5 kB
# retrofit-axios-ts Thanks to [nullcc](https://github.com/nullcc) for providing us with the codebase from [ts-retrofit](https://www.npmjs.com/package/ts-retrofit) ## The main difference between this version and the original ts-retrofit version is: - Support for rejected request functions on interceptors - [AbortSignal](#abortsignal) decorator which allow setting signal for the request by sending it as parameter - [DownloadProgress](#downloadprogress) and [UploadProgress](#uploadprogress) decorators which will allow setting callbacks for handling progress > A declarative and [axios](https://github.com/axios/axios) based retrofit implementation for JavaScript and TypeScript. ## Install ```bash $ npm i retrofit-axios-ts ``` ## Quick Overview Here is a typical service definition and usage: ```typescript import { GET, POST, PUT, PATCH, DELETE, BasePath, Header, Path, Body, BaseService, ServiceBuilder, Response, } from "retrofit-axios-ts"; interface User { id?: number; name: string; email: string; } @BasePath("/api/v1") class UserService extends BaseService { @GET("/users") async getUsers( @Header("Authorization") authorization: string, ): Promise<Response<Array<User>>> { return <Response<Array<User>>>{}; } @GET("/users/{userId}") async getUser( @Header("Authorization") authorization: string, @Path("userId") userId: number, ): Promise<Response<User>> { return <Response<User>>{}; } @POST("/users") async createUser( @Header("Authorization") authorization: string, @Body user: User, ): Promise<Response> { return <Response>{}; } @PUT("/users/{userId}") async updateUser( @Header("Authorization") authorization: string, @Path("userId") userId: number, @Body user: User, ): Promise<Response> { return <Response>{}; } @PATCH("/users/{userId}") async patchUser( @Header("Authorization") authorization: string, @Path("userId") userId: number, @Body user: Partial<User>, ): Promise<Response> { return <Response>{}; } @DELETE("/users/{userId}") async deleteUser( @Header("Authorization") authorization: string, @Path("userId") userId: number, ): Promise<Response> { return <Response>{}; } } (async () => { const authorization = "foobar"; const userService = new ServiceBuilder() .setEndpoint("https://www.your-host.com") .build(UserService); const response = await userService.getUsers(authorization); // Now you can get response data from response.data })(); ``` You can see [test file](test/retrofit-axios-ts.test.ts) to get more examples. ## ServiceBuilder Example: ```typescript import { AxiosRequestConfig } from "axios"; import { ServiceBuilder, RequestInterceptors, ResponseInterceptors } from "retrofit-axios-ts"; @BasePath("/api/v1") class ItemService extends BaseService { @GET("/items") async getItems(): Promise<Response<Array<Item>>> { return <Response<Array<Item>>> {} }; } const requestInterceptors: RequestInterceptors = { fulfilled: (config) => { console.log("Before sending request to server."); return config; }, rejected: (e) => { console.log("Request interceptor error."); return Promise.reject(e); }, }; const responseInterceptors: ResponseInterceptors = { fulfilled: (config) => { console.log("After receiving response from server."); return config; }, rejected: (e) => { console.log("Response interceptor error."); return Promise.reject(e) }, }; (async () => { const itemService = new ServiceBuilder() .setEndpoint(${ENDPOINT}) .setStandalone(true) .setRequestInterceptors(requestInterceptors) .setResponseInterceptors(responseInterceptors) .build(ItemService); const response: any = await itemService.getVersion(); console.log(response.data); })(); // outputs: // Before sending request to server. // After receiving response from server. // <response data> ``` ## Log You can set log callback to print some information after request finished (ok/error): ```typescript @BasePath("") export class HealthService extends BaseService { @GET("/ping") @ResponseStatus(200) async ping(): Promise<Response> { return <Response>{}; } } const myLogCallback = (config: RequestConfig, response: Response) => { const log = `[${config.method}] ${config.url} ${response.status}`; console.log(log); // [GET] http://localhost:12345/ping 200 }; const service = new ServiceBuilder() .setEndpoint("http://localhost:12345") .setLogCallback(myLogCallback) .build(HealthService); // or use this service.setLogCallback(myLogCallback); const response = await service.ping(); ``` ## Decorators ### BasePath - Position: Class `BasePath` decorator declares the path prefix of a service. ```typescript // The common path of ItemService is ${ENDPOINT}/api/v1 @BasePath("/api/v1") class ItemService extends BaseService {} ``` ### HTTP Method Decorators All HTTP method decorators have an optional second parameter with type **HttpMethodOptions**: ```typescript export interface HttpMethodOptions { ignoreBasePath?: boolean; } ``` #### GET - Position: Method `GET` decorator declares that what it decorated use HTTP **GET** method to request server. ```typescript @BasePath("/api/v1") class ItemService extends BaseService { // GET ${ENDPOINT}/api/v1/items @GET("/items") async getItems(): Promise<Response<Array<Item>>> { return <Response<Array<Item>>>{}; } // GET ${ENDPOINT}/items @GET("/items", { ignoreBasePath: true }) async getItemsWithoutBasePath(): Promise<Response<Array<Item>>> { return <Response<Array<Item>>>{}; } } ``` #### POST - Position: Method `POST` decorator declares that what it decorated use HTTP **POST** method to request server. ```typescript @BasePath("/api/v1") class ItemService extends BaseService { // POST ${ENDPOINT}/api/v1/items @POST("/items") async createItem(@Body item: Item): Promise<Response> { return <Response>{}; } } ``` #### PUT - Position: Method `PUT` decorator declares that what it decorated use HTTP **PUT** method to request server. ```typescript @BasePath("/api/v1") class ItemService extends BaseService { // PUT ${ENDPOINT}/api/v1/items/{itemId} @PUT("/items/{itemId}") async updateItem( @Path("itemId") itemId: number, @Body item: Item, ): Promise<Response> { return <Response>{}; } } ``` #### PATCH - Position: Method `PATCH` decorator declares that what it decorated use HTTP **PATCH** method to request server. ```typescript @BasePath("/api/v1") class ItemService extends BaseService { // PATCH ${ENDPOINT}/api/v1/items/{itemId} @PATCH("/items/{itemId}") async patchItem( @Path("itemId") itemId: number, @Body item: Partial<Item>, ): Promise<Response> { return <Response>{}; } } ``` #### DELETE - Position: Method `DELETE` decorator declares that what it decorated use HTTP **DELETE** method to request server. ```typescript @BasePath("/api/v1") class ItemService extends BaseService { // DELETE ${ENDPOINT}/api/v1/items/{itemId} @DELETE("/items/{itemId}") async deleteItem(@Path("itemId") itemId: number): Promise<Response> { return <Response>{}; } } ``` #### HEAD - Position: Method `HEAD` decorator declares that what it decorated use HTTP **HEAD** method to request server. ```typescript @BasePath("/api/v1") class FileService extends BaseService { // HEAD ${ENDPOINT}/api/v1/files/{fileId} @HEAD("/files/{fileId}") async getFileMetaInfo(@Path("fileId") fileId: number): Promise<Response> { return <Response>{}; } } ``` #### OPTIONS - Position: Method `OPTIONS` decorator declares that what it decorated use HTTP **OPTIONS** method to request server. ```typescript @BasePath("/api/v1") class ItemService extends BaseService { // OPTIONS ${ENDPOINT}/api/v1/items/{itemId} @OPTIONS("/items/{itemId}") async getFileMetaInfo(@Path("itemId") itemId: number): Promise<Response> { return <Response>{}; } } ``` ### Headers - Position: Method `Headers` decorator declares that what **static HTTP headers** should be added to request. ```typescript @BasePath("") export class AuthService extends BaseService { @POST("/oauth/token") @Headers({ "Content-Type": "application/x-www-form-urlencoded;charset=utf-8", Accept: "application/json", }) async auth(@Body body: OAuth): Promise<Response<Token>> { return <Response<Token>>{}; } } ``` ### Header - Position: Method Parameter `Header` decorator parameterizes the header in HTTP request. Client can provide a value to **one header**. ```typescript @BasePath("/api/v1") class ItemService extends BaseService { // GET ${ENDPOINT}/api/v1/items async getItems( @Header("Authorization") authorization: string, ): Promise<Response<Array<Item>>> { return <Response<Array<Item>>>{}; } } ``` ### HeaderMap - Position: Method Parameter `HeaderMap` decorator parameterizes the headers in HTTP request. Client can provide values to **multi headers**. ```typescript @BasePath("/api/v1") class ItemService extends BaseService { // GET ${ENDPOINT}/api/v1/items async getItems(@HeaderMap headers: any): Promise<Response<Array<Item>>> { return <Response<Array<Item>>>{}; } } ``` ### Path - Position: Method Parameter `Path` decorator parameterizes the part of path in HTTP request. ```typescript @BasePath("/api/v1") class ItemService extends BaseService { // GET ${ENDPOINT}/api/v1/items/{itemId} @GET("/items/{itemId}") async getItem(@Path("itemId") itemId: number): Promise<Response<Item>> { return <Response<Item>>{}; } } ``` ### Body - Position: Method Parameter `Body` decorator parameterizes the body in HTTP request. ```typescript @BasePath("/api/v1") class ItemService extends BaseService { // POST ${ENDPOINT}/api/v1/items @POST("/items") async createItem(@Body item: Item): Promise<Response> { return <Response>{}; } } ``` ### QueryArrayFormat - Position: Method `QueryArrayFormat` decorator declares that what kind of array format should be used in query. ```typescript @BasePath("/api/v1") class ItemService extends BaseService { // getItemsWithQueryArrayFormatIndices(["food", "book", "pet"]) // GET ${ENDPOINT}/api/v1/items?categories[0]=food&categories[1]=book&categories[2]=pet @GET("/items") @QueryArrayFormat("indices") async getItemsWithQueryArrayFormatIndices( @Query("categories") categories: string[], ): Promise<Response<Array<Item>>> { return <Response<Array<Item>>>{}; } // getItemsWithQueryArrayFormatBrackets(["food", "book", "pet"]) // GET ${ENDPOINT}/api/v1/items?categories[]=food&categories[]=book&categories[]=pet @GET("/items") @QueryArrayFormat("brackets") async getItemsWithQueryArrayFormatBrackets( @Query("categories") categories: string[], ): Promise<Response<Array<Item>>> { return <Response<Array<Item>>>{}; } // getItemsWithQueryArrayFormatRepeat(["food", "book", "pet"]) // GET ${ENDPOINT}/api/v1/items?categories=food&categories=book&categories=pet @GET("/items") @QueryArrayFormat("repeat") async getItemsWithQueryArrayFormatRepeat( @Query("categories") categories: string[], ): Promise<Response<Array<Item>>> { return <Response<Array<Item>>>{}; } // getItemsWithQueryArrayFormatComma(["food", "book", "pet"]) // GET ${ENDPOINT}/api/v1/items?categories=food,book,pet @GET("/items") @QueryArrayFormat("comma") async getItemsWithQueryArrayFormatComma( @Query("categories") categories: string[], ): Promise<Response<Array<Item>>> { return <Response<Array<Item>>>{}; } } ``` ### Queries - Position: Method `Queries` decorator declares that what **static queries** should be added to request. ```typescript @BasePath("/api/v1") class ItemService extends BaseService { // GET ${ENDPOINT}/api/v1/items?size=20 @GET("/items") @Queries({ size: 20, }) async getItems(): Promise<Response<Array<Item>>> { return <Response<Array<Item>>>{}; } } ``` ### Query - Position: Method Parameter `Query` decorator parameterizes the query in HTTP request. Client can provide a value to **one query parameter**. ```typescript @BasePath("/api/v1") class ItemService extends BaseService { // GET ${ENDPOINT}/api/v1/items?size=20 @GET("/items") async getItems(@Query("size") size: number): Promise<Response<Array<Item>>> { return <Response<Array<Item>>>{}; } } ``` ### QueryMap - Position: Method Parameter `QueryMap` decorator parameterizes the queries in HTTP request. Client can provide values to **multi queries**. ```typescript @BasePath("") class SearchService extends BaseService { // GET ${ENDPOINT}/search?a=foo&b=bar @GET("/search") async search(@QueryMap query: SearchQuery): Promise<Response<SearchResult>> { return <Response<SearchResult>>{}; } } ``` ### FormUrlEncoded - Position: Method `FormUrlEncoded` declares that the content type is **application/x-www-form-urlencoded;charset=utf-8** in HTTP request. ```typescript @BasePath("") export class AuthService extends BaseService { @POST("/oauth/token") @FormUrlEncoded async auth(@Body body: OAuth): Promise<Response<Token>> { return <Response<Token>>{}; } } ``` ### Field - Position: Method Parameter `Field` decorator parameterizes the field in HTTP request. It only takes effect when method is decorated by **@FormUrlEncoded**. ```typescript @BasePath("") export class AuthService extends BaseService { @POST("/oauth/token") @FormUrlEncoded async auth( @Field("username") username: string, @Field("password") password: string, ): Promise<Response<Token>> { return <Response<Token>>{}; } } ``` ### FieldMap - Position: Method Parameter `FieldMap` decorator parameterizes the field in HTTP request. It only takes effect when method is decorated by **@FormUrlEncoded**. ```typescript @BasePath("") export class AuthService extends BaseService { @POST("/oauth/token") @FormUrlEncoded async auth(@FieldMap fields: OAuth): Promise<Response<Token>> { return <Response<Token>>{}; } } ``` ### Multipart - Position: Method `Multipart` decorator declares that the content type is **multipart/form-data** in HTTP request. ```typescript @BasePath("/api/v1") export class FileService extends BaseService { @POST("/upload") @Multipart async upload( @Part("bucket") bucket: PartDescriptor<string>, @Part("file") file: PartDescriptor<Buffer>, ): Promise<Response> { return <Response>{}; } } ``` ### Part - Position: Method Parameter `Part` decorator parameterizes the part in HTTP request. It only takes effect when method is decorated by **@Multipart**. ```typescript @BasePath("/api/v1") export class FileService extends BaseService { @POST("/upload") @Multipart async upload( @Part("bucket") bucket: PartDescriptor<string>, @Part("file") file: PartDescriptor<Buffer>, ): Promise<Response> { return <Response>{}; } } ``` ### ResponseType - Position: Method - Options: 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream' `ResponseType` decorator declares response type in axios config. ```typescript @BasePath("/api/v1") export class FileService extends BaseService { @GET("/file") @ResponseType("stream") async getFile(@Path("fileId") fileId: string): Promise<Response> { return <Response>{}; } } ``` ### RequestTransformer - Position: Method `RequestTransformer` decorator provides a hook to handle request data before sending request to server. ```typescript @BasePath(API_PREFIX) export class TransformerService extends BaseService { @POST("/request-transformer") @RequestTransformer((data: any, headers?: any) => { data.foo = "foo"; // add something to request data return JSON.stringify(data); }) async createSomething(@Body body: Something): Promise<Response> { return <Response>{}; } } ``` ### ResponseTransformer - Position: Method `ResponseTransformer` decorator provides a hook to handle response data after receiving response from server. ```typescript @BasePath(API_PREFIX) export class TransformerService extends BaseService { @POST("/request-transformer") @RequestTransformer((data: any, headers?: any) => { data.foo = "foo"; // add something to response data return JSON.stringify(data); }) async createSomething(@Body body: Something): Promise<Response> { return <Response>{}; } } ``` ### Timeout - Position: Method `Timeout` decorator declares timeout in axios config. ```typescript @BasePath("/api/v1") class ItemService extends BaseService { // GET ${ENDPOINT}/api/v1/items @GET("/items") @Timeout(3000) async getItems(): Promise<Response<Array<Item>>> { return <Response<Array<Item>>>{}; } } ``` ### ResponseStatus - Position: Method `ResponseStatus` decorator declares status code for method, do nothing just a declaration. ```typescript @BasePath("/api/v1") class ItemService extends BaseService { // GET ${ENDPOINT}/api/v1/items @GET("/items") @Timeout(3000) async getItems(): Promise<Response<Array<Item>>> { return <Response<Array<Item>>>{}; } } ``` ### Config - Position: Method `Config` decorator provides a direct way to set config for a request in axios. ```typescript @BasePath("/api/v1") export class ConfigService extends BaseService { @GET("/config") @Config({ maxRedirects: 1, }) async getConfig(): Promise<Response> { return <Response>{}; } } ``` ### AbortSignal - Position: Method Parameter `AbortSignal` decorator provides a direct way to set an abort controller signal for a request in axios. ```typescript @BasePath("/api/v1") export class AbortSignalService extends BaseService { @GET("/abort") async setAbortSignal( @AbortSignal abortSignal: AbortSignal, ): Promise<Response> { return <Response>{}; } } ``` ### DownloadProgress - Position: Method Parameter `DownloadProgress` decorator provides a direct way to set an onDownloadProgress callback for a request in axios. ```typescript @BasePath("/api/v1") export class ProgressService extends BaseService { @GET("/download") async downloadFile( @DownloadProgress callback: (axiosProgressEvent: ProgressEvent) => void, ): Promise<Response> { return <Response>{}; } } ``` ### UploadProgress - Position: Method Parameter `UploadProgress` decorator provides a direct way to set an onUploadProgress callback for a request in axios. ```typescript @BasePath("/api/v1") export class ProgressService extends BaseService { @POST("/upload") @Multipart async uploadFile( @Part("file") file: PartDescriptor<Buffer>, @UploadProgress callback: (axiosProgressEvent: ProgressEvent) => void, ): Promise<Response> { return <Response>{}; } } ``` ### GraphQL and GraphQLVariables - Position: Method `GraphQL` decorator declares query for a GraphQL request. `GraphQLVariables` decorator declares variables for a GraphQL request. ```typescript const gqlQuery = `query ($name: String!, $owner: String!) { viewer { name location } repository(name: $name, owner: $owner) { stargazerCount forkCount } }`; @BasePath("") export class GraphQLService extends BaseService { @POST("/graphql") @GraphQL(gqlQuery, "UserAndRepo") async graphql1(@GraphQLVariables variables: any): Promise<Response> { return <Response>{}; } } ``` ### Deprecated - Position: Method `Deprecated` decorator marks a method is deprecated. ```typescript @BasePath("/api/v1") class ItemService extends BaseService { // GET ${ENDPOINT}/api/v1/items @GET("/items") @Deprecated("This method is deprecated") async getItems(): Promise<Response<Array<Item>>> { return <Response<Array<Item>>>{}; } } ``` ### Decorators Summary | Category | Name | Description | Decorator Position | Example | | :-----------------: | :------------------: | :-----------------------------------------------------------------------------------------------------------: | :----------------: | :-------------------------------------------------------------------------------------------------------------------: | | HTTP Method | @GET | GET Method | Method | @GET("/users") | | HTTP Method | @POST | POST Method | Method | @POST("/users") | | HTTP Method | @PUT | PUT Method | Method | @PUT("/users/{userId}") | | HTTP Method | @PATCH | PATCH Method | Method | @PATCH("/users/{userId}") | | HTTP Method | @DELETE | DELETE Method | Method | @DELETE("/users/{userId}") | | HTTP Method | @HEAD | HEAD Method | Method | @HEAD("/users/{userId}") | | HTTP Method | @OPTIONS | OPTIONS Method | Method | @OPTIONS("/users/{userId}") | | Base Path | @BasePath | Specifying the base path of a series of API endpoints | Class | @BasePath("/api/v1") | | Static Headers | @Headers | Specifying the static headers of API endpoint | Method | @Headers({ "content-type": "application/x-www-form-urlencoded", "Accept": "application/json" }) | | Header Parameter | @Header | Parameterized header | Method Parameter | @Header("X-Token") | | Header Parameters | @HeaderMap | Parameterized header | Method Parameter | @HeaderMap | | Path Parameter | @Path | Specifying parameter in path of API | Method Parameter | @Path("userId") | | Body | @Body | Specifying body data | Method Parameter | @Body | | Query Array Format | @QueryArrayFormat | Specifying query array format | Method | @QueryArrayFormat('repeat') | | Static Query | @Queries | Specifying static query data | Method | @Queries({ page: 1, size: 20, sort: "createdAt:desc" }) | | Query Parameter | @Query | Parameterized query | Method Parameter | @Query("group") | | Query Parameters | @QueryMap | Parameterized query | Method Parameter | @QueryMap | | Static Headers | @FormUrlEncoded | Specifying "content-type" to be "application/x-www-form-urlencoded" | Method | @FormUrlEncoded | | Field Parameter | @Field | Specifying field in method parameter, only takes effect when method has been decorated by @FormUrlEncoded | Method Parameter | @Field("name") | | Field Parameters | @FieldMap | Specifying field map in method parameter, only takes effect when method has been decorated by @FormUrlEncoded | Method Parameter | @FieldMap | | Static Headers | @Multipart | Specifying "content-type" to be "multipart/form-data" | Method | @Multipart | | Part Parameters | @Part | Specifying field map in method parameter, only takes effect when method has been decorated by @Multipart | Method Parameter | @Part("name") | | Response | @ResponseType | Specifying the response type in axios config | Method | @ResponseType("stream") | | RequestTransformer | @RequestTransformer | Specifying the request transformer in axios config | Method | @RequestTransformer((data: any, headers?: any) => { data.foo = 'foo'; return JSON.stringify(data); }) | | ResponseTransformer | @ResponseTransformer | Specifying the response transformer in axios config | Method | @ResponseTransformer((data: any, headers?: any) => { const json = JSON.parse(data); json.foo = 'foo'; return json; }) | | Timeout | @Timeout | Specifying the timeout in axios config | Method | @Timeout(5000) | | ResponseStatus | @ResponseStatus | Declare response status code for method, do nothing just a declaration | Method | @ResponseStatus(204) | | Config | @Config | A direct way to set config for a request in axios | Method | @Config({ maxRedirects: 1 }) | | AbortSignal | @AbortSignal | Sets an abort signal to the request | Method Parameter | @AbortSignal abortSignal: AbortSignal | | DownloadProgress | @DownloadProgress | Sets an onDownloadProgress callback to the request | Method Parameter | @DownloadProgress callback: (axiosProgressEvent: ProgressEvent) => void | | UploadProgress | @UploadProgress | Sets an onUploadProgress callback to the request | Method Parameter | @UploadProgress callback: (axiosProgressEvent: ProgressEvent) => void | | GraphQL | @GraphQL | Declares query for a GraphQL request. | Method | @GraphQL(gqlQuery, "operationName") | | GraphQLVariables | @GraphQLVariables | Declares variables for a GraphQL request. | Method | @GraphQLVariables | | Deprecated | @Deprecated | Marks a method is deprecated | Method | @Deprecated()<br>@Deprecated("This method is deprecated") | ## Test ```bash $ npm test ```