Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save lordsarcastic/3ae64d7b9be744a708d36ced81464943 to your computer and use it in GitHub Desktop.
Save lordsarcastic/3ae64d7b9be744a708d36ced81464943 to your computer and use it in GitHub Desktop.
How I implement external service calls for frontend (apart from Angular)
import { constants } from "@/utils/constants";
import { useEffect, useState } from "react";
import pricingService from "@/services/external/pricing.service";
import PricingSection, { PricingProps } from "@/components/Pricing";
const Home = () => {
const [pricingDetails, setPricingDetails] = useState<PricingProps>({
pricing:
constants.normal.home.pricing.priceSection.cta.pricing.toLocaleString(),
currency: constants.normal.home.pricing.priceSection.cta.currency,
currencySymbol:
constants.normal.home.pricing.priceSection.cta.currencySymbol,
});
const ipData = pricingService.useGetCountryByIP();
const ratesData = pricingService.useGetCurrencyRate();
useEffect(() => {
if (!(ipData.isSuccess && ratesData.isSuccess)) return;
const countryCode = ipData.data.data.country;
const rates = ratesData.data.data.rates;
const newPricingDetails = pricingService.convertPricingToLocalRate(
constants.normal.home.pricing.priceSection.cta.pricing,
countryCode,
rates,
);
setPricingDetails(newPricingDetails);
}, [ipData.data, ratesData.data]);
return <PricingSection {...pricingDetails} />

This outlines how I usually implement external service calls on the frontend. We use classes and leverage advantages to craft a compact and easily portable way to make API calls. Note that this could absolutely be done with functions, but I created this gist out of demand.

GitHub gist is just annoying and doesn't line up the files as you create them. Instead it orders it alphabetically. When I have time, I'll fix it. For now, manage it as it is.

Note: Treat hyphen (-) in file names as a directory slash (/).

This outlines how it is used in NextJS. There really is nothing special as it is the same way you'd normal functions.
import { useMutation } from "@tanstack/react-query";
import { endpoints } from "@/utils/endpoints";
import {
ChangePasswordErrorType,
ChangePasswordRequestType,
ChangePasswordResponseType,
LoginSchemaType,
RegisterRequestType,
ResetPasswordCodeErrorType,
ResetPasswordCodeRequestType,
ResetPasswordCodeResponseType,
ResetPasswordEmailRequestType,
ResetPasswordNewPasswordErrorType,
ResetPasswordNewPasswordRequestType,
} from "./auth.types";
import BaseService from "../base.service";
class AuthService extends BaseService {
public useLogin = () =>
useMutation({
mutationFn: async (data: LoginSchemaType) => {
const response = await this.client.post(endpoints.auth.login, {
...data,
});
return response;
},
});
public useRegister = () =>
useMutation({
mutationFn: async (data: RegisterRequestType) => {
const response = await this.client.post(endpoints.auth.register, {
...data,
});
return response;
},
});
public useResetPasswordEmail = () =>
useMutation({
mutationFn: async (data: ResetPasswordEmailRequestType) => {
const response = await this.client.post(
endpoints.auth.password.reset.email,
data,
);
return response;
},
});
public useResetPasswordCode = () =>
this.useMutationRequest<
ResetPasswordCodeRequestType,
ResetPasswordCodeResponseType,
ResetPasswordCodeErrorType
>(endpoints.auth.password.reset.code);
public useResetPasswordNewPassword = () =>
this.useMutationRequest<
ResetPasswordNewPasswordRequestType,
any,
ResetPasswordNewPasswordErrorType
>(endpoints.auth.password.reset.password);
public useChangePassword = () =>
this.useMutationRequest<
ChangePasswordRequestType,
ChangePasswordResponseType,
ChangePasswordErrorType
>(endpoints.auth.password.change);
}
const authService = new AuthService();
export default authService;
import { RequestErrorType } from "../base.service";
export type LoginSchemaType = {
email: string;
password: string;
};
export type RegisterRequestType = {
email: string;
password: string;
};
export type ResetPasswordEmailRequestType = {
email: string;
};
export type ResetPasswordCodeRequestType = {
email: string;
code: number;
};
export type ResetPasswordCodeResponseType = ResetPasswordCodeRequestType & {};
export type ResetPasswordCodeErrorType = RequestErrorType & {
code?: string[];
};
export type ResetPasswordNewPasswordRequestType = {
email: string;
code: number;
password: string;
};
export type ResetPasswordNewPasswordErrorType = RequestErrorType & {
email?: string[];
code?: string[];
password?: string[];
};
export type ChangePasswordRequestType = {
old_password: string;
new_password: string;
};
export type ChangePasswordResponseType = {};
export type ChangePasswordErrorType = RequestErrorType & {
old_password?: string[];
new_password?: string[];
};
import Axios, { AxiosInstance } from "axios";
export const axiosInstance: AxiosInstance = Axios.create({
baseURL: process.env.NEXT_PUBLIC_BASE_URL,
headers: { "Content-Type": "application/json" },
});
import { useMutation } from "@tanstack/react-query";
import { AxiosResponse } from "axios";
import { axiosInstance } from "./axios";
export type RequestErrorType = {
non_field_errors?: string[];
};
class BaseService {
/*
* A generic method to handle mutation
* @param endpoint - API endpoint to send the request
* @generic I - Request payload type
* @generic R - Response payload type
* @generic E - Error payload type
* @returns useMutation hook
* @example
* useMutationRequest<LoginSchemaType, LoginResponseType, LoginErrorType>(endpoints.auth.login);
* useMutationRequest<RegisterRequestType, RegisterResponseType, RegisterErrorType>(endpoints.auth.register);
*/
client = axiosInstance;
protected useMutationRequest = <I, R, E>(endpoint: string) =>
useMutation<AxiosResponse<R, E>, E, I>({
mutationFn: async (data) => {
const response = await this.client.post(endpoint, data);
return response;
},
});
}
export default BaseService;
import axios, { AxiosResponse } from "axios";
import { useQuery } from "@tanstack/react-query";
import { CountryByIP, CurrencyRate } from "./pricing.types";
import { Country } from "country-state-city";
import { PricingProps } from "@/components/Pricing";
class PricingService {
private countryAPIURL = "https://api.country.is";
private currencyAPIURL = "https://cdn.moneyconvert.net/api/latest.json";
private acceptedCountries: Record<string, Record<string, string>> = {
US: {
countryCode: "US",
currencyCode: "USD",
currencySymbol: "$",
},
NG: {
countryCode: "NG",
currencyCode: "NGN",
currencySymbol: "₦",
},
ZA: {
countryCode: "ZA",
currencyCode: "ZAR",
currencySymbol: "R",
},
KE: {
countryCode: "KE",
currencyCode: "KES",
currencySymbol: "KSh",
},
GH: {
countryCode: "GH",
currencyCode: "GHS",
currencySymbol: "GH₵",
},
};
public useGetCountryByIP = () =>
useQuery<AxiosResponse<CountryByIP, any>>({
queryKey: ["countryByIp"],
queryFn: async () => {
const response = await axios(this.countryAPIURL);
return response;
},
refetchOnWindowFocus: false,
});
public getCurrencyCodeByCountry = (countryCode: string): string => {
if (!this.acceptedCountries[countryCode]) return "USD";
return Country.getCountryByCode(countryCode)?.currency || "USD";
};
public useGetCurrencyRate = () =>
useQuery<AxiosResponse<CurrencyRate, any>>({
queryKey: ["currencyRate"],
queryFn: async () => {
const response = await axios(this.currencyAPIURL);
return response;
},
refetchOnWindowFocus: false,
});
// Ideally, we should avoid depending on props coming from a
// Next.js page component in a service file. This is because
// services are meant to be reusable across multiple components.
public convertPricingToLocalRate = (
pricing: number,
countryCode: string,
rates: Record<string, number>,
): PricingProps => {
const currencyCode = this.getCurrencyCodeByCountry(countryCode);
const pricingDetails: PricingProps = {
currency: currencyCode,
pricing: pricing.toLocaleString(),
currencySymbol:
this.acceptedCountries[countryCode]?.currencySymbol || "$",
};
const rate = rates[currencyCode];
if (rate)
pricingDetails.pricing = Math.round(pricing * rate).toLocaleString();
return pricingDetails;
};
}
const pricingService = new PricingService();
export default pricingService;
export type CountryByIP = {
country: string;
ip: string;
};
export type CurrencyRate = {
table: string;
rates: Record<string, number>;
lastupdate: string | Date;
};
While the next part isn't related to the `BaseService` class, it still suffices as
an example of how I'd use classes to bind API calls together
From this point on, I include a couple of usage examples on how I use this structure in my codebase
export const endpoints = {
auth: {
login: "/auth/login/",
register: "/auth/register/",
password: {
reset: {
email: "/auth/password/reset/initialize/",
code: "/auth/password/reset/code/",
password: "/auth/password/reset/password",
},
change: "/auth/password/change/",
},
},
project: {
create: "project/create/",
},
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment