This blog highlights the benefits of using a service layer in frontend applications. The service layer acts as a middleman between the frontend and APIs, improving code structure, scalability, and adaptability to backend changes. It aligns with SOLID principles and promotes SoC, OCP, and DIP. The blog also gives tips on implementing a service layer with a focus on flexibility and maintainability.
Incorporating a service layer into frontend applications is a strategic architectural decision that addresses several key challenges and enhances overall application quality. This blog explores the reasons behind adopting a service layer, its advantages, and how it mitigates common development hurdles. Additionally, we'll discuss strategies for implementing this layer on top of an abstract HTTP layer, emphasizing flexibility and extensibility without sepending on specific packages like Axios.
A service layer functions as an intermediary between the frontend application and external services, such as APIs. Beyond merely fetching data, it encompasses data transformation, logical adjustments, and potentially enforcing business rules before data reaches the application's core logic or UI components.
Adhering to the principle of Separation of Concerns (SoC), a service layer organizes code into distinct sections, each focusing on a specific aspect. This separation:
The service layer naturally aligns with several SOLID principles, notably the Open/Closed Principle (OCP) and Dependency Inversion Principle (DIP):
In a dynamic development environment, backend APIs frequently evolve. A service layer insulates the frontend from these changes, serving as a buffer that adapts to modifications in API structure or response formats. This decoupling ensures that frontend applications remain stable and up-to-date with minimal effort.
The service layer provides the flexibility to perform any desired transformation on the backend response. This capability is crucial for adapting the data to fit the frontend's requirements precisely, regardless of the original format or structure provided by the backend. Whether it involves data format conversion, data enrichment, or logical adjustments, the service layer acts as a versatile tool for tailoring backend data to enhance the frontend's usability and performance.
To effectively integrate a service layer, consider the following steps:
An abstract HTTP layer provides a unified interface for making HTTP requests, enabling the service layer to interact with APIs without being tied to specific HTTP clients like Axios or Fetch API. A sample of the HTTP layer can be like this:
import axios, { type AxiosInstance, type AxiosRequestConfig } from "axios";
import { backendBaseURL } from "@configs/url";
import type { Dictionary, Maybe } from "ts-wiz";
export type FetchOptions<
Params extends Dictionary = Dictionary,
Body extends Maybe<Dictionary> = undefined
> = AxiosRequestConfig<Body> & { params?: Params };
class HttpClient {
axiosInstance: AxiosInstance;
constructor() {
this.axiosInstance = axios.create({
timeoutErrorMessage:
"The client did not produce a request within the time",
});
}
get<
Data extends Dictionary = Dictionary,
Params extends Dictionary = Dictionary
>(options: FetchOptions<Params>) {
const config = typeof options === "string" ? { url: options } : options;
return this.request<Data, Params>({ method: "GET", ...config });
}
post<
Data extends Dictionary = Dictionary,
Params extends Dictionary = Dictionary,
Body extends Dictionary = Dictionary
>(options: Omit<FetchOptions<Params, Body>, "method"> = {}) {
return this.request<Data, Params, Body>({ method: "POST", ...options });
}
patch<
Data extends Dictionary = Dictionary,
Params extends Dictionary = Dictionary,
Body extends Dictionary = Dictionary
>(options: Omit<FetchOptions<Params, Body>, "method"> = {}) {
return this.request<Data, Params, Body>({ method: "PATCH", ...options });
}
put<
Data extends Dictionary = Dictionary,
Params extends Dictionary = Dictionary,
Body extends Dictionary = Dictionary
>(options: Omit<FetchOptions<Params, Body>, "method"> = {}) {
return this.request<Data, Params, Body>({ method: "PUT", ...options });
}
delete<
Data extends Dictionary = Dictionary,
Params extends Dictionary = Dictionary
>(options: Omit<FetchOptions<Params>, "method"> = {}) {
return this.request<Data, Params>({ method: "DELETE", ...options });
}
async request<
Data extends Dictionary = Dictionary,
Params extends Dictionary = Dictionary,
Body extends Maybe<Dictionary> = undefined
>(options: FetchOptions<Params, Body>) {
const { baseURL, ...restOptions } = options;
return this.axiosInstance
.request<Data>({ baseURL: baseURL ?? backendBaseURL, restOptions })
.then((res) => res.data);
}
}
const Http = new HttpClient();
export default Http;
Building the service layer on top of an abstract HTTP layer ensures that the application remains adaptable to technological shifts or improvements without substantial refactoring. This approach supports continuous development and enhancement of the frontend application, aligning with agile methodologies.
So the folder structure should look like this:
the auth/index.ts file contains an object which is for grouping all the services together:
import { authKnock, type AuthKnockResult } from "./knock";
import { authVerifyOtp, type AuthVerifyOtpResult } from "./verify";
import { authRefreshToken, type AuthRefreshTokenResult } from "./refreshToken";
const AuthServices = {
knock: authKnock,
verifyOtp: authVerifyOtp,
refreshToken: authRefreshToken,
};
export type AuthServicesResults = {
Knock: AuthKnockResult;
VerifyOtp: VerifyOtpResult;
RefreshToken: RefreshTokenResult;
};
export default AuthServices;
and for example auth/verifyOtp.ts can look like this:
import Http from "@utils/Http";
type Response = { token: string };
export type AuthKnockResult = { value: string };
const ENDPOINT = "/auth/refresh-token";
export default async function authKnock(phone: string): AuthKnockResult {
try {
const response = await Http.post<Response>({
url: ENDPOINT,
data: { phone },
});
const result: AuthKnockResult = { value: response.result.token };
return result;
} catch (error) {
throw error;
}
}
Ofcourse, you can change the structure of folders and files in order to make them look like the way you prefer them to do.
Incorporating a service layer into frontend applications offers significant advantages, including improved code organization, adherence to SOLID principles, and resilience against backend changes. By thoughtfully designing and implementing this layer atop an abstract HTTP layer, developers can create applications that are more maintainable, scalable, and capable of adapting to future developments.