Optimizing Vuex API Calls: Separation of Concerns, Olive‑Shaped Interfaces, and Response Normalization
This article walks through three progressive refactoring stages for Vuex‑based API calls—first separating concerns for maintainable code, then introducing a concise “olive‑shaped” interface with dynamic generation or proxy interception, and finally adding response normalization and three‑level error handling to produce robust, low‑maintenance frontend logic.
The author shares practical experience from an internal "generic front‑end" session, illustrating how to evolve Vuex usage while developing an IoT device‑linkage project. All code snippets are presented as pseudo‑code and contain no proprietary details.
Stage 1 – Separation of Concerns and Maintainable Code
Modules are split so that service1.js handles raw API requests and store1.js maps those services to Vuex actions, eliminating duplicated logic. However, the initial implementation repeats similar request handling across three service functions.
git clone [email protected]:qqvk/qvk.git cd qvk // enter project directory
npm install // install dependencies
npm run init // initialize
npm start // start project // lib/endpoints.js
export default {
getUserDeviceList: { method: 'GET', endpoint: '/getUserDeviceList' },
getUserSceneList: { method: 'GET', endpoint: '/getUserSeceneList' },
getUserScene: { method: 'GET', endpoint: '/getUserScene' }
};
export const ENV = 'https://the-service-address'; // lib/factory.js
export default ({ serviceName = '', serviceArguments = {} }) => {
const { method, endpoint } = API[serviceName];
const urlBase = `${ENV}${endpoint}`;
const { queryString, headers } = getQueryStringAndHeaders(serviceArguments);
let url, body, options;
if (method == 'POST') {
url = urlBase;
body = queryString;
options = { headers, method, body };
}
if (method == 'GET') {
url = `${urlBase}?${queryString}`;
options = { headers, method };
}
return { url, options };
};
// getQueryStringAndHeaders() and other helpers are omitted for brevity // lib/service1.js
export default {
async getUserSceneList() {
const { url, options } = factory({ serviceName: 'getUserSceneList' });
const res = await fetch(url, { ...options }).then(r => r.json());
return res;
},
async getUserDeviceList() {
const { url, options } = factory({ serviceName: 'getUserDeviceList' });
const res = await fetch(url, { ...options }).then(r => r.json());
return res;
},
async getUserScene({ scene_id }) {
const { url, options } = factory({ serviceName: 'getUserScene', serviceArguments: { scene_id } });
const res = await fetch(url, { ...options }).then(r => r.json());
return res;
}
}; // store/store1.js
import SERVICE from '../lib/service1';
const store = {
state: {},
actions: {
getUserSceneList() { return SERVICE['getUserSceneList'](); },
getUserScene({ scene_id }) { return SERVICE['getUserScene']({ scene_id }); }
}
};
export default new Vuex.Store(store);Stage 1 demonstrates clear module responsibilities but still repeats request handling in each service function.
Stage 2 – Olive‑Shaped Interface and Isomorphic Mapper
The repeated logic is extracted into a generic serve() function, and two implementation options are shown: a dynamic object builder or a Proxy that intercepts property access.
// lib/service2.js
function serve({ serviceName = '', serviceArguments = {} }) {
const { url, options } = factory({ serviceName, serviceArguments });
return fetch(url, { ...options }).then(r => r.json());
}
export default new Proxy({}, {
get(target, serviceName) {
return serve({ serviceName, serviceArguments: target[serviceName] });
}
});
// (Alternative dynamic generation code is commented out)A custom mapper aligns Vuex’s mapActions and mapMutations with the new service layer, removing duplicated action definitions.
// store/store2.mapper.js
import SERVICE from '../lib/service2';
export const mapActions = API => API.reduce((pre, serviceName) => {
pre[serviceName] = ({ commit }, serviceArguments) =>
SERVICE[serviceName](serviceArguments).then(res => {
commit(serviceName, res.data);
return res;
});
return pre;
}, {});
export const mapMutations = MAP => Object.keys(MAP).reduce((pre, serviceName) => {
pre[serviceName] = (state, data) => {
state[MAP[serviceName]] = data;
};
return pre;
}, {}); // store/store2.js
import { mapActions, mapMutations } from './store2.mapper';
const store = {
state: {},
mutations: { ...mapMutations({ getUserSceneList: 'scenes', getUserScene: 'scene', getUserDeviceList: 'devices' }) },
actions: { ...mapActions(['getUserSceneList', 'getUserScene', 'getUserDeviceList']) }
};
export default new Vuex.Store(store);Stage 2 reduces the service layer to a handful of lines, achieving the “olive‑shaped” entry‑point concept.
Stage 3 – Response Normalization and Three‑Level Error Handling
The mapper now normalizes responses to a unified shape { ok: true/false, payload: data|error } . Errors are categorized into network errors, system errors, and interface errors, each assigned a distinct code.
// store/store3.mapper.js
import SERVICE from '../lib/service3';
export const mapActions = API => API.reduce((pre, serviceName) => {
pre[serviceName] = ({ commit }, serviceArguments) =>
SERVICE[serviceName](serviceArguments).then(res => {
commit(serviceName, res.data);
if (res.code === 0) {
return { ok: true, payload: res };
}
return { ok: false, payload: res };
});
return pre;
}, {});
export const mapMutations = MAP => Object.keys(MAP).reduce((pre, serviceName) => {
pre[serviceName] = (state, data) => { state[MAP[serviceName]] = data; };
return pre;
}, {}); // lib/service3.js
function serve({ serviceName = '', serviceArguments = {} }) {
const { url, options } = factory({ serviceName, serviceArguments });
const controller = new AbortController();
const signal = controller.signal;
// Timeout guard (Promise‑based)
function TIMEOUT_GUARD({ reject, controller }) {
setTimeout(() => {
reject({ code: 7002, error: new Error('Request timeout') });
controller.abort();
}, 10);
}
return new Promise((resolve, reject) => {
TIMEOUT_GUARD({ reject, controller });
fetch(url, { ...options, signal })
.then(res => {
const { ok, status, statusText } = res;
if (ok) {
return res.json().then(r => {
const { code, msg, reqid } = r;
if (code === 0) return r;
return { code: 9001, error: r };
});
}
return { code: 8001, error: { status, statusText } };
})
.then(resolve)
.catch(error => {
if (error.name === 'AbortError') {
return { code: 7001, error };
}
if (error.code === 7002) return error;
return { code: 7000, error };
})
.then(resolve);
});
}
export default new Proxy({}, {
get(target, serviceName) {
return serve({ serviceName, serviceArguments: target[serviceName] });
}
});Stage 3 integrates the three‑level error model (network, system, interface) into the unified response format, allowing Vue components to handle all outcomes consistently.
Conclusion
The article demonstrates how incremental refactoring—separating concerns, abstracting a minimal “olive‑shaped” API surface, and normalizing responses with structured error handling—produces concise, maintainable front‑end code. Although the examples use Vuex, the same principles apply to other state‑management libraries such as Redux.
360 Tech Engineering
Official tech channel of 360, building the most professional technology aggregation platform for the brand.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.