How we updated a large internal service and what came of it

The third version of the beloved Vue framework has been out for a long time, and most of those using it have upgraded to the new version. But there will always be those, like our team, who have been putting this transition off to the back burner of tech debt — after all, there are more priority tasks! However, sooner or later this day comes, and here it is for us.

In this article I want to tell you how we switched to the new version, given that the service is quite large and very important for users. But first things first.

Description of the service

Our team is developing internal services for working with orders. In particular, we are engaged in our own sales, that is, these are orders for those goods that Ozon itself orders from the supplier, independently stores and sells on its behalf. Actually, in order to organize this entire work flow, we are developing internal services, which in turn are used by managers.

Most of our services are written using the Vue framework, but for added convenience we use Nuxt.

Frontend platform

Like most large companies, we have our own frontend platform, which is the so-called foundation for any new service. It is designed to save us from reinventing the wheel and has all sorts of features, which allows us to quickly start writing the interface of a new service without any problems. But it appeared later than our service, and this led to the fact that we wrote our interface on Nuxt2 with its own “wheels”. And, as it usually happens, everything came down to the fact that now, in addition to the fact that we need to move from the second version to the third, we also need to transfer everything that we have developed on our own, relying on platform solutions.

Why did we decide to upgrade?

  1. The official EOL (end of life) of Vue 2, which occurred on December 31, 2023, means that it no longer receives new versions, updates, and fixes.

  2. Vite is a fast, powerful, and easy-to-use alternative to webpack.

  3. New Vue – new features:

    1. Improved optimization and performance – new tree-shaking, faster rendering, smaller bundle size.

    2. Composition API.

    3. Full TypeScript support.

    4. Built-in Teleport.

    5. Support for the Suspense concept.

  4. Lack of support in internal developments, because it is not included in the platform.

  5. Positive attitude of management towards investing time in renewal.

Beginning of the transition

Well, let's get started!

First of all, it is worth noting that here I will tell you about only one of our services. I have already mentioned that we are developing several services, so when we started migrating this service to the new version of the framework, we created several projects on Nuxt3 from scratch. This undoubtedly gave us experience and some knowledge about the new version of the framework, which later helped us when migrating this service. I would also like to mention that for all our services we have our own library of common code solutions, components, service solutions. This library was also deliberately migrated to the new version of the framework. It mainly consists of regular components, so its migration consisted only of migrating the components to the new version of the framework.

The main problem is to understand where to start. Since Ozon is a large company with a large number of teams and interfaces, colleagues from the platform team created a project translation tool for us – codemod. On the Internet, you can find a lot of such codemods that help migrate from one version of a library or language to another, but each of them has its own features and requirements for the initial state of the code. This is what happened in our case. The codemod developed by the platform team worked quite well with those projects that were originally written using the platform. But our project was implemented using our own developments, so all parts of the service translated by the codemod had to be heavily edited so that they eventually became working. Therefore, it was decided to initiate the project from scratch using the platform, fortunately we have it (but in its absence, we will naturally start with a simple initiation of a new project on a new version).

Okay, the project has been initiated, but what next? Where to start?

Everyone decides for themselves how they find it easier to translate a large project, but we decided to start with translating our repository. We thought it was practical for several reasons:

  1. The store is quite massive and is used in many places. In our project, the store is especially important, since all the main logic of working with data is contained in it. Initially, our service had only one page displaying a list of orders. Various manipulations could be performed with each order – from correction to adding/changing/deleting a comment. And therefore, it was decided to break the entire store into modules, where each module was responsible for its functionality, applied directly to orders (you can see all the modules in the illustration below). For example, demands-lists-filters is responsible for filters, and demands-lists-reports is responsible for the generated reports.

  2. After translating pages/components, we will be able to immediately check the functionality of the translated part.

  3. We don’t need to translate the code that works with our APIs, because it’s done for us by the platform. In addition, all methods and types are automatically generated using the swagger-typescript-api package, which allows you to conveniently work with Rest API, provided that you write in TypeScript. This package generates methods and types used in these methods by endpoint for quick work with the API. The only important condition is that the API has a swagger contract. If you want to learn more, you can go to github repository and read in more detail.

Switch to Pinia

Our store, written in Vuex, consisted of quite a large number of modules. To be precise, 48.

Previously, we used Vuex in an unusual format, using classes with decorators and other features. Now we have switched to Pinia, as it has become the official library for state management in Vue 3, being a more lightweight, but no less convenient alternative to Vuex.

The main advantages of Pinia that I have noticed are:

  • Ease of use – working with Pinia is simple, everything is intuitive, and even a novice specialist will be able to quickly understand what and how it works.

  • TypeScript support out of the box.

  • A simple, flexible system of stores, instead of Vuex modules and namespaces.

  • Excellent integration with DevTools.

Below you can see the differences between writing the same module but using different state storage libraries. Here you can see another advantage of using both Nuxt3 and Pinia.

The advantage of Pinia is that there is no longer such a clearly defined concept of changing states through mutations as in Vuex on Nuxt2, and you can change the state in any function without losing reactivity.

The advantage of Nuxt3 (or, to be more precise, the Composition Api, which is presented in Vue 3) is visible here if you pay attention to working with request cancellation tokens. To work with the API, we use tokens, which, in turn, need to be updated, and by which requests need to be stopped if necessary. And previously, for each request, we created a separate token and a separate mutation to work with it, but now we have written a simple composable that carries these functions, which made life with them easier, and there is less code.

Vuex + Nuxt2
import { VuexModule, Module, VuexMutation, VuexAction, InjectNuxtContext, WithNuxtContext } from '@gdz/types'
import axios from 'axios'

import { NuxtAppContext } from '~/types'
import { IUsersStore, ProcessedUser } from '~/types/common/users'
import { UserInfoType } from '~/api/types'
import { getProcessedUsers } from '~/utils/users'

@Module({
    stateFactory: true,
    namespaced: true,
})
export default class Users extends VuexModule implements WithNuxtContext<IUsersStore> {
    lib!: NuxtAppContext

    cancelToken: IUsersStore['cancelToken'] = axios.CancelToken.source()

    availableUsers: IUsersStore['availableUsers'] = []

    @VuexMutation
    saveUsers(users: UserInfoType[]) {
        this.availableUsers = users
    }

    @VuexMutation
    createCancelToken() {
        this.cancelToken = axios.CancelToken.source()
    }

    @VuexAction
    @InjectNuxtContext
    async getUsers() {
        if (this.availableUsers.length > 0) {
            return
        }
        try {
            this.cancelToken.cancel()
            this.context.commit('createCancelToken')
            const { users } = await this.lib.$api.demandsLists.getDemandsListsUsers(this.cancelToken.token)
            this.context.commit('saveUsers', users)
        } catch (error) {
            if (axios.isCancel(error)) {
                return
            }
            console.error(error)
        }
    }

    get processedAvailableUsers(): (UserInfoType & ProcessedUser)[] {
        return getProcessedUsers(this.availableUsers)
    }
}
Pinia + Nuxt3
import axios from 'axios'
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

import { getApi } from '~/api/client'
import type { IUsersStore, ProcessedUser } from '~/types/common/users'
import { getProcessedUsers } from '~/utils/users'
import { useCancelToken } from '~/utils/store'
import type { DemandListUserType } from '~/api/types'

export const useUsersStore = defineStore('users', () => {
    const api = getApi()

    const cancelToken = useCancelToken()

    const availableUsers = ref<IUsersStore['availableUsers']>([])

    async function getUsers() {
        if (availableUsers.value.length > 0) {
            return
        }
        try {
            cancelToken.recreateCancelToken()
            const { users } = await api.gdzApiGateway.demandsLists.getDemandsListsUsers(cancelToken.cancelToken.value.token)
            availableUsers.value = users || []
        } catch (error) {
            if (axios.isCancel(error)) {
                return
            }
            console.error(error)
        }
    }

    const processedAvailableUsers = computed<(DemandListUserType & ProcessedUser)[]>(() => {
        return getProcessedUsers(availableUsers.value)
    })

    return {
        availableUsers,
        getUsers,
        processedAvailableUsers,
    }
})

Next, we simply accumulate patience and gradually transfer each storage module, trying not to forget anything anywhere.

When translating a repository, we often come across the need to use notifications that were implemented using the Nuxt plugin, so for a more comfortable translation, let's first translate all the plugins. There is nothing tricky in translating plugins from the second version to the third. Unlike the second version, you now need to use the defineNuxtPlugin function, which accepts a function with only one argument – nuxtApp, and instead of the inject function, you now need to use the provide function. Otherwise, there should be no difficulties in translating. It is only worth mentioning one point that if you use Nuxt and do not explicitly register plugins in the nuxt. config. ts (js) file – Nuxt allows you not to explicitly connect plugins, that is, everything that is located in the plugins folder in the root of the project will be connected automatically. And you need to keep in mind that plugins are connected in alphabetical order. And if you need, for example, for a certain plugin to be initialized after another, then either connect them explicitly through the config, or name them so that they are located in the desired sequence.

We decided it would be easier to rename the plugin files, so we added letter prefixes to organize their connections as we needed.

Translation of components and pages

Having translated the storage and plugins (and at the same time all sorts of auxiliary things like middlewares, etc.), we move on to the sweetest part, namely pages and components. Here, as in any other place, everyone decides for themselves how it is more convenient for them to do the translation, but we have derived the following sequence, which turned out to be the most convenient and productive for us:

  1. We translate all common components that are used in other components/pages.

  2. We go page by page and translate first all the components that relate to this page, and then the page itself. In our case, there were no special problems here either. But I will mention that in Nuxt3 (Vue 3) the reactivity system has changed a little. Now it is built on Proxy, so the Vue.set and Vue.delete methods, which were necessary for working with objects and arrays, are no longer needed. In Vue3, we have two types of reactive state declaration – ref and reactive. The main differences between them are:

    1. Ref is primarily used for primitive values, while reactive is used for objects, arrays, and Map/Set collections.

    2. To access the value of a state declared via ref, we must access the value property, while with reactive the access occurs as with a regular variable.

    3. There are a number of other features that are worth considering, but they are all well described in documentation. Test all functionality well after migration to be sure that everything works as you intended.

Then, just as measuredly, carefully and judiciously (as with the store modules), we translate component by component, so that eventually we translate everything. For us, this is about 231 components, which we translated for about 1.5 weeks, so don’t despair, everything is possible!

Measurements and conclusions

Once we have translated everything, we can see how it works for us, and most importantly, check how much the deployment (build) speed has changed.

Firstly, it is worth noting that the local dev server really starts much faster in the Vite + Nuxt3 bundle than the previously used Webpack + Nuxt2. Plus, let's not forget about Vite's HMR, which does not rebuild the bundle, but only updates the affected module, i.e. does not lead to a page reload and reset of all states, which also, in turn, albeit a little, but saves the developer's nerves and time.

Secondly, let's check what happened with the build and how the time it takes to deliver code to production has changed.

And here we will notice that earlier (on Nuxt2 + Webpack) we could see the following values:

From the presented values, we see that the average deployment time (TTM column) is ~ 7-8 minutes, and sometimes even rose to 14 minutes.
After switching to the new version, we got the following results:

We see that the average value, although slightly, has decreased to ~ 6 minutes. Yes, there are jumps up to 7-8, but, unfortunately, the deployment time also depends on many other factors.

In view of all the above, we conclude that switching to a new version of Vue (Nuxt) not only speeds up deployment, but also speeds up development time. It also adds convenience and comfort to the developer who will subsequently work with the project. The acceleration and comfort of the developer are provided by the same Vue3 innovations that I wrote about earlier, but to summarize:

  1. Composition API allows us to write flexible, modular component code. This allows us to better structure our code, reuse logic, and generally makes our code more readable.

  2. TypeScript out of the box, which saves time on modifying configs and installing additional packages, and allows you to immediately start writing code. Typing and autocompletion have also been improved in general.

  3. More transparent reactivity.

  4. Faster startup and build.

  5. Pinia is lightweight, fast, and easy to understand even for non-Vue experts (and has full TypeScript support).

  6. Convenient and fast Vue Devtools plugin for debugging.

Among other things, migrating to a new version is a good reason to get rid of legacy. In our case, when migrating a common component library, we got rid of a bunch of unnecessary, unoptimized code, which had a positive effect on the build and deployment. And, of course, we should not forget about relevance. By this I mean that the new version is actively being improved, new features and new fresh libraries appear, which sooner or later can help you in solving this or that problem.

From our observations, I can note that now, after the update, we have begun to implement tasks of varying degrees of complexity faster. If previously an average task took 3-4 days, now we can handle it in 2-3 days. The deployment speed has increased, which has repeatedly allowed us to make the necessary hotfixes, and in general, to make releases faster.

So, if you are still thinking about whether to upgrade or not, or you don’t think about it at all, or you are tormented by doubts that switching to a new version of the framework is just a waste of time and resources, then I hope our example can give you confidence. After all, switching to a new version of the framework is not only updating the number in package.json, but also a bunch of new cool features, increasing the speed and comfort of development.

I wish everyone success in this!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *