1
\$\begingroup\$

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.

\$\endgroup\$
1
\$\begingroup\$

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.

| improve this answer | |
\$\endgroup\$
  • \$\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 the HttpRequest<> into an object makes sense for the use of the scoped this. \$\endgroup\$ – VRoxa May 25 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 does request function. So instantiating objects seems look to me. Anyways, good approach, I really like the idea. Thanks \$\endgroup\$ – VRoxa May 25 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 May 26 at 18:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.