DEV Community

Cover image for Axiosfit in a real project - Arango Of Thrones
David (ダビッド ) for YggdrasilTS

Posted on

Axiosfit in a real project - Arango Of Thrones

Original post

In a previous post, I told about the YggdrasilTS flagship, Axiosfit. Now, it turns to show a sample in a real project.

Although Axiosfit is yet in the early version, the last version has enough features to be used in a real project and start testing how it works. Let's begin...

The project we are going to build is a NestJS application that loads the Game Of Thrones data inside an ArangoDB database using services built with Axiosfit.

'INFO' This data is extracted from one of the ArangoDB examples datasets and if you want to practice a little bit with ArangoDB, here you can find the tutorial.

The first step is to create a NestJS application. To do so, you can follow its documentation and create the application using its client with the name arangodb-axiosfit, or whatever name you want. After this, you can open the project with your best editor or IDE and you will see something like the following image:

Like its documentation sais, you can execute npm start and you will see how it works.

Now, we are going to adapt the application adding the Axiosfit library and create all the needed resources.

To add Axiosfit, we only need to execute npm i @yggdrasilts/axiosfit. After this, we are going to see the Axiosfit dependency in our package.json file with the latest version published:

...
"dependencies": {
  "@nestjs/common": "^6.10.14",
  "@nestjs/core": "^6.10.14",
  "@nestjs/platform-express": "^6.10.14",
  "@yggdrasilts/axiosfit": "^0.8.0", <- Axiosfit dependency
  "reflect-metadata": "^0.1.13",
  "rimraf": "^3.0.0",
  "rxjs": "^6.5.4"
},
...
Enter fullscreen mode Exit fullscreen mode

Before continue, I would like to talk about how we are going to use ArangoDB. If you want, you can install it normally using its documentation but, for the purpose of this example, we are going to use the ArangoDB docker container using TestContainers library.

'INFO' What is TestContainers? This library has been thought to support tests, providing lightweight, throwaway instances of common databases or anything else that can run in a Docker container.

In this case, it is not for testing purpose but TestContainers is an easy way to manage docker containers through the code.

In their web, TestContainers shows how to be used with java but also it has its own nodejs library, testscontainers-node, that we will use in the project. To install it, we only need to execute the following command: npm i -D testcontainers.

'INFO' Docker is a requirement if you want to use TestContainers.

Once installed, we will create a setup.ts file inside test folder where we will instantiate ArangoDB docker image with TestContainers.

import { GenericContainer } from 'testcontainers';
import { StartedTestContainer } from 'testcontainers/dist/test-container';
import logger from 'testcontainers/dist/logger';

let mockServer: StartedTestContainer;

export const stopMockServer = async () => {
  await mockServer.stop();
  logger.info('Mock Server stopped.');
};

export const startMockServer = async (port: number = 8529): Promise<StartedTestContainer> => {
  let arangoDBserverIP: string;
  let arangoDBserverPort: string;

  try {
    const container = await new GenericContainer('arangodb', '3.6.0');

    mockServer = await container
      .withExposedPorts(port)
      .withEnv('ARANGO_NO_AUTH', '1')
      .start();

    arangoDBserverIP = await mockServer.getContainerIpAddress();
    arangoDBserverPort = String(await mockServer.getMappedPort(port));
    const url = `http://${arangoDBserverIP}:${arangoDBserverPort}`;
    logger.info(url);
    process.env.MOCK_SERVER_URL = url;

    return mockServer;
  } catch (error) {
    logger.error(error);
    await stopMockServer();
    throw new Error(error);
  }
};

export default async () => await startMockServer();
Enter fullscreen mode Exit fullscreen mode

Finally, we are going to modify our main.ts file to initialize the ArangoDB with our NestJS application.

import { NestFactory } from '@nestjs/core';
import { INestApplication, Logger } from '@nestjs/common';

import { startMockServer, stopMockServer } from '../test/setup';

import { AppModule } from './app.module';

let baseUrl = '';
const port = process.env.PORT || 3000;
let app: INestApplication;
const logger = new Logger('Bootstrap');

async function stopArangoDBServer() {
  logger.debug('Stopping ArangoDB instance.');
  await stopMockServer();
  logger.debug('ArangoDB instance stopped.');
  app.close();
}

async function startArangoDBServer() {
  try {
    logger.debug('Starting ArangoDB instance.');
    const arangodbPort: number = Number(process.env.ARANGODB_PORT) || 8529;
    const arangodbServer = await startMockServer();
    const arangoDBserverIP = await arangodbServer.getContainerIpAddress();
    const arangoDBserverPort = String(
      await arangodbServer.getMappedPort(arangodbPort),
    );
    baseUrl = `http://${arangoDBserverIP}:${arangoDBserverPort}`;
    process.env.BASE_URL = baseUrl;
    logger.log(`ArangoDB server running at ${baseUrl}`);
    logger.debug('ArangoDB instance started.');
  } catch (error) {
    logger.error(error);
    await stopArangoDBServer();
  }
}

async function bootstrap() {
  await startArangoDBServer();

  app = await NestFactory.create(AppModule);
  await app.listen(port);
  logger.log(
    `arangodb-axiosfit application running at http://localhost:${port}`,
  );
}

bootstrap();

process.on('SIGINT', stopArangoDBServer);
process.on('SIGQUIT', stopArangoDBServer);
process.on('SIGTERM', stopArangoDBServer);
Enter fullscreen mode Exit fullscreen mode

After all, you can start the application npm start and see the ArangoDB server running at your computer:

odin@asgard:~/issues $ npm start

[Nest] 1193   - 02/28/2020, 5:10:53 PM   [Bootstrap] Starting ArangoDB instance.
[Nest] 1193   - 02/28/2020, 5:11:00 PM   [Bootstrap] ArangoDB server running at http://localhost:42359
[Nest] 1193   - 02/28/2020, 5:11:00 PM   [Bootstrap] ArangoDB instance started.
[Nest] 1193   - 02/28/2020, 5:11:00 PM   [NestFactory] Starting Nest application... +75ms
[Nest] 1193   - 02/28/2020, 5:11:00 PM   [InstanceLoader] AppModule dependencies initialized +9ms
[Nest] 1193   - 02/28/2020, 5:11:00 PM   [RoutesResolver] AppController {/}: +4ms
[Nest] 1193   - 02/28/2020, 5:11:00 PM   [RouterExplorer] Mapped {/, GET} route +2ms
[Nest] 1193   - 02/28/2020, 5:11:00 PM   [NestApplication] Nest application successfully started +2ms
[Nest] 1193   - 02/28/2020, 5:11:00 PM   [Bootstrap] arangodb-axiosfit application running at http://localhost:3000
Enter fullscreen mode Exit fullscreen mode

'INFO' Check line [Bootstrap] ArangoDB server running at [URL] to access ArangoDB web interface.

The base project is done and able to continue creating, now, the Axiosfit services.

Thankfully to NestJS module system, we are going to create our own arangodb module and we will be able to reuse it in other projects if we want. To do so, we only need to create the module either manually or using the NestJS CLI with the following command: nest g module arangodb. After that, the project structure will be like this one:

ArangoDB has different REST API endpoints grouped by category. Due to this organization, we are going to create a class, arangodb.axiosfit.service.ts, to manage each category and each category will have its own Axiosfit service into the services folder.

'INFO' We are only going to create services to manage the endpoints that we need for this project.

To start doing this, we are going to create our Axiosfit services.

'INFO' I am going to show how to create the collections.axiosfit.service.ts and you can get the rest of services and entities from the repository.

Let's start creating the collections.axiosfit.service.ts file inside src/arangodb/services folder and adding the CollectionService class.

export class CollectionService {

}
Enter fullscreen mode Exit fullscreen mode

Inside this class, we are going to manage some of the ArangoDB REST API endpoints related to collections.

To enable the Axiosfit features for this class, we need to add the @HTTP main decorator that indicates the class will be an Axiosfit instance.

import { HTTP } from '@yggdrasilts/axiosfit';

@HTTP()
export class CollectionService {

}
Enter fullscreen mode Exit fullscreen mode

By default, this decorator enables Axiosfit instance to use Observables but, in this case, we are going to change this default configuration to use Promises instead. We are going to enable the Axiosfit logger as well.

import { HTTP } from '@yggdrasilts/axiosfit';

@HTTP({ usePromises: true, enableAxiosLogger: true })
export class CollectionService {

}
Enter fullscreen mode Exit fullscreen mode

Ok. We already have the Axiosfit base service to manage the collection requests. Now, we are going to add some methods needed to create collections and getting their information.

The endpoints to manage will be the following ones:

  • Create collection: POST /_db/{database-name}/_api/collection
  • Get collection information: GET /_db/{database-name}/_api/collection/{collection-name}

You can see that we are going to manage some variables in our requests:

  • Methods: We need to have a GET and POST.
  • Path: We need to manage the database and collection names.
  • Body: Also, we need to send body information.

To manage all of these things, Axiosfit give us specific decorators:

  • Methods: We are going to use @GET and @POST.
  • Path: @Path will help us to manage the database and collection names.
  • Body: And @Body decorator is able to manage the body data.

Finally, Axiosfit uses axios to manage the requests. For this reason, all the requests will respond with an AxiosResponse object where the data will be the answer to the endpoints.

Getting all together, the CollectionService looks like this:

import { AxiosResponse, HTTP, GET, POST, Path, Body } from '@yggdrasilts/axiosfit';

import { CollectionCreateOptions, CreateCollectionResponse, CollectionInfo } from '../entities';

@HTTP({ usePromises: true, enableAxiosLogger: true })
export class CollectionService {
  /**
   * Return information about a collection.
   *
   * @param {string} db The name of the database.
   * @param {string} collection The name of the collection.
   * @returns {Promise<AxiosResponse<CollectionInfo>>} Data {@link CollectionInfo}
   * @memberof CollectionService
   */
  @GET('/_db/:db/_api/collection/:collection')
  public getCollectionInfo(@Path('db') db: string, @Path('collection') collection: string): Promise<AxiosResponse<CollectionInfo>> {
    return null;
  }

  /**
   * Create collection.
   *
   * @param {string} db The name of the database.
   * @param {CollectionCreateOptions} body {@link CollectionCreateOptions}
   * @returns {Promise<AxiosResponse<CreateCollectionResponse>>} Data {@link CreateCollectionResponse}
   * @memberof CollectionService
   */
  @POST('/_db/:db/_api/collection')
  public createCollection(@Path('db') db: string, @Body() body: CollectionCreateOptions): Promise<AxiosResponse<CreateCollectionResponse>> {
    return null;
  }
}
Enter fullscreen mode Exit fullscreen mode

As you see, using Axiosfit the requests are organized and stored in one class, you can manage the path using variables and of course, you can send body data if the endpoint requires it.

The last thing that we need to do is to instantiate the Axiosfit service to be able to use it but before, and after the Axiosfit services creation, we need to modify our modules, arangodb.module.ts and app.module.ts, adapt our main.ts file and finally, instantiate our Axiosfit services giving them the ArangoDB URL inside our arangodb.axiosfit.service.ts.

To do so, we are going to use the Dynamic Modules feature provided by NestJS. Using this technique, we will be able to register our modules passing them the ArangoDB URL as a variable. Let's start modifying arangodb.module.ts file.

import { Module, DynamicModule } from '@nestjs/common';

import { ArangoDBService } from './arangodb.axiosfit.service';

@Module({})
export class ArangodbModule {
  static register(baseUrl: string): DynamicModule {
    return {
      module: ArangodbModule,
      providers: [
        {
          provide: 'BASE_URL',
          useValue: baseUrl,
        },
        ArangoDBService,
      ],
      exports: [ArangoDBService],
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

You can see that instead of passing the configurations inside the @Module decorator, we have created a static register method that needs a string variable, baseUrl. This will be the ArangoDB URL. Also, we have created a new provider BASE_URL that we will be used to build the ArangoDBService.

import { Injectable, Inject, Logger } from '@nestjs/common';

import { Axiosfit } from '@yggdrasilts/axiosfit';

import { AdminService, DatabaseService, CollectionService, BulkService } from './services';

@Injectable()
export class ArangoDBService {
  private readonly logger = new Logger(ArangoDBService.name);

  public adminService: AdminService;
  public databaseService: DatabaseService;
  public collectionService: CollectionService;
  public bulkService: BulkService;

  constructor(@Inject('BASE_URL') baseUrl: string) {
    this.logger.debug(`Initializing ArangoDBService services using ${baseUrl}...`);
    // Initialize ArangoDB services
    this.adminService = new Axiosfit<AdminService>().baseUrl(baseUrl).create(AdminService);
    this.adminService = new Axiosfit<AdminService>().baseUrl(baseUrl).create(AdminService);
    this.databaseService = new Axiosfit<DatabaseService>().baseUrl(baseUrl).create(DatabaseService);
    this.collectionService = new Axiosfit<CollectionService>().baseUrl(baseUrl).create(CollectionService);
    this.bulkService = new Axiosfit<BulkService>().baseUrl(baseUrl).create(BulkService);
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, it turns to the app.module.ts file.

import { Module, DynamicModule } from '@nestjs/common';

import { AppController } from './app.controller';
import { AppService } from './app.service';

import { ArangodbModule } from './arangodb/arangodb.module';

@Module({})
export class AppModule {
  static register(baseUrl: string): DynamicModule {
    return {
      module: AppModule,
      imports: [ArangodbModule.register(baseUrl)],
      controllers: [AppController],
      providers: [AppService],
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

We have used the same technique here to pass the baseUrl variable between modules. And finally, we need to adapt our main.ts where we have the ArangoDB URL.

...
async function bootstrap() {
  await startArangoDBServer();

  app = await NestFactory.create(AppModule.register(baseUrl));
  await app.listen(port);
  logger.log(
    `arangodb-axiosfit application running at http://localhost:${port}`,
  );
}
...
Enter fullscreen mode Exit fullscreen mode

After all these modifications, we can run the application again, npm start, and see how the application is running fine.

The last thing to do is use the Axiosfit services to load the Game Of Thrones data inside ArangoDB.

'INFO' You can download the resources from the repository or from the ArangoDB datasets.

To do so, we are going to add a new method inside our arangodb.axiosfit.service.ts called initGOTData.

import { Injectable, Inject, Logger } from '@nestjs/common';

import { Axiosfit } from '@yggdrasilts/axiosfit';

import * as Characters from '../../test/resources/GameOfThrones/Characters.json';
import * as Traits from '../../test/resources/GameOfThrones/Traits.json';
import * as ChildOf from '../../test/resources/GameOfThrones/ChildOf.json';
import * as Locations from '../../test/resources/GameOfThrones/Locations.json';

import { CollectionType, QueryType } from './entities';

import { AdminService, DatabaseService, CollectionService, BulkService } from './services';

@Injectable()
export class ArangoDBService {
  private readonly logger = new Logger(ArangoDBService.name);

  private readonly db = 'GameOfThrones';
  private readonly collections: { name: string; type: CollectionType; data: object[] }[] = [
    { name: 'Characters', type: CollectionType.DOCUMENT, data: Characters },
    { name: 'Traits', type: CollectionType.DOCUMENT, data: Traits },
    { name: 'ChildOf', type: CollectionType.EDGES, data: ChildOf },
    { name: 'Locations', type: CollectionType.DOCUMENT, data: Locations },
  ];

  public adminService: AdminService;
  public databaseService: DatabaseService;
  public collectionService: CollectionService;
  public bulkService: BulkService;

  constructor(@Inject('BASE_URL') baseUrl: string) {
    this.logger.debug(`Initializing ArangoDBService services using ${baseUrl}...`);
    // Initialize ArangoDB services
    this.adminService = new Axiosfit<AdminService>().baseUrl(baseUrl).create(AdminService);
    this.adminService = new Axiosfit<AdminService>().baseUrl(baseUrl).create(AdminService);
    this.databaseService = new Axiosfit<DatabaseService>().baseUrl(baseUrl).create(DatabaseService);
    this.collectionService = new Axiosfit<CollectionService>().baseUrl(baseUrl).create(CollectionService);
    this.bulkService = new Axiosfit<BulkService>().baseUrl(baseUrl).create(BulkService);
  }

  async initGOTData() {
    const databaseResponse = await this.databaseService.createDatabase({ name: this.db });
    if (databaseResponse.data.error) {
      throw new Error(`Problems creating database '${this.db}'. ${databaseResponse.data.errorMessage}`);
    }
    this.logger.debug(`Database ${this.db} created.`);

    for (const collection of this.collections) {
      const createCollectionResponse = await this.collectionService.createCollection(this.db, {
        name: collection.name,
        type: collection.type,
      });
      if (createCollectionResponse.data.error) {
        throw new Error(`Error creating collection '${collection.name}'.`);
      }
      const collectionInfo = await this.collectionService.getCollectionInfo(this.db, collection.name);
      if (collectionInfo.data.error) {
        throw new Error(`Error checking collection '${collection.name}'.`);
      }
      const bulkImportResponse = await this.bulkService.importJson(
        this.db,
        { type: QueryType.LIST, collection: collection.name },
        collection.data,
      );
      if (bulkImportResponse.data.error) {
        throw new Error(`Error importing data for collection '${collection.name}'.`);
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

'INFO' This code is based in the import.js file from the ArangoDB datasets.

Finally, we create our bootstrap endpoint in our app.controller.ts to call this initialization.

import { Controller, Get, Logger } from '@nestjs/common';

import { AppService } from './app.service';
import { ArangoDBService } from './arangodb/arangodb.axiosfit.service';

@Controller()
export class AppController {
  private readonly logger = new Logger(AppController.name);

  constructor(private readonly appService: AppService, private readonly arangodbService: ArangoDBService) {}

  @Get('bootstrap')
  async initArangoDB(): Promise<string> {
    this.logger.debug('Initializing ArangoDB with GOT data...');
    await this.arangodbService.initGOTData();
    return 'Game Of Thrones data added to ArangoDB server.';
  }

  @Get()
  async getHello(): Promise<string> {
    this.logger.debug(JSON.stringify((await this.arangodbService.adminService.getStatus()).data));
    return this.appService.getHello();
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, we can start our application, npm start, go to http://localhost:3000/bootstrap and, if there are no errors, we will see the following message in the browser: Game Of Thrones data added to ArangoDB server. and if we go to the terminal, we can see the Axiosfit logs:

Also, if we go to ArangoDB web UI, [ARANGODB_SERVER_URL]/_db/GameOfThrones/_admin/aardvark/index.html#collections, we will be able to see the collections and edges imported.

'INFO' ARANGODB_SERVER_URL can be found in the logs in the line [Bootstarp] ArangoDB server running at...

Conclusion

In this post we have seen how using Axiosfit we have been able to create organized services to call different ArangoDB REST endpoints to load or get data. Axiosfit give us specific decorators facilitating the request management and storage.

You can see more examples inside the Axiosfit samples and also reading its documentation.

If you have enjoyed this post and Axiosfit library, I would really appreciate your feedback giving us Github stars, open Issues requesting new features, problems using the library and also your comments :-)

Enjoy!! 🌳

Latest comments (0)