Implementing a Clean Architecture Modular Application in Nuxt/Vue Typescript — Part 2: Services

Introduction

In the first part we implemented the Domain layer: created the Task entity and a TaskRepository interface. Now we are going to create a TaskService that implements that interface. Then we will create a HttpService to fetch tasks from an API. This API does not exist, so we will create a mock service and use dependency injection to decouple the service from a specific implementation (like the mock or real API service).

What we will see in this part:

The full code is available in this repository.

The Service Environment

Figure 1: TaskService class diagram

The above diagram shows the main classes we will be building. The task service implements the TaskRepository interface. I also creates an instance of TaskParser in the constructor and we will inject the HttpService.

TaskService

TaskParser

HttpService

AxiosCreator

The tests first

// src/app/modules/task/infrastructure/__tests__/TaskService.spec.ts
import { cid, container, mockTransient, resetContainer } from "inversify-props"
import { containerBuilder } from "@@/src/ui/plugins/inversify"
import { IAxiosCreator } from "@@/src/app/shared/http"
import { ITaskRepository } from "../../domain"
beforeEach(() => {
resetContainer()
containerBuilder()
})
describe('TaskService', () => {
describe('getMany', () => {
it('should get tasks', async () => {
const service = container.get<ITaskRepository>(cid.TaskService)
const result = await service.getMany()
expect(result.isOk()).toBeTruthy()
})
it('should return error result', async () => {
const service = container.get<ITaskRepository>(cid.TaskService)
const result = await service.getMany()
expect(result.isErr()).toBeTruthy()
})
it('should return error result on timeout', async () => {
const service = container.get<ITaskRepository>(cid.TaskService)
const result = await service.getMany()
expect(result.isErr()).toBeTruthy()
})
})
})

As you can see, the tests initializes the service and checks if the results are ok or errored. Remember what I said about using a result class, this class contains the results or the error, in case something goes wrong.

Result Class

Luckily there is a result class already implemented in typescript in the library neverthrow. This class has ok and err functions to make a Result. For example:

import { ok } from 'neverthrow'const myResult = ok({ myData: 'test' }) // instance of `Ok`myResult.isOk() // true
myResult.isErr() // false

Inversify-Props

The code

TaskService

import { Result, combine } from 'neverthrow'
import { ITaskData, ITaskRepository } from '../domain/Task.types'
import { TaskParser } from './TaskParser'
import { ITaskParser, ITaskResponse } from './TaskService.types'
import { inject } from 'inversify-props'
import { HttpError, IHttpService } from '@@/src/app/shared/http'
import { ParseError } from '@@/src/app/shared/error/ParseError'
export class TaskService implements ITaskRepository {
private parser: ITaskParser
private httpService: IHttpService
constructor(@inject() httpService: IHttpService) {
this.parser = new TaskParser()
this.httpService = httpService
this.httpService.initService('https://my-url')
}
create(): Promise<Result<ITaskData, ParseError | HttpError>> {
throw new Error('Method not implemented.')
}
remove(): Promise<Result<ITaskData, ParseError | HttpError>> {
throw new Error('Method not implemented.')
}
edit(): Promise<Result<ITaskData, ParseError | HttpError>> {
throw new Error('Method not implemented.')
}
getOne(): Promise<Result<ITaskData, ParseError | HttpError>> {
throw new Error('Method not implemented.')
}
async getMany(): Promise<Result<ITaskData[], ParseError | HttpError>> {
const parseTo = (taskResponse: ITaskResponse) => {
const tasks = taskResponse.items.map(this.parser.toDomain)
return combine(tasks)
}
return await this.httpService.get<ITaskResponse, ITaskData[]>(
{ url: '/tasks' },
{ parseTo }
)
}
}

Our TaskService class follows the diagram in the previous section. Only one method from the interface is implemented: getMany().

TaskParser

export class TaskParser implements ITaskParser {
toDomain(data: HttpTask): Result<ITask, ParseError> {
try {
const taskData: ITaskData = {
title: data.title,
description: data.description,
state: data.state as TaskState,
schedule: data.schedule ? new Date(data.schedule) : null,
due: data.due ? new Date(data.due) : null,
}
const task = new Task(taskData)
return ok(task)
} catch (error) {
return err(new ParseError(error.message))
}
}
}

HttpService

export class HttpService implements IHttpService {
private axiosService: AxiosInstance
private axiosCreator: AxiosCreator
constructor(axiosCreator: AxiosCreator) {
this.axiosCreator = axiosCreator
this.axiosService = axios.create()
}
initService(baseUrl: string) {
this.axiosService = this.axiosCreator.createAxiosInstance(baseUrl)
this._initializeRequestInterceptor()
this._initializeResponseInterceptor()
}
public async get<T, M>(
{ url, config }: IHttpRequest,
parser: Parser<T, M>
): Promise<Result<M, ParseError | HttpError>> {
try {
const response = await this.axiosService.get<T>(url, config)
return this._parseFailable<T, M>(response.data, parser.parseTo)
} catch (error) {
return err(error)
}
}
private _parseFailable<T, M>(
data: T,
parser: FailableParser<T, M>
): Result<M, ParseError> {
try {
return parser(data)
} catch (error) {
const parseError = new ParseError(error.message)
return err(parseError)
}
}
}

Ok, we have our HttpService that uses a factory AxiosCreator to make an instance of AxiosInstance. We can initialize the HttpService and pass a factory that creates a mocked AxiosInstance for unit testing or a real instance for connecting to the API. By doing this we are actually making a very basic version of dependency injection. Where do we create or instantiate all this injectable dependencies ? It will depend on the application, usually, there is a “container” that creates all the injectable dependencies and the dependent classes refer to the container. This is exactly what Inversifyjs does for dependency injection.

Dependency Injection

For an even easier interface, we will use Inversify-Props which is a wrapper for Inversifyjs designed to be used with class property decorators like Nuxt-Property-Decorator.

In the HttpService we are injecting an AxiosCreator. We can add our AxiosCreator class to the container, and later specify a MockAxiosCreator that creates fake axios instances. That way, we can leave the HttpService and also our TaskService and their unit tests specification untouched, and just add a mock class to the container and everything else will work just the same.

Lets create the AxiosCreator class and inject it to the HttpService using Inversify-Props:

import { inject } from 'inversify-props'export interface IAxiosCreator {
createAxiosInstance(baseUrl: string): AxiosInstance
}
export class AxiosCreator implements IAxiosCreator {
createAxiosInstance(baseUrl: string): AxiosInstance {
return axios.create({
baseURL: baseUrl,
headers: { 'Content-Type': 'application/json' }
})
}
}
export class HttpService implements IHttpService {
private axiosService: AxiosInstance
private axiosCreator: AxiosCreator
constructor(@inject() axiosCreator: AxiosCreator) {
this.axiosCreator = axiosCreator
this.axiosService = axios.create()
}
initService(baseUrl: string) {
this.axiosService = this.axiosCreator.createAxiosInstance(baseUrl)
this._initializeRequestInterceptor()
this._initializeResponseInterceptor()
}
...}

In the same way we can now inject the HttpService to the TaskService:

import { inject } from 'inversify-props'
import { HttpError, IHttpService } from '@@/src/app/shared/http'
export class TaskService implements ITaskRepository {
private parser: ITaskParser
private httpService: IHttpService
constructor(@inject() httpService: IHttpService) {
this.parser = new TaskParser()
this.httpService = httpService
this.httpService.initService('https://my-url')
}
}

With the dependencies injected, the only remaining step is to create the container. There should be just one container in the application, usually in Vue, the container is created with the creation of the app. Here, with Nuxt, a good place is to create the container in a Plugin that runs before every other setup.

NOTE: With Nuxt, the store is initialized before plugins. I decided to not use the Nuxt store, and instead use a regular store initialized as a Plugin. We will cover store in the next post.

The dependency injection plugin is as follows:

// src/ui/plugins/inversify.ts
import 'reflect-metadata'
import { container } from 'inversify-props'
import { AxiosCreator, HttpService, IAxiosCreator, IHttpService } from '@@/src/app/shared/http'
import { ITaskRepository } from '@@/src/app/modules/task/domain'
import { TaskService } from '@@/src/app/modules/task/infrastructure'
export function containerBuilder() {
container.addTransient<IAxiosCreator>(AxiosCreator)
container.addTransient<IHttpService>(HttpService)
container.addTransient<ITaskRepository>(TaskService)
}
containerBuilder()

We are adding the AxiosCreator and HttpService dependencies to the container. When we call @inject it will create the objects from the container. The order in which we add the dependencies also matters.

Notice that we are exporting the containerBuilder function so we can create our container inside unit tests. The reflect-metadata is a restriction of the Inversify library, it is required to be imported only once.

Continue with the tests

Lets start by defining some fake data with the structure that the API will have.

const nextYear = new Date().setFullYear(new Date().getFullYear() + 1)
export const mockApiTask: HttpTask[] = [
{
title: 'Create Nuxt App',
description: 'I have to make a nuxt sample app',
state: 'DOING',
schedule: null,
due: null,
},
{
title: 'Drink water',
description: 'Always drink water',
state: 'TODO',
schedule: null,
due: nextYear.toLocaleString(),
},
]

The first task in our fake data has null dates, but the second task is set to one year foward, so that it is always a valid task.

Now continuing with the mock axios instance:

import axios, { AxiosInstance } from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { inject } from 'inversify-props';
import { IAxiosCreator } from '.';
import { HttpTask, ITaskResponse } from '../../modules/task/infrastructure';
export type TestActions = 'ok' | 'error' | 'timeout' | 'parseError' | 'clientError' | 'serverError'export class MockAxiosCreator implements IAxiosCreator {
mock!: MockAdapter
actionType: TestActions
constructor(@inject('ActionType') actionType: TestActions) {
this.actionType = actionType
}
createAxiosInstance(_baseUrl: string): AxiosInstance {
const instance = axios.create({ baseURL: _baseUrl });
this.mock = new MockAdapter(instance);
if (this.actionType === 'ok')
this.setMockActions()
if (this.actionType === 'error')
this.setErrorActions()
if (this.actionType === 'timeout')
this.setTimeoutActions()
return instance
}
setMockActions() {
this.mock.onGet('/tasks').reply((_config: any) => {
const response: ITaskResponse = {
total: 2,
page: 1,
hasNext: false,
hasPrev: false,
items: mockApiTask
}
return [200, JSON.stringify(response)]
})
}
setErrorActions() {
this.mock.onGet('/tasks').networkError()
}
setTimeoutActions() {
this.mock.onGet('/tasks').timeout()
}
}

We create a MockAxiosCreator class that will create an axios instance but wrapped in a MockAdapter class. We are also injecting a constant value actionType that will set the mock actions for the instance, in this case, we want to test a successful query that returns the fake data, and error actions for a network error and timeout.

But how do we use the mock instance in our tests?

Unit testing with mocks using inversify-props

// src/app/modules/task/infrastructure/__tests__/TaskService.spec.ts
import { cid, container, mockTransient, resetContainer } from "inversify-props"
import { containerBuilder } from "@@/src/ui/plugins/inversify"
import { IAxiosCreator } from "@@/src/app/shared/http"
import { ITaskRepository } from "../../domain"
beforeEach(() => {
resetContainer()
containerBuilder()
mockTransient<IAxiosCreator>(cid.AxiosCreator, MockAxiosCreator)
})
describe('TaskService', () => {
describe('getMany', () => {
it('should get tasks', async () => {
container.bind<TestActions>('ActionType').toConstantValue('ok')
const service = container.get<ITaskRepository>(cid.TaskService)
const result = await service.getMany()
expect(result.isOk()).toBeTruthy()
})
it('should return error result', async () => {
container.bind<TestActions>('ActionType').toConstantValue('error')
const service = container.get<ITaskRepository>(cid.TaskService)
const result = await service.getMany()
expect(result.isErr()).toBeTruthy()
})
it('should return error result on timeout', async () => {
container.bind<TestActions>('ActionType').toConstantValue('timeout')
const service = container.get<ITaskRepository>(cid.TaskService)
const result = await service.getMany()
expect(result.isErr()).toBeTruthy()
})
})
})

The last thing we add to the spec file is the constant value injected to the MockAxiosCreator. So in each test we bind a value to the ActionType attribute used by the MockAxiosCreator to return data from a successful query or throw an error.

If we execute the tests on this file now, we should get three of them passing.

PASS src/app/modules/task/infrastructure/__tests__/TaskService.spec.ts
TaskService
getMany
✓ should get tasks (8 ms)
✓ should return error result (2 ms)
✓ should return error result on timeout (2 ms)
---------------------------------|---------|----------|---------|---------|----------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
---------------------------------|---------|----------|---------|---------|----------------------
app/modules/task/infrastructure | 72.73 | 37.5 | 44.44 | 70.97 |
TaskParser.ts | 61.54 | 37.5 | 50 | 61.54 | 18-32
TaskService.ts | 76.47 | 100 | 42.86 | 73.33 | 19-28
app/shared/http | 87.96 | 30 | 62.5 | 87.62 |
HttpError.ts | 30.77 | 0 | 0 | 25 | 14-32,37
HttpService.ts | 86.67 | 50 | 90 | 85.71 | 37,78-79,103
HttpStatusCode.ts | 100 | 100 | 100 | 100 |

Conclusion

This was a longer and more complex post. There is a lot going on with the Result class of neverthrow, Dependency Injection with Inversify-Props and mocking Axios, so I suggest you go and look at the full code in the repository.

The next post will cover the Interactor class, which will implement the executor pattern, and Vuex store. We will also use dependency injection again to inject the interactor to the vuex module and create a TaskService mock for unit testing the store.