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.