Anti Corruption Layer (ACL) for your API communication
What's an Anti Corruption Layer?
Short said, it makes sure that the data you use in your frontend will always be as you expected it when you did the implementation. So even if the response from the API alters, your frontend will not run into fatal errors.
For this will introduce a layer (the ACL) that will transform the data received from the API into previously defined models only used on the frontend. We basically remove the dependency the frontend has to the backend.
💡 Tip 💡
A good Anti Corruption Layer makes your application work even if one or more API requests fail, by setting default values for all properties the frontend needs to work.
Why should you use an ACL in your application
Already small changes in the data you receive from the API-request may break your application. Imagine a property name of a json response changes like this:
{
...
"isValid": true,
"isValidForUsage": true,
...
}This will affect all the usages for isValid you have in your application and will end up in errors or not correctly rendered components if you try to access isValid while it's not present anymore.
Possible error if you don't check for this property:
Uncaught TypeError: Cannot read properties of undefined (reading 'isValid') at...How to implement an ACL into your Application
To avoid any console errors, or even non-rendered components that will break your application layout and usability, we will now introduce an ACL step-by-step.
Initial State without ACL
Let's start with the following setup of code:
export const fetchDataFromApi = async (): AxiosPromise<BackEndApiResponse[]> => {
return await axios.get('/data');
};export const fetchData = async (): Promise<BackEndApiResponse[]> => {
try {
// fetch data, by calling the API layer
const {data: responseData} = await fetchDataFromApi();
// return the plain backend dto to the application
return responseData.data;
} catch (error) {
handleError(error)
return [];
}
};export const useDataStore = defineStore('dataStore', () => {
const yourData Ref<BackEndApiResponse[]> = ref([]);
const getData = async () => {
// here we call the Service layer, that will then call the API layer
fetchData().then((data: BackEndApiResponse[]) => {
yourData.value = data;
});
};
})<template>
<CustomComponent :isValid="yourData.isValid"/>
</template>
<script setup lang="ts">
const dataStore = useDataStore();
const { yourData } = storeToRefs(dataStore);
</script>export interface BackEndApiResponse {
createdAt: number;
createdBy: string;
isValid: boolean;
}Plan, what properties you need on the Frontend
As you can see from the snippet of the Vue Component, we currently only access one of the three properties (isValid) of the BackEndApiResponse interface that is returned by the backend. Still we re-use the same interface for the frontend implementation, making the frontend rely on the backend and carrying two unnecessary properties with us.
🔎 Info
You don't need to think too much about what you need now and what you might need in the future. It's enough to just focus on what you need for the current implementation.
Let's change that!
Create Frontend Interfaces
First step of detaching the frontend from the backend is by introducing independent models, types and interfaces, only used by the frontend.
💡 Tip 💡
This also applies if the interface is (for now) exactly the same as the interface used for the frontend. We set this up for a clean project and for easy maintainability in the future.
For small projects that might sound overwhelming and overengineered, but you will thank yourself in the future, when you have to change / improve something.
export interface BackEndApiResponse {
createdAt: number;
createdBy: string;
isValid: boolean;
}
export interface FrontEndInterface {
isValid: boolean;
} Create Mapper Layer
Now that we have our frontend only interface, we need a new layer -> The Mapper Layer.
This layer is responsible to map our data in two ways:
- Backend Interfaces -> Frontend Interfaces (when fetching data)
- Frontend Interfaces -> backend Interfaces (when posting data)
// mapper file
// Backend -> Frontend
const mapBackendInterfaceToFrontendInterface = (dataFromBackend: BackEndApiResponse[]): FrontEndInterface[] => {
const mappedData: FrontEndInterface[] = [];
dataFromBackend.map((dataEntry) => {
const mappedTechStackEntry: BackEndApiResponse = {
isValid: dataEntry.isValid,
};
mappedData.push(mappedTechStackEntry);
});
return mappedData;
}
// Frontend -> Backend
const mapFrontendInterfaceToBackendInterface = (frontendData: FrontEndInterface, User: UserData): BackEndApiResponse => {
return {
isValid: frontendData.isValid,
createdAt: new Date().getTime(),
createdBy: User.Username
}
}Map data in Service Layer
Now that we have the mappers ready, we can use them in our Service Layer.
❗ Don't forget change the Store Implementation ❗
In the same step you have to update your store implementation and all other places that still use the backend interface, to only use the Frontend Interface.
⚠️ Warning ⚠️
If you made any changes to the properties when creating the frontend interfaces, make sure to also update your frontend components.
export const fetchData = async (): Promise<FrontEndInterface[]> => {
try {
// fetch data, by calling the API layer
const {data: responseData} = await fetchDataFromApi();
// return the plain backend dto to the application
return responseData.data;
// map the data we fetched into our frontend interface
return mapFrontendInterfaceToBackendInterface(responseData.data);
} catch (error) {
handleError(error)
return [];
}
};export const useDataStore = defineStore('dataStore', () => {
const yourData Ref<BackEndApiResponse[]> = ref([]);
const yourData Ref<FrontEndInterface[]> = ref([]);
const getData = async () => {
// here we call the Service layer, that will then call the API layer
fetchData().then((data: BackEndApiResponse[]) => {
fetchData().then((data: FrontEndInterface[]) => {
yourData.value = data;
});
};
});What happens if the dto from the Backend updates
Now we come to the real benefit of an ACL! Let's assume you have everything setup as mentioned above, and the api response (dto) changes a property (in our case isValid -> isInvalid). As you can see, this does not just change the property name, but also inverts the logic of the property, making all your components and methods that use it incorrect.
Now instead of fixing all the places we use this property in, you only have to fix one place, the Anti Corruption Layer!
💡 Tip 💡
In this example, we will retain the logic we have in our frontend by just inverting the boolean value we retrieve from the backend. With this, our frontend interface stays the same, which saves us a lot of time!
export interface BackEndApiResponse {
createdAt: number;
createdBy: string;
isValid: boolean;
isInvalid: boolean;
}
export interface FrontEndInterface {
isValid: boolean;
}// mapper file
// Backend -> Frontend
const mapBackendInterfaceToFrontendInterface = (dataFromBackend: BackEndApiResponse[]): FrontEndInterface[] => {
const mappedData: FrontEndInterface[] = [];
dataFromBackend.map((dataEntry) => {
const mappedTechStackEntry: BackEndApiResponse = {
isValid: dataEntry.isValid,
isValid: !dataEntry.isInvalid,
};
mappedData.push(mappedTechStackEntry);
});
return mappedData;
}
// Frontend -> Backend
const mapFrontendInterfaceToBackendInterface = (frontendData: FrontEndInterface, User: UserData): BackEndApiResponse => {
return {
isValid: frontendData.isValid,
isInvalid: !frontendData.isValid,
createdAt: new Date().getTime(),
createdBy: User.Username
}
}Summary
If you followed the steps above, you successfully created an Anti Corruption layer for your Vue Application. As you've seen, updating or maintaining your application that uses an Anti Corruption Layer for your API requests is very easy. So even thought in the first place it takes more time to set up all the layers on your application, you will save a lot of time if your application grows, and you need to maintain it.
As a last step, here is an overview of how your code could look like if you're done with the implementation of your first ACL, as well as a quick 'Rule of Thumb' for the layers we used.
export const fetchDataFromApi = async (): AxiosPromise<BackEndApiResponse[]> => {
return await axios.get('/data');
};export const fetchData = async (): Promise<FrontEndInterface[]> => {
try {
const {data: responseData} = await fetchDataFromApi();
return mapFrontendInterfaceToBackendInterface(responseData.data);
} catch (error) {
handleError(error)
return [];
}
};const mapBackendInterfaceToFrontendInterface = (dataFromBackend: BackEndApiResponse[]): FrontEndInterface[] => {
const mappedData: FrontEndInterface[] = [];
dataFromBackend.map((dataEntry) => {
const mappedTechStackEntry: BackEndApiResponse = {
isValid: dataEntry.isValid,
};
mappedData.push(mappedTechStackEntry);
});
return mappedData;
}
const mapFrontendInterfaceToBackendInterface = (frontendData: FrontEndInterface, User: UserData): BackEndApiResponse => {
return {
isValid: frontendData.isValid,
createdAt: new Date().getTime(),
createdBy: User.Username
}
}export const useDataStore = defineStore('dataStore', () => {
const yourData Ref<FrontEndInterface[]> = ref([]);
const getData = async () => {
fetchData().then((data: FrontEndInterface[]) => {
yourData.value = data;
});
};
});<template>
<CustomComponent :isValid="yourData.isValid"/>
</template>
<script setup lang="ts">
const dataStore = useDataStore();
const { yourData } = storeToRefs(dataStore);
</script>export interface BackEndApiResponse {
createdAt: number;
createdBy: string;
isValid: boolean;
}
export interface FrontEndInterface {
isValid: boolean;
}Dataflow
Backend
↑
|
↓
Api Layer
↑
|
↓
Service Layer <------> Mapper Layer
↑
|
↓
Frontend⚠️ Warning ⚠️
Data in the structure from the backend (using backend interfaces) should never get past the Service Layer.
API Layer
The API Layer is responsible for only one action: Making requests to the API! This means it should never have any logic inside.
Service Layer
The Service Layer should hold all the logic that is needed so the Frontend can communicate with the API-Layer. Therefor it does the following:
- Call the API Layer (to fetch or send data)
- Call the Mapper Layer to prepare data
Mapper Layer
The Mapper Layer is responsible to map the data in both ways - Backend -> Frontend & Frontend -> Backend.