@aventii/runtime (1.0.2)
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-Bydé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 |