retroxios
v0.4.4
Published
A type-safe HTTP client for Node.js and browsers.
Downloads
47
Maintainers
Readme
Retroxios allows you to easily construct declarative yet flexible HTTP clients (powered by axios) for API services, by using decorators and leveraging the type system of TypeScript at the same time.
It automagically turns all your decorated methods into axios HTTP request calls against your API service.
You can think of this as a TypeScript port of Square's Retrofit.
By using decorators, a large amount of repetitious code can be reduced. Less code, fewer bugs, more power.
Features
- ✍🏻 More declarative code
- 🤖 Less repetitious code (DRY)
- 👨🏻💻 Better developer experience
- 🦆 Make good use of TypeScript's type system
Overview
- Define a class containing methods (as endpoints) of the API service
class ExampleService {
@GET("users/{name}", {
queries: { page: 1 }, // (default queries)
headers: { foo: "bar" }, // (default headers)
})
public async getUser(
@Path("name") name: string, // replaces '{name}' in the endpoint
@Query("page") page: number = 1, // add 'page=1' to queries
@Header("foo") foo: string = "bar", // add [foo: bar] to headers
@QuerySpread filters?: Filters // entries of this argument become key-value queries
): Promise<AxiosResponse<UserResponse>> {
// No body for this method as it will transform into an axios request call.
return nothing(name, page, foo, filters); // put all unused parameters into this function
}
}
- Create an instance of
ExampleService
viaRetroxios
// Construct a Retroxios object with a base config.
const baseURL = "https://api.example.com/";
const builder = new Retroxios({ baseURL, headers: { Accept: "application/json" } });
// Build an ExampleService instance.
const service = builder.create(ExampleService);
// You can additionally extend the base config to
// have multiple sub services deriving from a parent config.
const subService = builder.extend({ baseURL: `${baseURL}/sub/` }).create(ExampleSubService);
- Call the HTTP requests as you normally do
// GET https://api.example.com/users/john?page=3&gender=M
// with headers: [Accept: "application/json"] [foo: "bar"]
const call = service.getUser("john", 3, undefined, { gender: "M" });
call.then(({ status, data }) => console.log(status, data)).catch(console.error);
Download
$ npm install retroxios
Usage
More examples here.
import Retroxios from "retroxios";
import * as decorators from "retroxios/decorators";
Request Decorators
Support HTTP
GET
,HEAD
,POST
,PUT
,DELETE
,OPTIONS
andPATCH
methods.
The relative endpoint of the URL is specified as the first parameter in the decorator.
@GET("users/list")
The second parameter (defaults
) receives optional default queries and headers that will be added to the request
if there is no corresponding parameters or when they are undefined
.
@GET("users/list", {
queries: { page: 1, limit: 10 },
headers: { Accept: "application/json" }
})
Note that these are method decorators and must be
placed above the @Config()
method decorator (if any).
Also, these decorators should not be attached to the same method more than once.
The methods attached with these decorators should be an async
function that
returns Promise<AxiosResponse<T>>
where T
is the object type of the expected JSON response.
For example:
async getUser(): Promise<AxiosResponse<UserResponse>>
Path Manipulation
The path of a request endpoint can be updated dynamically using replacement blocks and parameters on the method.
A replacement block is an alphanumeric (_
is also allowed) key surrounded by {
and }
.
A corresponding parameter must be decorated with @Path("key")
using the same key.
@GET("group/{id}/users")
async groupList(@Path("id") groupID: number): Promise<AxiosResponse> {
return nothing(groupID);
};
You can additionally specify a default value for the path to use when the corresponding parameter is undefined
,
by using the syntax {key=(default)}
, where the default value is surrounded by (
and )
following a =
.
@GET("group/{id=(999)}/users")
async groupList(@Path("id") groupID = 999): Promise<AxiosResponse> {
return nothing(groupID);
};
Query Manipulation
Queries can be added using the @Query("key")
parameter decorator like this:
@GET("group/{id}/users")
async groupList(
@Path("id") groupID: number,
@Query("sort") sort: string
): Promise<AxiosResponse> {
return nothing(groupID, sort);
};
For complex query combinations, the @QuerySpread
parameter decorator can be used.
@GET("group/{id}/users")
async groupList(
@Path("id") groupID: number,
@QuerySpread options: Options
): Promise<AxiosResponse> {
return nothing(groupID, options);
};
Header Manipulation
Headers can be added using the @Header("key")
parameter decorator like this:
@GET("group")
async getGroup(@Header("Authorization") authorization: string): Promise<AxiosResponse> {
return nothing(authorization);
};
For complex query combinations, the @HeaderSpread
parameter decorator can be used.
@GET("group")
async getGroup(@HeaderSpread headers: GroupHeaders): Promise<AxiosResponse> {
return nothing(headers);
};
Request Body
You can supply a data body to the request using the @Body
parameter decorator like this:
@POST("users/new")
async createUser(@Body user: UserEntity): Promise<AxiosResponse> {
return nothing(user);
};
Note that this decorator is only allowed when using the POST
, PUT
and PATCH
request decorators.
Per Request Config
You can supply an additional config (AxiosRequestConfig
) for a particular request like this:
@GET("user/posts")
@Config({ ... })
async getUserPosts(): Promise<AxiosResponse> {
return nothing();
};
Some options such as url
, params
, headers
and data
may get overridden by decorated parameters.
Note that this decorator should only be attached once per method and should not be used
when defaults
arguments are supplied to the reqeust decorator at the same time.
Response Manipulator
The @Manipulate()
method decorator allows you to manipulate the response or
even change the return type of the decorated request method (such as wrap the response in another object etc.).
@GET("user/posts")
@Manipulate((response: AxiosResponse): string => response.statusText})
async getUserPosts(): Promise<string> {
return nothing();
};
Note that this decorator should only be attached once per method.
Difference with Response Interceptor
export type Interceptor<AxiosResponse> = {
onFulfilled?: (value: AxiosResponse) => AxiosResponse | Promise<AxiosResponse>;
onRejected?: (error: any) => any;
};
As you can see, onFulfilled
requires you to return an AxiosResponse
(or its promised variant).
This means you cannot control what you want to get freely. Having said that, interceptors are quite useful in some other cases.
Difference with AxiosTransformer
export interface AxiosTransformer {
(data: any, headers?: any): any;
}
// Declaration from 'axios/index.d.ts'
As you may already noticed, AxiosTransformer
does not receive the whole response as the parameter,
instead it receives only the data and the headers of the response. Therefore, its capability is limited comapred with @Manipulate()
.
Returning nothing()
The
nothing(..._args: any[])
function returnsnull as any
. It accepts anything as arguments.
All methods decorated with any request decorator are recommended to return nothing()
.
Since these methods do not have a body and have nothing to return, linters such as eslint
would complain
about returning void
cannot assign to the method's return type Promise<AxiosResponse<T>>
.
Therefore, returning nothing()
can eliminate these warnings.
Moreover, due to the same reason, the parameters passed to the decorated method are unused.
Therefore, you can feed these unused arguments into nothing(...args)
in order to eliminate no-unused-vars
warnings.
Builder Configurations
Learn more about the config (
AxiosRequestConfig
) and the interceptors here.
The Retroxios
object acts as a builder to construct service instances based on a given config.
The service instances it creates will then inherit its config.
const baseConfig: AxiosRequestConfig = { ... };
const baseBuilder = new Retroxios(baseConfig);
const baseService = baseBuilder.create(ExampleBaseService);
Note that some options in the config (such as url
, params
, headers
and data
) supplied to
the Retroxios
object may get overridden by the same options specified in the request decorators.
Additionally, you can supply interceptors like this:
baseBuilder.interceptors.request.use(
(config) => {
console.log(config);
return config;
},
(error) => console.log(error)
);
baseBuilder.interceptors.response.use(
(response) => {
console.log(response);
return response;
},
(error) => console.log(error)
);
The Retroxios
object can be extended with another config to create service instances that derive from a base config.
const subConfig: AxiosRequestConfig = {};
const subBuilder = baseBuilder.extend(subConfig);
const subService = subBuilder.create(ExampleSubService);
Contributing
If you have any ideas on how to improve this project or if you think there is a lack of features, feel free to open an issue, or even better, open a pull request. All contributions are welcome!