0

Context: I am building a site that is essentially just a UI for eBird's API.

My first question is about naming. Wikipedia defines a client as "a piece of computer hardware or software that accesses a service made available by a server as part of the client–server model of computer networks." So by this definition, my site is a client of eBird's API. However, when talking about an internal utility of my codebase that takes parameters and uses them to make a request should that also be called a client? I ask because I am having difficulty searching for relevant examples using the word "client".

For clarity, here is the "client" I have:

export interface EbirdApiParams {
  apiKey?: string;
  queryParams?: QueryParam[];
  urlParams?: UrlParam[];
}

export interface QueryParam {
  defaultValue?: NonNullable<QueryParamValue>;
  name: string;
  value: QueryParamValue;
}

export type QueryParamValue = boolean | number | string | string[] | undefined;

export interface UrlParam {
  name: string;
  value: string | number;
}

function buildQueryString(queryParams: QueryParam[]) {
  if (queryParams.length === 0) {
    throw Error('No query params provided.');
  }

  const queryString = queryParams
    .filter(
      (
        param
      ): param is QueryParam & { value: NonNullable<QueryParamValue> } => {
        const { defaultValue, value } = param;

        if (value === undefined || value === defaultValue) {
          return false;
        }

        if (typeof value === 'string' || Array.isArray(value)) {
          return value.length !== 0;
        }

        return true;
      }
    )
    .map(({ name, value }) => {
      return `${name}=${value.toString()}`;
    })
    .join('&');

  return `?${queryString}`;
}

export async function makeRequest(
  endpoint: string,
  options: EbirdApiParams = {}
) {
  const { apiKey, queryParams, urlParams } = options;

  let requestUrl = `https://api.ebird.org/v2/${endpoint}`;

  if (urlParams !== undefined) {
    requestUrl = mergeUrlParams(requestUrl, urlParams);
  }

  if (queryParams !== undefined) {
    requestUrl += buildQueryString(queryParams);
  }

  return await fetch(requestUrl, {
    headers: {
      'x-ebirdapitoken': apiKey ?? '',
    },
  });
}

function mergeUrlParams(endpoint: string, urlParams: UrlParam[]) {
  if (urlParams.length === 0) {
    throw Error('No URL params provided.');
  }

  let mergedUrl = endpoint;

  urlParams.forEach(({ name, value }) => {
    const mergeTag = `{{${name}}}`;

    if (!mergedUrl.includes(mergeTag)) {
      throw Error(`Merge tag "${mergeTag}" not found.`);
    }

    mergedUrl = mergedUrl.replace(`{{${name}}}`, value.toString());
  });

  if (/{{.*}}/.test(mergedUrl)) {
    throw Error(`Unresolved URL params within ${mergedUrl}`);
  }

  return mergedUrl;
}

The "client" basically just slightly curries the request by always including the API key and the base of the request URL as well as transforming the URL params and query params into a single URL.

Given the above, my second question is where should this reside in the codebase? Currently the "client" is in /src/utilites/ebirdApiClient.ts and I also have /src/hooks/useEbirdApi.ts which is a React hook that takes the basic makeRequest method and uses it to mirror all the endpoints that exist on the API. I won't paste that file here because it's large, but you can view it here if you'd like.

Third, I would also like to split apart /src/hooks/useEbirdApi.ts so that each endpoint has it's own file since the hook itself is large and hard to maintain in its current state. Where should those files live?

Obviously this is somewhat subjective, so I'm not looking for a single concise answer that is "correct" but I have had a hard time researching this so any guidance would be appreciated.

1
  • Interesting question. I think you are taking the term client at once perhaps to literally and insufficiently broadly. A client is a construct which relies on an abstraction which provides a well defined interface to perform an operation. An interface here just means and abstraction such as a function, module, object, class, etc. Given how significant your client layer is to the overall app that you're building I would suggest you export it close to the root of your package. Jul 13 at 14:16

1 Answer 1

3

However, when talking about an internal utility of my codebase that takes parameters and uses them to make a request should that also be called a client?

Yes.

Regarding the code arrangement, apply the "high cohesion and low coupling" principle. Keep together what work and change together. Everything related to EbirdApi belongs to the same domain. All these elements work together and share reasons to change together too.

Consider moving allEbirdApi* elements (classes, interfaces, types, client, hooks, etc) to a single space, for example, src/services/ebirds. Then, organize elements by sub-folders. Each sub-folder is a sub-domain, so high cohesion and low coupling apply. For example: src/services/ebirds/model, src/services/ebirds/hooks, src/services/ebirds/client

You could even go further and create a module.

Links of interest

3
  • 1
    Thanks for these great resources. I did a pretty major refactor around this advice and I think things are much cleaner now. Compare before and after. Thanks for the help! Jun 22 at 22:12
  • You have made a big change inside the component folder. It looks much better now. I would rename the folder type to model. I think we developers don't think in terms of types when we refer to domain models. Also In terms of TS I think types are different than interfaces. I would do a little research about this subject. Regarding hook folder, I find it a bit misleading. I have had to look inside and am still unsure about the content. Finally, while pages is fine, I tend to think in terms of views, which is used in technical documentation too.
    – Laiv
    Jun 22 at 22:32
  • Don't take my words too seriously too. These things are easy to refine as you work on the code. As a pet project, if the actual arrangement helps you to locate things, then is good.
    – Laiv
    Jun 22 at 22:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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