In this article, I will talk about how to use dependency injection (DI) in a Typescript Vue app with Awilix.
Before I go ahead and just talk about how to implement it, I will explain why I use DI in the project and what I want to achieve.
The problem
Some months ago, I got a ticket to implement a preview mode in a widget repository. This widget is a checkout widget where the user will fill out some forms to pay for the product.
This widget is made with VueJS and is using Vuex to create some modules. Almost all modules have one action that will trigger one request, set the loading state while waiting for the response, then it will get its data and save it in the module together with a success state. If the request fails, it will set the status as failed in the module.
So, to implement the preview mode I have to replace all these requests with mock data. For example, instead of fetching the API to validate the payment to get the response, it will return an object with a fake success response to prevent this request.
This way, the user can navigate through the widget flow without any request being made, removing the risk of changing something wrongly.
How to achieve the preview mode
First, let’s check how to do this preview mode without DI.
Not using Dependency Injection
As I mentioned, the preview mode consists of showing the app workflow while it is being used, but instead of displaying real data, it will display fake data.
To do that, we must only return fake data instead of hitting the real server. Doing that is very simple, you just need to add a verification on each server request to return fake data when the preview mode is enabled.
Before
const data = await requestDataFromServer()
After
const data = isPreviewMode ? mockedData : await requestDataFromServer()
This would definitely solve the problem but I don’t think is the best thing to do because we will need to add this verification on all requests.
Also, the preview mode logic will be spread in a bunch of files and we would add more complexity to each file. Because now, we must add a new responsibility: check if the preview mode is enabled.
Thinking in a SOLID way, it’s always good to avoid this situation where a file has multiple responsibilities because that leads to a complex code with a bunch of logic, huge tests, worse readability, and hard to add a new feature or even fix a bug.
The last thing is if someone adds a new request in another file without knowing about the preview mode, it may break the preview mode. Because now, in one place of the app it will make a request instead of using a mock data.
Using Dependecy Injection
I’m not gonna lie, I love the concept of DI and I always want to use it. Therefore, in this task DI was the first thing that came to my mind. But before that, I analyzed if it was worth doing, since by using it I would’ve had to refactor a bunch of files and if they had no tests, I would’ve had to create them to make sure everything would work after this big refactor.
But let’s talk about DI, with it we can create a container where you can register some class or function with the implementation we need.
Let’s go to an example:
Imagine we have an app that will validate your credit card. In this case, we can create an interface to specify how this service will work.
// src/services/CreditCardService.d.ts
export interface CreditCard {
number: number;
holderName: string;
expiration: {
month: number;
year: number;
}
cvv: number;
}
export interface CreditCardService {
valid(creditCard: CreditCardData): Promise<boolean>;
}
Here I created two interfaces, one with the credit card information (CreditCard
) and another one with the service contract (CreditCardService
). So, the service will have one method called valid
that will receive the credit card data and returns a boolean from a Promise
saying if the credit card is valid or not.
I’m assuming this will return a Promise
because you may use an API to validate the credit card.
Now, let’s implement it! The first thing is to create the class that will implement the CreditCardService
interface. To do that you only need to add implements CreditCardService
after the class name. It will show an error saying that you must implement the CreditCardService
interface.
To implement it you must add a valid
method with the same interface signature.
import { CreditCardService } from '@/services/CreditCardService'
class CreditCardApiService implements CreditCardService {
valid(creditCard: CreditCard): Promise<boolean> {
return fetch('https://my-credit-card-api/v1/validate', {
method: 'POST',
body: JSON.stringify(creditCard)
})
.then(response => response.json())
.then(data => {
return data.isValid
})
}
}
export default CreditCardApiService
Now, to use it let’s create a container that will hold this credit card service.
Here I will create an interface for the container called Container
that will have all the service signatures. Then, I will create our container using this interface:
import CreditCardApiService from '@/services/CreditCardApiService'
import { CreditCardService } from '@/services/CreditCardService'
interface Container {
creditCardService: CreditCardService;
}
const container: Container = {
creditCardService: new CreditCardApiService(),
}
export default container
To use the credit card service we must use the container:
import container from '@/container'
const { creditCardService } = container;
await creditCardService.valid(myCrediCard) // true or false
In order to add this preview mode, we must:
1 – Create a CreditCardMockedService
class that will implements CreditCardService
interface
2 – Add a verification in the container creating to user CreditCardApiService
or CreditCardMockedService
1)
import { CreditCardService } from '@/services/CreditCardService'
class CreditCardMockedService implements CreditCardService {
valid(creditCard: CreditCard): Promise<boolean> {
return Promise.resolve(true)
}
}
export default CreditCardMockedService
2)
import CreditCardApiService from '@/services/CreditCardApiService'
import CreditCardMockedService from '@/services/CreditCardMockedService'
import { CreditCardService } from '@/services/CreditCardService'
// image we have a config
file that will hold this info
import { isPreviewMode } from '@/configs'
interface Container {
creditCardService: CreditCardService;
}
const container: Container = {
creditCardService: isPreviewMode ? new CreditCardMockedService() : new CreditCardApiService(),
}
export default container
This way, the one who is using the container
doesn’t know what is the real creditCardService
implementation. It doesn’t even know the CreditCardMockedService
and CreditCardApiService
exist. It only knows that creditCardService.valid
will validate the credit card.
With this strategy we can put the preview mode logic in the container creation, we just need to create a class (or function) with the real implementation and another with mock data. One good thing is that we will have two files, each file with your logic and context, with fewer responsibilities and easier to test.
Let’s check each file’s responsibility:
CreditCardApiService
-> it will use some API to check if the credit card is valid or not.CreditCardMockedService
-> it will always say that the credit card is valid.container
-> it will use the real or mocked implementation according toisPreviewMode
variable.- Who is using the
container
-> it will usecreditCardService
to know if a credit card is valid or not.
A quick glance at SOLID
As you can see, now we will have smaller pieces of code, each one with it’s own responsibility, which makes your code more SOLID.
By the way, if you don’t know what SOLID is I advise you to search for materials about it. This will make your code clearer, and easier to test and understand. Here is a Sandi Metz video that I always recommend for developers that don’t know about SOLID: GORUCO 2009 – SOLID Object-Oriented Design by Sandi Metz.
Now, going back to the main subject.
The Vue app
Before diving into the DI implementation, let’s talk about the widget/project specs. In this project we are using:
vue
(v2.6.10
)vue-property-decorator
(v8.3.0
) -> to use typescript in our component as classesvuex
(v3.1.2
) -> to manage the application statetypescript
(v3.7.3
).
As I mentioned before, the app’s logic was inside the vuex modules, and they were structured like this:
- src
- store
- modules
- config
- reservation
- confirmation
...
The config
contains all widgets configs that will be different according to each client or scenario. All the other modules contain app logic like:
- Some user or event will trigger a module’s action, setting the loading state
- This action will make a request that may use another module state
- The result of this action will update the module state, either for success or error state
Another detail, the whole store was not using typescript 😕. So, I first have to add types to all of them, and to do that I use vuex-module-decorators
(v2.0.0
) because its works like vue-property-decorator
that we are using to create the components.
After refactoring all vuex modules with Typescript I started the DI implementation. The first thing to do was install awilix
package, I choose this library because it’s small and simple to use and set up it.
Sample app
To show you guys the code and a real example, I created a repository on Github with the same widget pattern.
- Github Repository: https://github.com/nolleto/vue-with-awilix-sample
- Live Demo: https://vue-with-awilix-sample.netlify.app/
In this demo, you can search and list all users from Github and their repositories. You can change to preview mode to return mocked data.
So, when searching for a user name, it will request Github’s API, which will return a list of users. When you select a user, the app will do another request to Github’s API to get all the repositories to list in the app.
In this sample app, I created a container
that will have a sourceCodeService
property, which will return a service that contains methods to get the users (searchUser
) and repositories (searchUser
), and it will have a property name
that contains the service name (Github
).
I called it sourceCodeService
instead of githubService
because this way I can create an implementation for any service like Github. For example, if we want to add support to GitLab, we just need to create a GitLabApiService
and set it in the sourceCodeService
property from the container
.
Implementing DI in the Vue app
Going back to the widget, one important thing to mention is that the preview property is on the config
vuex module. When the widget is mounted it will trigger a request to get the configs from the server and set it in the module. The only way to know if the widget is with the preview mode enabled or not is by accessing the config
vuex module.
The next step is to decide where to put my container creation. Initially, I was thinking to add this file at src/container.ts
but since I must get the config
vuex module to know if the preview mode is enabled or disabled, I decided to create a new vuex module that will hold the container. This way, I can get the preview mode value and create a container with this information.
Creating the container
import { Action, Module, Mutation, VuexModule } from '@/store/modules/vuex-module-decorators'
import createContainer, { ContainerType } from './createContainer'
@Module({
namespaced: true
})
class Container extends VuexModule {
current!: ContainerType['cradle']
@Mutation
setContainer(newContainerCradle: ContainerType['cradle']): void {
this.current = newContainerCradle
}
@Action
updateContainer(): void {
const { commit, rootState } = this.context
const { isPreviewMode } = rootState.configs
const newContainer = createContainer({ isPreviewMode })
commit('setContainer', { ...newContainer.cradle })
}
}
export default Container
Source: src/store/modules/container/index.ts
This module will hold the container’s cradle, which is an object that will contain the implementations. Something like:
{
myService: myServiceImplementation,
...
}
This way, I can access the current
state from the container
vuex module in other modules.
Here is the createContainer
implementation that this module is using:
import { AwilixContainer, asClass, createContainer } from 'awilix'
import GithubApiService from '@/services/sourceCode/github/GithubApiService'
import SourceCodeMockService from '@/services/sourceCode/SourceCodeMockService'
import { SourceCodeService } from '@/services/sourceCode/SourceCodeService'
type ContainerDependencies = {
sourceCodeService: SourceCodeService
}
type ContainerType = AwilixContainer<ContainerDependencies>
type Props = {
isPreviewMode: boolean
}
const createDIContainer = (props: Props): ContainerType => {
const { isPreviewMode } = props
const container = createContainer<ContainerDependencies>()
container.register({
sourceCodeService: asClass(isPreviewMode ? SourceCodeMockService : GithubApiService)
})
return container
}
export { ContainerType }
export default createDIContainer
Source: src/store/modules/container/createContainer.ts
The createDIContainer
is the function that the module is calling and it will receive an isPreviewMode
property to know which implementation will be injected: SourceCodeMockService
or GithubApiService
. Here, I’m using awailix
to create the DI container.
The last piece of the container creation is to trigger the updateContainer
action from the Container
vuex module. Otherwise, the current
state will always be undefined
. To achieve this I created a vuex plugin:
import { StoreType } from '@/store/Store'
const containerPlugin = (store: StoreType): void => {
const updateContainer = (): void => {
store.dispatch('container/updateContainer')
}
// Initial setup
updateContainer()
store.subscribe((mutation): void => {
if (mutation.type === 'configs/setConfigs') {
updateContainer()
}
})
}
export default containerPlugin
Source: src/store/plugins/container/index.ts
This plugin will trigger the updateContainer
action through calling store.dispatch('container/updateContainer')
. As you can see, there are two moments when I update my container:
- When the store is mounted
- When the configs change
To know when the configs were being changed, I added a subscription to the store. That will be triggered on every mutation, then I just check if the mutation is the one that will change my configs.
Now my container is working and synced with my configs. The next step is to consume this container in another module.
Consuming the container
As I mentioned, in this repo the user will select a Github user by typing a name, then a request will be made to the Github API and I will list the users that will come from this response. So, here I create a vuex module to hold this logic:
import { Action, Module, Mutation, VuexModule } from '@/store/modules/vuex-module-decorators'
import { SourceCodeServiceUser } from '@/services/sourceCode/SourceCodeService'
import { getErrorMessage } from '@/selectors/error'
type Status = 'idle' | 'loading' | 'success' | 'error'
@Module({
namespaced: true
})
class SourceCodeUsers extends VuexModule {
users: SourceCodeServiceUser[] = []
status: Status = 'idle'
errorMessage: string | null = null
get hasError(): boolean {
return Boolean(this.errorMessage)
}
get isLoading(): boolean {
return this.status === 'loading'
}
@Mutation
setPending(): void {
this.users = []
this.errorMessage = null
this.status = 'loading'
}
@Mutation
setFulfilled(newUsers: SourceCodeServiceUser[]): void {
this.users = newUsers
this.status = 'success'
}
@Mutation
setError(errorMessage: string): void {
this.errorMessage = errorMessage
this.status = 'error'
}
@Action
async search(term: string): Promise<void> {
const { commit, rootState } = this.context
const { sourceCodeService } = rootState.container.current
commit('setPending')
try {
const users = await sourceCodeService.searchUser(term)
commit('setFulfilled', users)
} catch (error: unknown) {
const errorMessage = getErrorMessage(error)
commit('setError', errorMessage)
}
}
}
export * from './composition'
export default SourceCodeUsers
Souce: src/store/modules/sourceCode/users/index.ts
The main thing here is the search
action that will be triggered every time a user types a user name to search. To get the Container
module I need to access this.context.rootState.container
. Since this module has a current
state that will hold the "real container", I must access it through this.context.rootState.container.current
. The result of it will be the cradle
that is an object with the services implementations.
Explaning with code:
@Action
async search(term: string): Promise<void> {
const { rootState } = this.context
const containerVuexModule = rootState.container // the vuex module
const cradle = containerVuexModule.current // the container object
const { sourceCodeService } = cradle // the container implementation for sourceCodeService
Now, we can use this sourceCodeService
to search the github user:
const users = await sourceCodeService.searchUser(term)
Is important to mention that the SourceCodeUsers
vuex module doesn’t know the real implementation.
Toggling preview mode
In this app, I created a component called AppConfigs that will render a switch to enable or disable the preview mode.
Every time it changes it will trigger the updateConfigs
action from Configs
vuex module:
import { Action, Module, Mutation, VuexModule } from '@/store/modules/vuex-module-decorators'
type AppConfigs = {
isPreviewMode: boolean
}
@Module({
namespaced: true
})
class Configs extends VuexModule {
isPreviewMode = false
@Mutation
setConfigs(newConfigs: AppConfigs): void {
this.isPreviewMode = newConfigs.isPreviewMode
}
@Action
updateConfigs(newConfigs: AppConfigs): void {
const { commit } = this.context
commit('setConfigs', newConfigs)
}
}
export * from './composition'
export default Configs
Source: src/store/modules/configs/index.ts
When this happens, it will also trigger my plugin that watches the mutations and consequently, it will re-generate the container.
Here is the flow when updateConfigs
is triggered:
1 – It will update the Configs
vuex module with new configs
2 – The containerPlugin
will dispatch 'container/updateContainer'
action
3 – The Container
vuex module will re-create the container
and set its cradle in current
state.
4 – All places that are using the container will use the new implementation.
Conclusion
Using DI, we can avoid adding more complexity to each vuex module by extracting each logic in one class. I didn’t put any samples here but we could also have clearer tests (check here all the tests: link).
Another win is that now your vuex modules requests are using a service so they don’t know, for example, the fetch URL or anything about the request implementation, making it also easier to test.
I had a great time implementing this solution and I love the way it works!
But what are your thoughts about it? Do you like my implementation? Would you do something different? Don’t forget to comment here 😉
Also, if you want to, you can fork my sample repo and try to add a GitLab or BitBucket integration 🚀
Thanks for reading my article ❤️
We want to work with you. Check out our "What We Do" section!