@aventii/runtime (1.0.4)

Published 2026-08-25 11:06:48 +00:00 by Valentin

Installation

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

About this package

@aventii/runtime

Runtime HTTP opinionated pour les microservices Node.js Aventii. @aventii/runtime standardise l'enveloppe HTTP et le cycle de vie d'un microservice : création d'Express, serveur HTTP, conventions communes, parsing des bodies et cookies, CORS optionnel, healthcheck, démarrage, arrêt gracieux et gestion des erreurs fatales.

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

Installation

npm install @aventii/runtime

Prérequis : Node.js >= 22. Le package utilise Express 5 et parse les cookies par défaut avec cookie-parser.

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 notamment en charge :

  • express() et le serveur HTTP ;
  • trust proxy et X-Powered-By ;
  • les body parsers ;
  • le parsing des cookies dans req.cookies ;
  • CORS lorsqu'il est configuré ;
  • le healthcheck ;
  • le 404 et le handler 500 ;
  • SIGINT / SIGTERM ;
  • les erreurs fatales Node ;
  • le démarrage et l'arrêt gracieux.

API publique

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

createService(options) retourne :

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

app et server existent immédiatement après createService(), avant même le démarrage.

Architecture recommandée

Pour les microservices simples, la configuration du runtime reste dans infrastructure/runtime.js :

// infrastructure/runtime.js
const { createService } = require('@aventii/runtime');
const config = require('./config');
const { database, getDatabaseStatus } = require('./db');
const service = createService({
    name: 'pmc-example',
    port: config.EXAMPLE_PORT,
    health: {
        path: '/example/health',
        check: () => {
            const etatDB = getDatabaseStatus();
            return {
                healthy: etatDB.connecte,
                status: etatDB.connecte ? 'ok' : 'mongo_down',
                etat: etatDB
            };
        }
    },
    initialize: async () => {
        await database.initialize();
    },
    close: async () => {
        await database.close();
    }
});
module.exports = service;

Le point d'entrée reste alors minimal :

// index.js
const service = require('./infrastructure/runtime');
const { internalContextMiddleware } = require('./infrastructure/link');
service.app.use(internalContextMiddleware);
service.app.use('/example', require('./routes'));
service.run();

Le principe retenu est que la consommation et la configuration des packages @aventii/* restent autant que possible dans infrastructure/, tandis que index.js décrit essentiellement l'assemblage métier.

initialize et setup

initialize prépare les ressources avant l'ouverture HTTP :

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

setup est exécuté après initialize et avant l'écoute HTTP. Il est utile lorsque les routes dépendent de ressources ou modèles initialisés.

function createEntrepriseRuntime(setup) {
    return createService({
        name: 'pmc-entreprise',
        port: config.ENTREPRISE_PORT,
        initialize: async () => {
            await database.initialize();
            initModels();
        },
        setup,
        close: async () => {
            await database.close();
        }
    });
}

Puis :

const service = createEntrepriseRuntime(({ app }) => {
    const { User, Entreprise } = models;
    app.use('/entreprise', require('./modules/User/UserRoutes')({ User }));
    app.use('/entreprise', require('./modules/Entreprise/EntrepriseRoutes')({ Entreprise }));
});
service.run();

Le callback setup reçoit { app, server } et peut être synchrone ou asynchrone.

Ordre de démarrage

createService()
    ↓
start() / run()
    ↓
initialize()
    ↓
setup({ app, server })
    ↓
finalisation Express
    ↓
listen()
    ↓
READY

Le service n'accepte aucune requête avant la fin de initialize et setup.

Priorité du healthcheck

Le healthcheck du runtime est installé de façon à rester prioritaire sur les routes dynamiques ajoutées par setup. Ainsi :

health: {
    path: '/entreprise/health'
}

ne doit pas être capturé par :

router.get('/:entrepriseId', handler);

GET /entreprise/health reste donc le healthcheck et non une recherche de l'entreprise health. Les autres middlewares ajoutés explicitement par le consommateur conservent la sémantique et l'ordre normaux d'Express.

Options principales

name

Obligatoire, chaîne non vide :

name: 'pmc-pics'

port

Obligatoire, entier de 1 à 65535 :

port: config.PICS_PORT

host

Défaut :

host: '0.0.0.0'

trustProxy

Défaut :

trustProxy: 1

Équivalent à app.set('trust proxy', 1). La valeur reste configurable.

disablePoweredBy

Défaut :

disablePoweredBy: true

Le runtime exécute alors app.disable('x-powered-by').

Cookies

Par défaut, le runtime installe cookie-parser :

cookies: true

Toute route ou tout middleware ajouté au service peut donc lire directement :

req.cookies.token
req.cookies.refresh_token

Aucun app.use(cookieParser()) supplémentaire n'est nécessaire dans le microservice.

Pour désactiver explicitement ce comportement :

const service = createService({
	name: 'stateless-service',
	port: 8080,
	cookies: false
});

Le parsing des cookies est indépendant de la configuration body. Ainsi, body: false désactive les parsers JSON/urlencoded du runtime mais conserve req.cookies par défaut. Cela permet notamment à un service ayant besoin d'un body brut pour vérifier une signature de continuer à utiliser une authentification par cookie.

Le runtime ne définit aucune politique de cookie : il ne choisit ni nom, ni domaine, ni httpOnly, ni secure, ni sameSite, ni durée. Il se contente de parser l'en-tête Cookie; la création et les attributs des cookies restent au package ou au microservice concerné.

Body parsers

Par défaut :

body: {
    json: true,
    jsonLimit: '10mb',
    urlencoded: true,
    urlencodedLimit: '10mb',
    urlencodedExtended: true
}

Exemple :

body: {
    json: true,
    jsonLimit: '20mb',
    urlencoded: false
}

Pour tout désactiver :

body: false

C'est notamment utile lorsqu'un microservice doit installer lui-même son parser pour conserver un body brut :

const service = createService({
    name: 'access',
    port: config.ACCESS_PORT,
    body: false
});
service.app.use(express.json({
    limit: '100kb',
    verify: (req, res, buffer) => {
        if (req.originalUrl.startsWith('/webhooks/notchpay')) {
            req.rawBody = buffer.toString('utf8');
        }
    }
}));

CORS

CORS est optionnel. Sans configuration, le runtime ne l'active pas.

cors: false

Lorsqu'il est activé, cors doit être un objet contenant au minimum origin :

cors: {
    origin: 'https://example.com',
    credentials: true
}

Une fonction est également acceptée :

cors: {
    credentials: true,
    origin: (origin, callback) => {
        if (!origin) return callback(null, true);
        if (config.CORS_ORIGINS.includes(origin)) return callback(null, true);
        return callback(new Error(`Origin refusée: ${origin}`));
    }
}

Le runtime fournit la mécanique CORS ; la politique des origines reste au microservice.

Healthcheck

Par défaut :

GET /health

Chemin personnalisé :

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

Désactivation :

health: false

Healthcheck enrichi

health: {
    path: '/pics/health',
    check: () => {
        const etatDB = getDatabaseStatus();
        if (!etatDB.connecte) {
            return {
                healthy: false,
                status: 'mongo_down',
                message: 'Base de donnees non connectee',
                etat: etatDB
            };
        }
        return {
            healthy: true,
            message: 'Connexion Mongo active',
            etat: etatDB
        };
    }
}

check peut être synchrone ou asynchrone et reçoit { req }. Il peut retourner un booléen ou un objet. Dans un objet, healthy contrôle le code HTTP (200 ou 503), status peut remplacer le statut standard et les autres propriétés sont ajoutées à la réponse. Si check lève une exception, le runtime répond en 503 avec status: 'check_failed' et journalise l'erreur.

start() et run()

start()

await service.start();

À utiliser lorsque l'appelant veut gérer lui-même une erreur de démarrage :

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

Un second démarrage produit une RuntimeError de code INVALID_START_STATE.

run()

service.run();

API recommandée pour les points d'entrée simples. run() appelle start() et, en cas d'échec, journalise l'erreur puis termine le processus avec le code 1.

stop() et close()

Arrêt manuel :

await service.stop('maintenance');

Cycle :

READY
  ↓
SHUTTING_DOWN
  ↓
server.close()
  ↓
close()
  ↓
STOPPED

Plusieurs stop() concurrents partagent la même opération : close() n'est exécuté qu'une fois. close est le hook de libération des ressources :

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

Il peut fermer une DB, des métriques, timers, pollers, clients externes ou toute autre ressource appartenant au microservice.

Timeout d'arrêt

Défaut :

shutdownTimeoutMs: 15000

Si l'arrêt dépasse ce délai, une RuntimeError de code SHUTDOWN_TIMEOUT est produite, les connexions restantes sont fermées lorsque le serveur le permet et l'état passe à failed.

Signaux et erreurs fatales

Par défaut :

handleSignals: true
exitOnSignal: true
handleFatalErrors: true
exitOnFatalError: true

Le runtime gère SIGINT et SIGTERM. Il appelle stop(signal), puis termine normalement avec le code 0, ou 1 si l'arrêt échoue. Il gère également uncaughtException et unhandledRejection : arrêt gracieux puis code 1. Pour les tests ou une gestion externe :

handleSignals: false,
handleFatalErrors: false

exitOnSignal: false et exitOnFatalError: false permettent d'exécuter le cleanup sans appeler ensuite process.exit().

États et statut

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'

RUNTIME_STATES est figé.

service.getStatus();

Retour typique :

{
    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
}

404 et erreurs Express

Le runtime installe le fallback 404 final :

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

Une erreur Express non gérée est journalisée côté serveur. Le client reçoit seulement :

{
    "error": "Internal server error"
}

avec HTTP 500. Les détails internes ne sont pas exposés.

Logger

Défaut :

logger: console

Le logger fournit principalement log et error. Pour désactiver les logs :

logger: null

Les appels sont optionnels et les messages du package sont préfixés par [runtime].

Socket.IO et serveur exposé

service.server existe avant start() :

const service = createService({
    name: 'access',
    port: config.ACCESS_PORT
});
initSockets(service.server);
service.run();

Le runtime conserve la responsabilité du listen(), des signaux et de l'arrêt HTTP.

serverFactory

Défaut conceptuel :

serverFactory: app => http.createServer(app)

Une fabrique personnalisée peut être injectée. Le serveur retourné doit être compatible Node et fournir au minimum listen() et close().

Defaults principaux

host: 0.0.0.0, trustProxy: 1, disablePoweredBy: true, body JSON/URL-encoded à 10mb, health /health, timeout de shutdown 15000ms, gestion des signaux et erreurs fatales activée, logger: console. CORS reste désactivé tant qu’aucune configuration cors n’est fournie.

Articulation avec @aventii/config

@aventii/config
      ↓
configuration validée et typée
      ↓
infrastructure/runtime.js
      ↓
@aventii/runtime
      ↓
Express + serveur + lifecycle

Le runtime reçoit normalement des valeurs déjà validées :

const config = require('./config');
const service = createService({
    name: 'pmc-pdf',
    port: config.PDF_PORT
});

Ce que Runtime ne fait pas

@aventii/runtime ne doit pas :

  • lire process.env ;
  • construire la configuration ;
  • connaître MongoDB ou Mongoose ;
  • connaître pmc-keys ;
  • créer les clients de communication inter-services ;
  • authentifier les utilisateurs ;
  • définir les routes ou modèles métier ;
  • connaître Rapidoo, Gomada ou un autre produit ;
  • gérer Docker ;
  • décider de la politique métier d'un healthcheck ;
  • cacher Express derrière une DSL propriétaire. Ces responsabilités restent dans le microservice ou dans les autres packages Aventii.

Résumé

Sans runtime, chaque microservice réimplémente une partie de :

Express
+ conventions HTTP
+ CORS éventuel
+ body parsers
+ health
+ listen
+ initialize / setup
+ SIGINT / SIGTERM
+ shutdown
+ fatal errors
+ 404 / 500

Avec @aventii/runtime :

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

Le métier reste du JavaScript/Express ordinaire. Le package retire la plomberie répétitive et impose un cycle de vie commun aux microservices Aventii.

Dependencies

Dependencies

ID Version
cookie-parser ^1.4.7
cors ^2.8.6
dotenv ^17.4.2
express ^5.2.1
Details
npm
2026-08-25 11:06:48 +00:00
56
UNLICENSED
latest
10 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