2

I have a Vue3 app in which I use the following approach to API calls:

  • there's an api folder, inside which lie functions that directly make a call to the backend. For example:
export async function getCourses(): Promise<Course[]> {
    const response = await axios.get(`/courses/`);
    return response.data;
}

export async function getCourse(courseId: string): Promise<Course> {
    const response = await axios.get(`/courses/${courseId}/`);
    return response.data
}

export async function getExercises(courseId: string): Promise<Exercise[]> {
    const response = await axios.get(`/courses/${courseId}/exercises/`);
    return response.data
}
  • there a Vuex store, whose actions invoke those functions in the api folders and commit data to the store's state

  • Vue components access the store state and dispatch actions to it in order to retrieve data

The problem I have with this approach is that the api functions get extremely repetitive. I have to define 4 different functions just to enable CRUD on an entity, and all of them take almost the same parameters (for example, in my app, all resources are sub resource of a course and hence require the courseId parameter for fetching) and have the same structure (await an axios call and return the response's data).

I'd much prefer a method where I declaratively define the routes and the parameters they expect (as far as url components such as id's), and then be able to type-safely do something like this:

getApiService().courses(courseId).delete() // deletes a course
getApiService().courses(courseId).exercises().get(exerciseId) // returns an exercise
getApiService().courses(courseId).exercises().post(payload) // creates and returns an exercise
getApiService().courses(courseId).exercises().get(exerciseId).choices().list() // returns the list of choices for an exercise
// and so on

What would be a good way to accomplish this? Are there any existing packages that do this for Vue3?

3
  • It generally may be better to keep it WET to some degree, unless you deal with serious amounts of repetitions, this results in more maintainable code. See deconstructconf.com/2019/dan-abramov-the-wet-codebase. It's unclear how getApiService().courses(courseId).exercises().get(exerciseId).choices().list() is going to work. It's asynchronous and can't return a result immediately to chain it. Is it supposed to be Fluent interface pattern? Commented Aug 12, 2022 at 8:36
  • Kind of. The example you picked would only really make a call upon calling the last list(). The previous chained methods would pretty much just build the call url. Commented Aug 12, 2022 at 14:06
  • I see. Then it's unspecific to Vue. You may want to search how this is done in some libs that use fluent interface+ts. Basically with Fluent Interface you build action object (Command pattern) and return this with each method except list, and and execute a command with list. If you do this for your own use, not for learning purpose or public lib with fancy api, this is 100% overengineering you'll regret later. Most times KISS beats DRY, just takes some practice to see when it really doesn't Commented Aug 12, 2022 at 14:46

2 Answers 2

1

You're wandering into the middle ground of wanting to slightly improve the interface of a worldwide favourite industry standard library. Don't do it ! It's so fraught with pitfalls. Your desired interface has your concepts in the code (courses) so only you are going to write it. But the thing you write will be flaky and incomplete, and will get in your way as soon as you need to handle an error condition or add a custom header. Longer I live, more I think everyone's project should be one layer thick. Search your conscience as soon as you start calling your own code. Use these fabulous libraries, vue, axios etc, and marvel at how much they're doing for you and how terse their interfaces are. It was much worse before. A little bit of repetition is worth it for the sake of simplicity, clarity, ease of maintenance.

Sign up to request clarification or add additional context in comments.

Comments

1

The current code is clean and reasonably WET, this makes this much easier to maintain than possible DRY solutions. The hazards of zealous DRY approaches are covered in this popular talk and explained in this article.

If it's known these functions always return untransformed data, they could be replaced with some DRYer helper function:

const apiCall = <T extends any>(url: string, options?: AxiosRequestConfig<T>): Promise<T> => {
  const response = await axios<T>(url, options);
  return response.data
}

Passing HTTP method through options makes it more flexible to use.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.