UP | HOME
Diego Rodriguez Mancini

Diego Rodriguez Mancini

Software Engineer

Implementing a Clean Architecture Modular Application in Nuxt/Vue Typescript Part 2: Services
Published on Feb 24, 2021 by Diego Rodriguez Mancini.

Introduction

NOTE: This is the second part on a series of posts. The first part covers all the necessary topics so I’m not going to talk too much about that stuff again.

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

task-service-class-diagram.png

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

The task service is the main class that performs the use cases. It should interact with an external service, our API, and parse the data to and from domain entities.

TaskParser

This class is in charge of converting entities to data transfer objects and vice versa. The data transfer object could be comming from the external API or from user input as a RequestObject.

HttpService

This class is in charge of requesting data from an external API. We will be using Axios to perform http requests. To make this service more generic, we will be using a factory called AxiosCreator that will provide axios instance objects that can be mocked using mock-axios-adapter.

AxiosCreator

This is the factory class that creates axios instances.

The tests first

For unit testing this, I’m going to consider the class diagram as a unit. So, this unit contains the TaskService, TaskParser and HttpService and the tests will include all these classes. The specification file is short, because we are only testing the getMany method of the TaskService class.

// 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

I suggest you read this article about the Result class. If you don’t want to read, the TLDR is that we will encapsulate the return data from every use case in a Result class. Instead of throwing errors, we will treat everything as a Result and perform error checks on this Result. We can also combine multiple results in a single Result class that returns a single error or the valid results.

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

What is this container object in the spec file? The container is an object provided by the library Inversify-Props, it contains the necessary dependencies that we can inject to our classes. In this case we are using container.get to get an instance of TaskService. This is the same as creating a new object but it has all dependencies that the service uses (provided they are already added to the container)

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

The TaskParser has a toDomain method that receives data from the API and converts it to a TaskEntity. If the creation of the Task entity is successful it returns an Ok result. If it fails returns an Err result, but not throw an error.

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

For the HttpService I’m going to cut the interface declarations and some private methods. You can go ahead and check the repository to see the full code. Also, most of the code was taken from another great nuxt + clean architecture repository.

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

Inversifyjs provides a container object that we can bind to our dependencies, like the AxiosCreator or the HttpService (even the TaskService will be injectable) and then inject those dependencies using a decorator, which will in other words get the objects from the container and instantiate them accordingly.

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

Our tests had a problem. They expected that the AxiosCreator created a working axios instance with a real API, but we don’t want to make http calls to the production API in our unittests, so unless we have a testing API, we need to create a mocked axios instance. And to do that, we’ll use mock-axios-adapter.

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

Inversify-Props has a mockTransient method to replace a class in the container with a mock, so we are replacing the AxiosCreator class with the MockAxiosCreator class. Now, when the HttpService calls for the AxiosCreator it will actually use the mocked one.

// 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

In this part of the series, we created a TaskService that uses an HttpService to fetch tasks from an API, parse them and return an Ok or Err result class which allows us for better error handling. We also succcessfully implemented dependency injection within our HttpService and TaskService, while also injecting a mock axios instance for unit testing.

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.