Home > Net >  Standardized fetch function to communicate with API
Standardized fetch function to communicate with API

Time:04-02

I wrote my server backend in PHP. Now I want to create the communication between frontend (typescript) and backend.

I would like to receive a standardized response for each of my API requests. Accordingly, each response from my server is structured like this:

type Response<T> = {
    status: boolean,
    statusCode: number | null,
    statusText: string | null,
    statusDetails: string | null,
    data: T | null
};

Important: Since not all API requests return data (e.g. login and logout), the key data can also be null. If data is available, it should be of generic type T. This type depends on the API request. Example:

type ApiInitData = {
    user: {
        online: boolean | null,
        name: string | null,
        userGroupName: string | null,
        language: string | null,
        decimals: number | null,
    },
    language: {
        supportedLanguages: string[];
        defaultLanguage: string
    }
}

So if the API request was successful, I want to get an object of type Response<ApiInitData>. In this case the key data in Response<T> is not null of course.

My goal is to use a Typescript function that returns a Promise. In the .then of the Promise I want to control the further processing (e.g. display error message). Example:

Api.login(username, password).then((response) => {
    if(response.status === true){
        // Ok
    }else{
        // Some error has occurred.
        console.log(response.statusCode);
        console.log(response.statusText);
    }
});

When an error occurs, the Promise should be forced into the Response<null> type so that a standardized object is always returned in the end. This code snippet is my attempt:

//
// Example for login
//
static login = (username: string, password: string) => {
        const data = {
            'api': 'login',
            'username': username,
            'password': password
        };
        return this.request<Response<null>>(data);
    }

//
// Example for init data
//
static getInitData = () => {
        const data = {
            'api': 'getInitializationData'
        };
        return this.request<Response<ApiInitData | null>>(data);
    }

//
// Standard request function for all API requests.
//
static request = <T>(data: object, options: RequestInit = {}, url: string | null = null) => {
        const requestOptions = this.prepareRequest(data, options, url);
        return fetch(requestOptions.url, requestOptions.options)
            .then((response) => {
                if (response.ok) {
                    return response.json() as Promise<T>;
                } else {
                    const result: Response<null> = {
                        status: false,
                        statusCode: response.status,
                        statusText: response.statusText,
                        statusDetails: null,
                        data: null,
                    }
                    return new Promise<Response<null>>((resolve) => { resolve(result) });
                }
            }).catch((error) => {
                const result: Response<null> = {
                    status: false,
                    statusCode: 503,
                    statusText: error,
                    statusDetails: null,
                    data: null,
                }
                return new Promise<Response<null>>((resolve) => { resolve(result) });
            });
    }

The typescript compiler throws this error message: The argument of type "(response: Response) => Promise | Promise<Response>" cannot be assigned to the parameter of type "(value: Response) => T | PromiseLike". (line 4 of my request function).

I've no idea how to fix it. So I've two questions:

  • How can I fix the error ?
  • Is my approach used a common solution to this problem? Or is a completely different approach currently recommended here?

CodePudding user response:

Concerning the first question: The function given to .then() returns Promise<T> in case of success but Promise<Response<null>> in case of error. To just fix the error you could explicity state the desired types like

.then(function(response): Promise<T | Response<null>> { ...

But it looks like you want to return Promise<Response<T>> in both cases (in order to evaluate response.status === true as mentioned). The result of response.json() would then need to be wrapped by a Response<T> in the same way as the error.

Concerning the second question: The way you approach this here is a pattern known as Either Monad.

edit: Cleared up the answer about the possible options.

CodePudding user response:

After much trial and error, I have now found a working solution for me. I now return Promise.resolve and force its type to T.

return Promise.resolve(result as unknown as T);

static request = <T>(data: object, options: RequestInit = {}, url: string | null = null) => {
        const requestOptions = this.prepareRequest(data, options, url);
        return fetch(requestOptions.url, requestOptions.options)
            .then((response) => {
                if (response.ok) {
                    return response.json() as Promise<T>;
                } else {
                    const result: Response<null> = {
                        status: false,
                        statusCode: response.status,
                        statusText: response.statusText,
                        statusDetails: null,
                        data: null,
                    }
                    return Promise.resolve(result as unknown as T);
                }
            }).catch((error) => {
                const result: Response<null> = {
                    status: false,
                    statusCode: 503,
                    statusText: error,
                    statusDetails: error,
                    data: null,
                }
                console.log(error);
                return Promise.resolve(result as unknown as T);
            });
    }
  • Related