Plan

Problématique

  • Beaucoup de micro-service
  • Beaucoup d'API
  • Duplication de code
  • Bugs difficiles à corriger partout et évolutions difficiles
  • Peu scalable

Objectif

  • Uniformiser les microservices Node.js
  • Mutualiser les efforts de fix & évolution
    • Être compatible avec Kubernetes
    • Embarquer le must-have de tous micro-service
  • Faciliter la création de micro-services

Origines

n9-node-micro

  • Fichiers séparés (routes, controller)
  • Validation avec Joi
  • Sécurité avec Helmet
  • Trace des appels HTTP entrant avec Morgan
  • Scripts d'init (i.e. création d'index en BDD)
  • Proxy/Gateway/BFF possible
  • Gère le /routes pour les ACLs

Que fait n9-node-routing

  • Reprend ce que faisait n9-node-micro
  • Simplifie les déclarations de routes & contrôleur
  • Gère l'injection de dépendances
  • Intègre class-tranformer & class-validator pour les données en entrée
    • Fixe leur version
  • Fourni un loggeur et un gestionnaire de configuration (exposée en HTTP pour permettre le débug)
  • Monitoring & APM prêt à l'emploi (NewRelic & Sentry)
  • Swagger/OpenAPI dispo
  • Client HTTP
const log = global.log.module('contracts-init');
const db: Db = global.db;

module.exports = async () => {
	try {
		log.info('BEGIN Initialise contracts');
		await db.collection("contracts").createIndex({ idContract: 1 });
		log.info('END Initialise contracts');
	} catch (err) {
		log.error('error-contracts-init', err);
	}
};

import { Container } from '@neo9/n9-node-routing';
import { ContractsRepository } from './contracts.repository';

export default async (): Promise<void> => {
	await Container.get(ContractsRepository).initIndexes();
};

import { Inject, Service, N9Log } from "@neo9/n9-node-routing";
import { Configuration } from "../../conf/models/configuration.models";
import { ContractEntity, ContractListItem } from "../../models";

@Service()
export class ContractsRepository {
  constructor(private readonly conf: Configuration, private readonly logger: N9Log, @Inject("db") db: Db) {
    this._mongoClient = new N9MongoDBClient<ContractEntity, ContractListItem>(
      "contracts",
      ContractEntity,
      ContractListItem,
    );
  }
  public async initIndexes(): Promise<void> {
    await this._mongoClient.createIndex("idContract");
  }
}

contracts.init.ts

contracts.started.ts

\implies

contracts.service.ts

import * as ContractsService from './contracts.service';
const log = global.log;

async function createContract(req: Request, res: Response, next: NextFunction): Promise<void> {
	try {
		const contract: Contract = req.body;
		const session = req.session;

		const createdContract = await ContractsService.create(contract, session);
		...
		res.json(createdContract);;
	} catch (err) {
		log.error(err);
		next(new ExtendableError('contract-creation-error', 500));
	}
}

import { ContractsService } from './contracts.service';

@Service()
@JsonController('/contracts')
export class ContractsController {
  	constructor(
		private readonly logger: N9Log,
        private readonly contractsService: ContractsService
	) {}
	
  	@Post('/')
	@Acl([{ action: 'createProducts' }])
	public async createProduct(
		@Session() session: TokenContent,
		@Body() contract: ContractRequestCreate,
	): Promise<ProductEntity> {
        const createdContract = await ContractsService.create(contract, session);
		...
		return createdContract;
	}
}

contracts.controller.ts

contracts.controller.ts

\implies
import { createContract, insertContractInternal} from './contracts.controller';
import * as validation from './contracts.validation';
export default [
	{
		path: '/contracts',
		method: 'post',
		validate: validation.createContract,
		handler: [
			Sessions.sessionRequire,
			createContract
		],
		acl: { perms: [{ action: 'createContract' }] }
	},
    ...
  ];

import { ContractsService } from './contracts.service';

@Service()
@JsonController('/contracts')
export class ContractsController {
  	constructor(
		private readonly logger: N9Log,
        private readonly contractsService: ContractsService
	) {}
	
  	@Post('/')
	@Acl([{ action: 'createProducts' }])
	public async createProduct(
		@Session() session: TokenContent,
		@Body() contract: ContractRequestCreate,
	): Promise<ProductEntity> {
        const createdContract = await ContractsService.create(contract, session);
		...
		return createdContract;
	}
}

contracts.routes.ts

\implies

contracts.controller.ts

\implies
const contractObject = Joi.object().keys({
	therapyCode: Joi.string().required(),
	startDate: Joi.date().max(Joi.ref('endDate')).required(),
	endDate: Joi.date().allow(null).optional(),
});



export class ContractRequestCreate {
	@IsString()
	@IsNotEmpty()
	therapyCode: string;

	@IsDate()
	@isBefore('endDate')
	@Transform(DateParser.transform)
	public startDate: Date;

	@IsOptional()
	@IsDate()
	@Transform(DateParser.transform)
	public endDate?: Date;
}

contracts.validation.ts

contract-request-create.models.ts

Structure des fichiers

Métriques par défaut

Métriques par défaut : Event Loop & GC

Pourquoi pas Nest.js

  • Obligation de gérer des modules et de référencer les dépendances de chaque module explicitement
  • Beaucoup de développement custom à faire pour arriver au niveau de routing-controllers (parsing des QueryParams, gestion des stream en réponse, gestion des exceptions, ...)

Des Questions ?