[Fullstack Note] Javascript request in Browser and NodeJS environment

Compare with fetch and axios in source code level.

Posted by Jamie on Sunday, June 9, 2024

Preface

When it comes to align fetch in both frontend and backend environment, axios is the most frequent library which be mentioned. However, what is the difference between Client/Server side fetch?

Axios

We could use axios to make a request in both Browser side or server side directly. This is because axios itself handles and wraps the different environments of the browser and Node, and then exposes them through a single interface. In the browser, axios uses XMLHttpRequest(XHR) to handle requests, while in the Node environment, it uses the library http/https. Below is a simplified demo based on the axios source code.

  • Handling in the browser — function xhrAdapter would return a Promise based on XMLHttpRequest.

    export async function xhrAdapter(config) {
    return new Promise((resolve, reject) => {
        const request = new XMLHttpRequest();
        request.open(config.method.toUpperCase(), config.url, true);
    
        Object.keys(config.headers).forEach((key) => {
            request.setRequestHeader(key, config.headers[key]);
        });
    
        request.onload = function () {
            const response = {
                data: JSON.parse(request.responseText),
                status: request.status,
                statusText: request.getAllResponseHeaders(),
                config: config,
                request: request,
            };
            resolve(response);
        };
        request.onerror = function () {
            reject(new Error("Network Error"));
        };
    
        request.ontimeout = function () {
            reject(new Error(`Timeout of ${config.timeout} ms exceeded`));
        };
    
        request.send(config.data);
    });
    }
    
  • In the Node environment, is httpAdpater function, which returns a Promise based on http or https.

    import http from "http";
    import https from "https";
    
    export async function httpAdpater(config) {
    return new Promise((resolve, reject) => {
        const url = new URL(config.url);
        const isHttps = url.protocol === "https";
        const options = {
            method: config.method,
            headers: config.headers,
        };
    
        const request = (isHttps ? https : http).request(
            url,
            options,
            (response) => {
                let data = "";
    
                response.on("data", (chunk) => {
                    data += chunk;
                });
                response.on("end", () => {
                    resolve({
                        data: JSON.parse(data),
                        status: response.statusCode,
                        statusText: response.statusMessage,
                        headers: response.headers,
                        config: config,
                        request: response,
                    });
                });
            },
        );
    
        request.on("error", (error) => {
            reject(error);
        });
        if (config.data) {
            request.write(config.data);
        }
    
        request.end();
    });
    }
    
    
  • Finally, will check whether the environment is in the browser or in the Node , in order to return the right instance—if browser, return xhrAdapter; if Node environment, return httpAdpater.

    async function getDefaultAdapter() {
    if (typeof XMLHttpRequest !== "undefined") {
        // for browser, using XHR adapter
        const { xhrAdapter } = await import("./xhrAdapter.js");
    
        return xhrAdapter;
    } else if (typeof process !== "undefined") {
        // for Node env, using HTTP Adpater
        const { httpAdpater } = await import("./httpAdpater.js");
    
        return httpAdpater;
    }
    
    throw new Error("No suitable adapter found");
    }
    
  • The completed mocking axios source code could refer through the link

fetch

Since 2015, there has been a new Fetch API in Browser environment, and it has become the standard in web async request. However, there is still no comparable Fetch API in Node environment, so many SDKs choose node-fetch library to make up for this.

Basically, it is also related with checking global environment—if in Browser environment, could use native fetch directly; if in Node environment, could import node-fetch dynamically. (no dynamically would cause compile error)

export async function initFetch() {
    let fetch;

    if (typeof window !== "undefined" && typeof window.fetch === "function") {
        fetch = window.fetch;
    } else {
        const nodeFetch = await import("node-fetch");
        fetch = nodeFetch.default;
    }

    if (typeof global !== "undefined") {
        global.fetch = fetch;
    } else if (typeof window !== "undefined") {
        window.fetch = fetch;
    }
}

However, the latest version of node-fetch only supports ESM, so it would cause compiling error in the commonjs project. In this situation, could replace node-fetch with http/https

import http from "http";
import https from "https";
import { URL } from "url";

function fetch(url, options = {}) {
    return new Promise((resolve, reject) => {
        const urlObj = new URL(url);
        const isHttps = urlObj.protocol === "https:";
        const { method = "GET", headers = {}, body } = options;

        const requestOptions = {
            method,
            headers,
        };

        const request = (isHttps ? https : http).request(
            urlObj,
            requestOptions,
            (response) => {
                let data = "";

                response.on("data", (chunk) => {
                    data += chunk;
                });

                response.on("end", () => {
                    const responseData = {
                        ok:
                            response.statusCode >= 200 &&
                            response.statusCode < 300,
                        status: response.statusCode,
                        statusText: response.statusMessage,
                        headers: response.headers,
                        url: response.url || url,
                        json: () => Promise.resolve(JSON.parse(data)),
                        text: () => Promise.resolve(data),
                        blob: () => Promise.resolve(Buffer.from(data)),
                    };
                    resolve(responseData);
                });
            },
        );

        request.on("error", (error) => {
            reject(error);
        });

        if (body) {
            request.write(body);
        }

        request.end();
    });
}

export default fetch;

Finally, make the global fetch point to the fetch function.

  • could check the completed mock fetch source code through link

Conclusion

Furthermore, many third party libraries and SDKs avoid using Axios for network requests, primarily to reduce additional dependencies and minimize the bundle size. Additionally, since the native Fetch API in Node.js is only available starting from version 21, manual wrapping of the http/https modules or directly using node-fetch is still necessary for backward compatibility.

The completed source code is on github js-fetch_vs_axios

Ref

ChangeLog

  • 20240609-init
  • 20260501–translate by claude code