I am currently working in a project based on a microservices architecture pattern.
Services are wired up by HTTP calls. They eventually call each others for fetching or putting some data.
I am working on the web page which envelops the whole microservices. I mean, the web page actually needs to send and retrieve data from all the services (except one).
There are several times where the web page behaves as a broker, i.e.:
Fetch some data from service 1,
fetch some data from service 2.
Send something to service 3 based on previous responses.
That's why I decided to implement some functions to avoid lots of duplicated code chunks. This is what I call the promise-based HTTP abstraction layer.
I am developing the web page in Typescript, by the way.
I want to mention what my request
function is. The request
function performs HTTP requests, unsurprisingly.
It is an axios call wrapper. However, for the sake of simplicity, since it is not that important I'll show only the header.
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
declare function request<TResult>(method: HttpMethod, url: string, body?: any): Promise<TResult>;
And, for comfort, I declared its prefixed argument different calls
const get = request.bind(this, 'GET');
const post = request.bind(this, 'POST');
// And so on ...
But I refactored this to take profit of the request
type argument
const get = <TResult>(url: string, body: any) => request<TResult>('GET', url, body);
const post = <TResult>(url: string, body: any) => request<TResult>('POST', url, body);
// And so on ...
And, a waitAll
function which requests to call the URLs passed in and returns a single Promise
which will be resolved whenever all the requests are done.
interface HttpRequest<TResult> {
url: string;
request: (url: string, body?: any) => Promise<TResult>;
body?: any;
};
const waitAll = (...reqs: HttpRequest<any>[]): Promise<any[]> =>
Promise.all(reqs.map(req => req.request(req.url, req.body)));
With this, I can do something like
(async () => {
const req1: HttpRequest<FirstResponse> = {
url: `http://example.com/one`,
request: post,
body: {
id: 9,
data: [`hi`, `there`]
}
};
const req2: HttpRequest<SecondResponse> = {
url: `http://example.com/one`,
request: get
};
const results = await waitAll(req1, req2);
const [r1, r2] = results;
const body = { ...r1, ...r2 };
const finalResponse = await post<ThirdResponse>(`http://example.com/three`, body);
// do something w/ final response
})();
This is working but I notice some wrong things.
It feels weird the HttpResponse<>
object, that wraps the url
and the body
that, in fact, will be called inside its own request
property function.
And when I call waitAll
, I'd expect my r1
and r2
to be of the matching type (I mean, r1
must be
of type FirstResponse
, and r2
must be of type SecondResponse
) just like finalResponse
is of type ThirdResponse
.
I am pretty sure there are lots of improvements to achieve a better-looking API and, the most important thing, take profit of Typescript type safety.
Any thoughts are welcome.
1 Answer 1
First thing, you're right about HttpRequest
and the oddness of having to pass manually body
and url
.
To improve the code, I would suggest to chose a programming paradigm, and to stick to it:
example in OOP:
class HttpRequest<TResult> {
constructor(private method: HttpMethod, private url: string, private body?: any) {}
request():Promise<TResult> {
return request(this.method, this.url, this.body);
}
};
const req1 = new HttpRequest<FirstResponse>('POST', `http://example.com/one`, {
id: 9,
data: [`hi`, `there`]
});
const req2 = new HttpRequest<SecondResponse>('GET', `http://example.com/one`);
// you now just have to call req1.request() for example , without parameters
in more FP friendly way of doing thing:
function my_request<T>(httpReq: HttpRequest) {
return (): Promise<T> => request(httpReq.method, httpReq.ufl, httpReq.body)
}
const req1 = my_request<FirstResponse>({method:'POST', url:`http://example.com/one`, body:{
id: 9,
data: [`hi`, `there`]
});
const req2 = my_request<SecondResponse>({method:'GET', url:`http://example.com/one`});
// you now just have to call req1() for example , without parameters
Now, we can improve the typing of waitAll
with mapped types, for example for the OOP version (it's equivalent for the FP one):
function waitAll<T extends any[]>(...reqs: {[K in keyof T]: HttpRequest<T[K]>}): Promise<T> {
// the any here is mandatory as Promise.all doesn't support (yet) the above signature
return Promise.all(reqs.map(req => req.request())) as any;
}
// result type will be a [FirstResponse, SecondResponse]
const result = waitAll(req1, req2)
Now, you should ask yourself if you're not "over-abstracting". A code like that:
const req1 = request('POST', `http://example.com/one`, {id: 9});
const req2 = request('GET', `http://example.com/one`);
const [r1, r2] = await Promise.all(req1, req2);
const finalResponse = await request('POST', `http://example.com/three`, body);
uses only standard TS stuffs (await
, Promise.all
), hence is often easier to read for a developper that didn't write the code.
-
\$\begingroup\$ Hi, @Molochdaa. The idea of the mapped types for keeping the typed-member array is fantastic; never thought about it. However, I must point out the
as any
in its implementation seems quite dirty.as Promise<T>
is allowed and it feels cleaner. On the other hand, wrapping theHttpRequest<>
into an object makes sense for the use of the scopedthis
. \$\endgroup\$VRoxa– VRoxa2020年05月25日 13:57:16 +00:00Commented May 25, 2020 at 13:57 -
\$\begingroup\$ Finally, I don't really agree with the last statement you said. Note that
Promise.all
is no longer keeping the type response anymore. Moreover, I want (even need) to declare request without invoking 'em instantly, which doesrequest
function. So instantiating objects seems look to me. Anyways, good approach, I really like the idea. Thanks \$\endgroup\$VRoxa– VRoxa2020年05月25日 14:00:13 +00:00Commented May 25, 2020 at 14:00 -
\$\begingroup\$ If you want lazy Promise, can you also try something like (would be the same with a class or a function):
type LazyPromise<A> = {request: () => Promise<A>};
function waitAll<T extends any[]>(t: {[K in keyof T]: LazyPromise<T[K]>}): LazyPromise<T> { return {request: () => Promise.all(t.map(p => p.request()))} as LazyPromise<T>; }
Promise.all
type should keep the type of the request... up to a certain number (maybe 7 or 8 promises? ) . The different variants for pair, triplet, 4-tuple, etc. are hardcoded in TypeScript lib.d.ts \$\endgroup\$Molochdaa– Molochdaa2020年05月26日 18:38:50 +00:00Commented May 26, 2020 at 18:38