@aventii/runtime (1.0.2)

Published 2026-08-21 10:59:26 +00:00 by Valentin

Installation

@aventii:registry=
npm install @aventii/runtime@1.0.2
"@aventii/runtime": "1.0.2"

About this package

@aventii/runtime

Runtime HTTP opinionated pour les microservices Node.js Aventii.

@aventii/runtime standardise la manière dont un microservice Aventii existe, démarre, écoute, expose son état et s'arrête, sans connaître son métier.

Le package crée Express et le serveur HTTP, applique les conventions HTTP communes et centralise le cycle de vie du processus :

création
   ↓
initialisation
   ↓
écoute HTTP
   ↓
service prêt
   ↓
SIGINT / SIGTERM / stop()
   ↓
arrêt HTTP
   ↓
fermeture des ressources
   ↓
service arrêté

Il ne configure ni base de données, ni authentification, ni communication inter-services : ces briques restent injectées par le microservice via des middlewares et des hooks.


Installation

npm install @aventii/runtime

Prérequis :

Node.js >= 22

Le package utilise Express 5.


1. Utilisation minimale

const { createService } = require('@aventii/runtime');

const service = createService({
	name: 'pmc-mail',
	port: 8444
});

service.app.get('/hello', (req, res) => {
	res.json({ hello: 'world' });
});

service.run();

Le runtime prend alors en charge :

  • la création de express();
  • la création du serveur HTTP;
  • X-Powered-By désactivé;
  • trust proxy;
  • les parsers JSON et URL-encoded;
  • le healthcheck standard;
  • le 404 final;
  • le handler Express 500;
  • SIGINT;
  • SIGTERM;
  • les erreurs fatales Node;
  • l'arrêt gracieux.

2. Exemple avec une base de données

const { createService } = require('@aventii/runtime');

const config = require('./infrastructure/config');
const { database } = require('./infrastructure/db');
const { router } = require('./routes');
const { closeMetrics } = require('./infrastructure/metrics');

const service = createService({
	name: 'pmc-di',
	port: config.DI_PORT,

	initialize: async () => {
		await database.initialize();
	},

	close: async () => {
		closeMetrics();
		await database.close();
	}
});

service.app.use('/di', router);

service.run();

Le runtime ne sait pas ce qu'est database.

Il sait seulement appeler :

await initialize();

avant l'écoute HTTP, puis :

await close();

pendant l'arrêt.


3. API publique

const {
	createService,
	RUNTIME_STATES,
	RuntimeError
} = require('@aventii/runtime');

4. createService(options)

Signature conceptuelle :

const service = createService({
	name,
	port,
	host,

	trustProxy,
	disablePoweredBy,

	body,
	health,

	initialize,
	close,

	shutdownTimeoutMs,

	handleSignals,
	handleFatalErrors,

	exitOnSignal,
	exitOnFatalError,

	logger,

	serverFactory
});

Retour :

{
	app,
	server,
	start,
	run,
	stop,
	getStatus
}

5. Options

name

Obligatoire.

name: 'pmc-pics'

Doit être une chaîne non vide.

Le nom est utilisé notamment dans les logs et le healthcheck.


port

Obligatoire.

port: 8446

Doit être un entier compris entre 1 et 65535.

Avec @aventii/config, l'usage normal est :

port: config.PICS_PORT

host

Par défaut :

host: '0.0.0.0'

Le runtime appelle :

server.listen(port, host);

trustProxy

Par défaut :

trustProxy: 1

Le runtime applique :

app.set('trust proxy', trustProxy);

Ce choix correspond au déploiement Aventii derrière un reverse proxy.

Il reste configurable :

createService({
	name: 'foo',
	port: 8080,
	trustProxy: false
});

disablePoweredBy

Par défaut :

disablePoweredBy: true

Le runtime applique :

app.disable('x-powered-by');

6. Body parsers

Par défaut, le runtime installe :

express.json({
	limit: '10mb'
});

express.urlencoded({
	extended: true,
	limit: '10mb'
});

Configuration explicite :

createService({
	name: 'pmc-pics',
	port: 8446,

	body: {
		json: true,
		jsonLimit: '20mb',

		urlencoded: true,
		urlencodedLimit: '20mb',
		urlencodedExtended: true
	}
});

Désactiver JSON

body: {
	json: false
}

Désactiver URL-encoded

body: {
	urlencoded: false
}

Désactiver tous les parsers

body: false

Cela peut être utile pour un service qui doit manipuler lui-même les streams bruts.


7. Ajouter middlewares et routes

Le runtime expose directement l'application Express :

service.app

L'usage reste donc celui d'Express normal :

service.app.use(internalContextMiddleware);
service.app.use('/pics', router);

ou :

service.app.get('/foo', handler);

Le package ne crée pas de DSL de routage Aventii.


8. Ordre d'installation

Lors de createService() :

Express
↓
conventions HTTP
↓
body parsers

Le microservice ajoute ensuite ses middlewares et routes :

middlewares métier
↓
routes métier

Lors de start() / run(), le runtime finalise l'application avec :

healthcheck
↓
404 final
↓
handler Express 500

Cette finalisation tardive permet d'ajouter librement les routes métier avant le démarrage.

Attention

Un middleware global ajouté avant start() s'applique également au healthcheck ajouté ensuite.

Exemple :

service.app.use(internalContextMiddleware);

service.run();

Le healthcheck sera donc derrière internalContextMiddleware.

C'est parfois souhaité, parfois non : l'ordre Express reste entièrement réel et explicite.

De même, éviter d'installer soi-même un catch-all 404 avant start(), car il pourrait intercepter le healthcheck du runtime.


9. Healthcheck standard

Par défaut, le runtime expose :

GET /health

Réponse typique :

{
	"status": "ok",
	"service": "pmc-pdf",
	"runtimeStatus": "ready",
	"timestamp": "2026-08-21T08:00:00.000Z",
	"uptime": "42.31s"
}

Le code HTTP est :

200 si healthy
503 sinon

10. Désactiver le healthcheck

health: false

11. Changer le chemin du healthcheck

health: {
	path: '/pics/health'
}

Le chemin doit commencer par /.


12. Healthcheck enrichi

Un service peut ajouter son propre contrôle technique :

const service = createService({
	name: 'pmc-pics',
	port: config.PICS_PORT,

	health: {
		path: '/pics/health',

		check: async () => {
			const etatDB = getDatabaseStatus();

			return {
				healthy: etatDB.connecte,
				etat: etatDB
			};
		}
	}
});

Le résultat sera intégré à la réponse :

{
	"status": "ok",
	"service": "pmc-pics",
	"runtimeStatus": "ready",
	"etat": {
		"connecte": true
	},
	"timestamp": "...",
	"uptime": "..."
}

13. Contrat de health.check

Le callback reçoit :

{
	req
}

Il peut être synchrone ou asynchrone.

Retour booléen

check: () => true

ou :

check: () => false

Retour objet

check: () => ({
	healthy: false,
	status: 'mongo_down',
	etat: getDatabaseStatus()
})

healthy contrôle le code HTTP.

status remplace le statut standard.

Les autres propriétés sont ajoutées à la réponse.

Exception

Si le check throw :

check: async () => {
	throw new Error('Mongo unavailable');
}

le runtime répond :

{
	"status": "check_failed",
	"service": "...",
	"runtimeStatus": "ready",
	"timestamp": "...",
	"uptime": "..."
}

avec un code :

503

et journalise l'erreur côté serveur.


14. initialize()

Hook optionnel exécuté avant l'écoute HTTP.

initialize: async () => {
	await database.initialize();
}

L'ordre est :

state = starting
↓
finalisation Express
↓
await initialize()
↓
installation des handlers process
↓
server.listen()
↓
state = ready

Le service n'accepte donc aucune requête avant la fin de l'initialisation.


15. Échec de démarrage

Si initialize() ou listen() échoue :

state = failed
↓
suppression des handlers process
↓
tentative de close()
↓
propagation de l'erreur

Avec :

await service.start();

l'erreur est propagée au consommateur.

Avec :

service.run();

le runtime journalise l'échec puis termine le processus avec :

exit code 1

16. start()

await service.start();

start() convient lorsque le code appelant veut conserver la maîtrise de l'erreur de démarrage.

Exemple :

try {
	await service.start();
} catch (err) {
	// gestion personnalisée
}

Le service ne peut être démarré qu'une fois.

Un second appel provoque une RuntimeError avec :

code = INVALID_START_STATE

17. run()

service.run();

run() est prévu pour les points d'entrée simples.

Il appelle start() puis, en cas d'échec :

[runtime] <service> startup failed:

et :

process.exit(1);

Pour la majorité des microservices Aventii, run() est l'API recommandée au point d'entrée.


18. stop(reason?)

Arrêt manuel :

await service.stop();

ou :

await service.stop('maintenance');

Ordre d'arrêt :

state = shutting_down
↓
server.close()
↓
await close()
↓
state = stopped

Les logs ressemblent à :

[runtime] pmc-pdf stopping (SIGTERM)...
[runtime] pmc-pdf stopped.

19. Arrêts concurrents

Plusieurs appels simultanés :

service.stop();
service.stop();
service.stop();

partagent la même opération d'arrêt.

Le hook close() n'est exécuté qu'une seule fois.


20. close()

Hook optionnel pour fermer les ressources du microservice :

close: async () => {
	closeMetrics();
	await database.close();
}

Le runtime ne connaît pas les ressources concernées.

Cela peut être :

  • MongoDB;
  • Mongoose;
  • métriques;
  • timers;
  • consommateurs;
  • clients externes;
  • files d'attente;
  • ressources métier.

21. Timeout d'arrêt

Par défaut :

shutdownTimeoutMs: 15000

Soit 15 secondes.

Si l'arrêt gracieux dépasse cette durée, le runtime lève une RuntimeError :

code = SHUTDOWN_TIMEOUT

et tente :

server.closeAllConnections?.();

Le state devient :

failed

Configuration :

shutdownTimeoutMs: 5000

22. SIGINT et SIGTERM

Par défaut :

handleSignals: true

Le runtime installe des handlers pour :

SIGINT
SIGTERM

Lors d'un signal :

signal
↓
stop(signal)
↓
server.close()
↓
close()
↓
process.exit(0)

Si l'arrêt échoue :

process.exit(1)

Ce comportement est particulièrement adapté à Docker, qui utilise SIGTERM lors de l'arrêt normal d'un conteneur.


23. Désactiver la gestion des signaux

Utile notamment dans certains tests ou lorsqu'un orchestrateur externe possède déjà cette responsabilité :

handleSignals: false

24. exitOnSignal

Par défaut :

exitOnSignal: true

Pour laisser le process vivant après traitement du signal :

exitOnSignal: false

Le runtime exécutera toujours stop(), mais n'appellera pas process.exit().


25. Erreurs fatales Node

Par défaut :

handleFatalErrors: true

Le runtime écoute :

uncaughtException
unhandledRejection

Une erreur fatale déclenche :

log erreur fatale
↓
stop(source)
↓
arrêt gracieux
↓
process.exit(1)

Les rejets non Error sont convertis en Error.


26. Désactiver la gestion des erreurs fatales

handleFatalErrors: false

Cela peut être utile dans un runner de tests ou si le processus possède déjà une politique globale.


27. exitOnFatalError

Par défaut :

exitOnFatalError: true

Pour empêcher le runtime d'appeler process.exit(1) après le cleanup :

exitOnFatalError: false

28. États du runtime

const { RUNTIME_STATES } = require('@aventii/runtime');

Valeurs :

RUNTIME_STATES.CREATED
// 'created'

RUNTIME_STATES.STARTING
// 'starting'

RUNTIME_STATES.READY
// 'ready'

RUNTIME_STATES.SHUTTING_DOWN
// 'shutting_down'

RUNTIME_STATES.STOPPED
// 'stopped'

RUNTIME_STATES.FAILED
// 'failed'

29. getStatus()

service.getStatus();

Exemple :

{
	name: 'pmc-pdf',
	state: 'ready',
	port: 8448,
	host: '0.0.0.0',
	listening: true,
	startedAt: '2026-08-21T08:00:00.000Z',
	stoppedAt: null,
	uptimeSeconds: 42.317
}

L'objet retourné ne donne pas accès à des références mutables internes.


30. 404 standard

Lorsque aucune route ne correspond :

{
	"error": "Route inconnue : /foo"
}

avec :

HTTP 404

Le handler est installé au moment de start().


31. Handler Express 500

Une erreur non gérée dans la chaîne Express est journalisée :

[runtime] pmc-pdf unhandled request error:

Le client reçoit uniquement :

{
	"error": "Internal server error"
}

avec :

HTTP 500

Les détails internes de l'erreur ne sont pas renvoyés au client.

Si les headers sont déjà envoyés, le runtime délègue à Express via :

next(err);

32. Logger

Par défaut :

logger: console

Le logger doit être compatible avec les méthodes utilisées :

log
error

Les appels sont optionnels :

logger?.log?.(...)
logger?.error?.(...)

Pour désactiver les logs du runtime :

logger: null

Tous les messages générés par le package sont préfixés :

[runtime]

33. Serveur HTTP exposé immédiatement

Le serveur est créé dès l'appel à :

createService()

Donc :

const service = createService(...);

service.server

est disponible avant start().

Cela permet d'intégrer des systèmes qui doivent s'attacher au serveur HTTP, notamment Socket.IO.


34. Exemple Socket.IO / Access

const { Server } = require('socket.io');
const { createService } = require('@aventii/runtime');

const service = createService({
	name: 'access',
	port: config.ACCESS_PORT,

	initialize: async () => {
		await database.initialize();
	},

	close: async () => {
		await database.close();
	}
});

const io = new Server(service.server, {
	cors: {
		origin: config.CORS_ORIGINS
	}
});

service.app.use('/api', routes);

service.run();

Le runtime conserve la maîtrise de :

listen()
close()
SIGINT
SIGTERM

tout en laissant Access attacher Socket.IO au serveur avant son démarrage.


35. serverFactory

Par défaut :

serverFactory: app => http.createServer(app)

Il est possible d'injecter une autre fabrique :

createService({
	name: 'foo',
	port: 8080,

	serverFactory: app => {
		return http.createServer(app);
	}
});

Le serveur retourné doit être compatible avec Node et fournir au minimum :

server.listen(...)
server.close(...)

Cette option facilite également les tests et les besoins HTTP particuliers.


36. RuntimeError

const { RuntimeError } = require('@aventii/runtime');

Structure :

new RuntimeError(message, {
	code,
	cause
});

Propriétés :

err.name
// 'RuntimeError'

err.code
// ex. 'SHUTDOWN_TIMEOUT'

err.cause

Le message est automatiquement préfixé :

[runtime]

Codes actuellement utilisés notamment :

RUNTIME_ERROR
INVALID_START_STATE
SHUTDOWN_TIMEOUT

37. Exemple complet : PICS

Avant Runtime, un point d'entrée classique doit gérer lui-même :

express()
trust proxy
x-powered-by
body parsers
health
404
SIGINT
SIGTERM
verrou d'arrêt
database.initialize()
database.close()
listen()
startup errors

Avec Runtime :

const { createService } = require('@aventii/runtime');

const config = require('./infrastructure/config');

const {
	database,
	getUserCollection,
	getDatabaseStatus
} = require('./infrastructure/db');

const {
	internalContextMiddleware
} = require('./infrastructure/link');

const {
	closeMetrics
} = require('./infrastructure/metrics');

const {
	router
} = require('./routes/picsRoutes');

const service = createService({
	name: 'pmc-pics',
	port: config.PICS_PORT,

	health: {
		path: '/pics/health',

		check: () => {
			const etatDB = getDatabaseStatus();

			if (!etatDB.connecte) {
				return {
					healthy: false,
					status: 'mongo_down',
					etat: etatDB
				};
			}

			try {
				getUserCollection();

				return {
					healthy: true,
					etat: etatDB
				};

			} catch (err) {
				return {
					healthy: false,
					status: 'mongo_error',
					message: err.message,
					etat: etatDB
				};
			}
		}
	},

	initialize: async () => {
		await database.initialize();
	},

	close: async () => {
		closeMetrics();
		await database.close();
	}
});

service.app.use(internalContextMiddleware);
service.app.use('/pics', router);

service.run();

38. Intégration avec les autres packages Aventii

@aventii/runtime ne dépend directement d'aucun autre package Aventii.

Architecture typique :

@aventii/config
      ↓
configuration typée
      ↓
@aventii/runtime
      │
      ├── initialize() ──► @aventii/db
      │
      ├── app.use() ─────► @aventii/link
      │
      ├── app.use() ─────► @aventii/security
      │
      └── close() ───────► @aventii/metrics / @aventii/db

Chaque package reste indépendant.


39. Ce que Runtime ne fait pas

@aventii/runtime ne doit pas :

  • lire process.env;
  • créer la configuration;
  • connaître MongoDB ou Mongoose;
  • connaître pmc-keys;
  • créer les clients @aventii/link;
  • authentifier les utilisateurs;
  • connaître Rapidoo, Gomada ou un autre produit;
  • définir les routes métier;
  • définir les modèles;
  • gérer Docker;
  • imposer une logique métier de healthcheck;
  • cacher Express derrière une DSL propriétaire.

Sa responsabilité est de standardiser l'enveloppe HTTP et le cycle de vie d'un microservice Aventii.


40. Defaults

L'appel :

createService({
	name,
	port
});

utilise notamment les valeurs suivantes :

{
	host: '0.0.0.0',

	trustProxy: 1,
	disablePoweredBy: true,

	body: {
		json: true,
		jsonLimit: '10mb',

		urlencoded: true,
		urlencodedLimit: '10mb',
		urlencodedExtended: true
	},

	health: {
		path: '/health'
	},

	shutdownTimeoutMs: 15000,

	handleSignals: true,
	handleFatalErrors: true,

	exitOnSignal: true,
	exitOnFatalError: true,

	logger: console,

	serverFactory: app => http.createServer(app)
}

41. Workflow recommandé d'un microservice

const { createService } = require('@aventii/runtime');

const config = require('./infrastructure/config');

const service = createService({
	name: 'pmc-example',
	port: config.EXAMPLE_PORT,

	initialize: async () => {
		// initialiser les ressources
	},

	close: async () => {
		// fermer les ressources
	}
});

// Middlewares
service.app.use(...);

// Routes
service.app.use('/example', routes);

// Démarrage
service.run();

Le point d'entrée reste du JavaScript Express parfaitement ordinaire, mais toute la plomberie répétitive disparaît.


Résumé

@aventii/runtime transforme ceci :

chaque microservice réimplémente
Express
+
configuration HTTP
+
health
+
listen
+
startup
+
SIGINT
+
SIGTERM
+
shutdown
+
fatal errors

en :

createService()
      │
      ├── service.app
      ├── service.server
      ├── initialize()
      ├── health
      ├── start() / run()
      └── close() / stop()

Le principe directeur est :

Runtime sait comment un microservice Aventii vit. Il ne sait pas ce que ce microservice fait.

Dependencies

Dependencies

ID Version
cors ^2.8.6
dotenv ^17.4.2
express ^5.2.1
Details
npm
2026-08-21 10:59:26 +00:00
117
UNLICENSED
11 KiB
Assets (1)
Versions (5) View all
1.0.4 2026-08-25
1.0.3 2026-08-21
1.0.2 2026-08-21
1.0.1 2026-08-21
1.0.0 2026-08-21