Making a large scale app with vue.js (part 2): a little bit of Object Oriented Programming.

Hi everybody. This is the second part of a series about making a large scale frontend application with vue.js, inspired by an article from Anthony Gore: Is Vue.js a Good Choice for a Large-Scale Application? In the first part ( modularize your store! ), we saw how to organize and manage your data store, by creating modules for each data model needed in the app.

Here we’ll see how to go further by encapsulating a lot of computing related to the data inside ES2015 classes, to keep your store modules as light as possible on one hand, to control the data you want to keep in the browser, and also to have an abstraction layer between the frontend and the backend, being able to easily accept changes in the data model as the product evolves.

To illustrate let’s build a real-life project. As I can’t stand anymore Todo Lists, I propose something funnier: a minimalistic movie searching app, using The Movie Database (TMDb) open API.

The purpose will be very simple: we’ll have a text input field where we can enter a query string, and on submitting it the app shows up cards for the found movies with their title and a poster image. I’ve deployed the final build to a GitHub page for those who want to see without waiting: https://vertcitron.github.io/movie-search/

According to the API documentation, we’ll essentially need one endpoint to perform our searches, the /search/movie one: https://developers.themoviedb.org/3/search/search-movies. You can see that it returns a JSON with an array of movies data, each containing several pieces of information, including the title and the poster_path properties we need.

If you want to explore, you can find all the code for this project in a GitHub repo: https://github.com/vertcitron/movie-search.git. You will find the different steps of what’s following in the step1, step2, and step3 branches. The master branch is what has been deployed to GitHub Pages and is based on step3.

Let’s first check out the step1 branch. In this first attempt, I did the stuff according to what has been told in part 1, with a modular store, without going further. I pass quickly over the axios set up to request the API (axios is attached to the Vue’s prototype in main.js via the vue-axios plugin), and over the general app architecture, which is a very basic Vue Cli 3 scaffold with Vuex. The API key has been set up in an environment variable in the .env file.

Step 1: the naïve approach

As we’ve seen in part 1, let’s implement modules for each data resource we need in the application. The first module config.js is for the moment very simple: a movies prop in the state storing an array, the collection getter for this array, the setMovies mutation that writes it and the search action making the request to the API and handling the response:

As you can see, if the request is a success, we simply mutate movies with the parsed JSON, and we empty it on an error or if the search string was empty. On error, I chose the feedback to occur only in the console, to stay simple.

In the movies raw data, we’ll need the id property to have a unique identifier, the title and the poster_path property to display the poster image. But in the API response, this property is not sufficient to build the URL that will permit to display it (https://developers.themoviedb.org/3/getting-started/images). We’ll need for that to request another endpoint: configuration, that will bring us the missing variables to build images URL. We’ll find it in images.base_url and images. poster_sizes in the response. That leads us to another module handling that endpoint:

Both modules are then imported into the store.js file.

The remaining stuff is quite simple: in the App.vue component we have the text input field with a v-model on the search data, and when it’s submitted the movies/search action is dispatched. We also dispatch the config/get action in the created hook as it has to be done only once. The result is handled by the MovieCard.vue component, included with a v-for loop over the movies/collection getter, each iteration giving it the movie data as a prop. These movie cards show up in a flex container for a pleasant layout.

Inside MovieCard.vue component, we simply display the movie title property and an image tag to show the poster. But as we said, we have to rebuild the images URLs. The naïve first attempt is to build it from inside the component with a computed property posterUrl, that’s what I did in step 1:

This works good, but what if we need later the poster URL elsewhere in the application? We can duplicate this code, which is an anti-pattern. We can also put this in a mixin, and add it to any component that needs it. That’s much better, but it complexifies the app by spreading data handling in different places, which is bad for maintainability.

Step 2: handle data computing from the relevant store module

From my experience with large and complex data sets, I learned that the clearest way is to manipulate the data from where it’s stored. So the logic wants we generate the poster URL from the movies module, with a dedicated getter.

But as it stores not a movie but a collection of movies, this getter has to be informed on which movie to handle. So it will return a function taking a movie id as a parameter, and we implement another similar getter returning the movie of a given id:

Then we can remove the posterUrl computed property from the MovieCard.vue component, and directly call the movies/posterUrl getter:

We are happy, all the stuff related to movies is now handled in one place. But this is a very simple example. Image how the module can become if, after several months of evolution, our app deals with all the complex data returned by the API… movies.js could become really huge (it happened…).

Step 3: encapsulating data and methods in a class

Another thing: for me, the fact to call a getter to have a movies collection, and then to give back a movie from this collection to perform computing on its data sucks. Computing should be part of the object itself.

Bingo, that’s exactly what permits javascript prototypes, and in a very pleasant manner with the ES2015 classes implementation. It seems obvious that we need a Movie class, managing in one place the data for a movie and the methods to compute things on that data.

In a new models directory, let’s create a MovieClass.js file, containing that class:

You can see first that, in the constructor where we pass the raw data, we keep in only the properties that are relevant to our application, where we stored all the information in the previous step. This is a primary gain as if an endpoint responds with thousands of results, the memory allocation of the browser will be better.

Next, we added in the class a posterUrl getter, which performs the computation for a complete image URL, getting the missing information from the config store module.

We have now to modify the search action in the movies.js store module, where we have to remove the unnecessary getters introduced at step 2 and transform the raw data given by the endpoint to Movie objects before storing it:

Now, we have only Movie objects in the collection, the MovieCard.vue component can now become very very simple:

Imagine now that you add things to display in the movie cards. You just have to add the properties and methods you need in MovieClass.js, and elsewhere you have a Movie object, as for example in MovieCard.vue, you’ll have all that properties and methods immediately available.

Imagine also that TMDb changes a thing, for example, poster_path becomes poster_location. You just have to replace:

this.poster_path = rawData.poster_path
by :
this.poster_path = rawData.poster_location

and your app continues working without anymore modification…

This pattern may seem heavy for such a simple thing that we’re doing here, but trust me, with really complex data, you will avoid a great number of headaches…

To go further, we could remove the created hook that calls the config/get action from App.vue, and inside MovieClass.js, test if the config data is defined when entering the posterUrl getter, and perform the needed action from there if not. To do this you’ll have to transform the getter to return an asynchronous function like this:

I understand that all of this may seem strange for some people. So don’t hesitate to clone the repository ( https://github.com/vertcitron/movie-search.git ) and play with the different steps (check out branches step1, step2, step3), and if needed, don’t hesitate to ask me anything.

EDIT : What to do when modifying data ?

I make this late addition to the article because as I only explained what to do when reading data, some of you wanted to know how write or modify data to the API.

Unfortunately, in the example I took with TMDb API, there are obviously no public unauthenticated POST, PUT or DELETE endpoints to use. There is an endpoint that permits to rate a movie but that needs to have a session id.

I don’t want to add complexity here, so I will just explain, without showing a working example, because it involves too much things to get a valid session id, and I don’t want this article too long.

Let’s imagine we have in the movie cards a component showing the rate of the movie (as 5 stars for example), and allowing the user to submit a vote.

When the user clicks on a star to vote, it will trigger a vote action in the movies module: this.$store.dispatch('movies/vote', { id: this.movie.id, rate: val })

In the movies module, we’ll need a mutation that updates a movie in the list. As usual, we use a Vue.set to avoid reactivity problems.

When mutating a data to a backend, the idea is first to submit the modification to the API, then wait a response, if successful ask for the modified data, and when it comes, update with it the local data. Doing so, you are certain that your store always remains synced with the backend.

That said, here is our new action. The vote endpoint from TMBd just respond that it’s ok, but don’t send back the modified movie data. So we have to request it again on GET movie/id when vote has been sent, and then we mutate the state with received data :

In others cases, in a DELETE operation for example, you should do the same : delete your local data only if you got the confirmation from backend that things have really been deleted, and so on.

What’s next…

In the next part, we’ll show how we can use several APIs from the same frontend app by abstracting them in the same OOP manner.

Thank you very much for reading and best regards.