@sidymohamed_12/package-core
v1.1.0
Published
Noyau partagé : IoC Container, BaseController, Exceptions, Middlewares, RestResponse, utils JWT/Hash
Maintainers
Readme
@sidymohamed_12/package-core
Noyau partagé pour les APIs Express/TypeScript : IoC Container, BaseController, Exceptions HTTP, Middlewares configurables, RestResponse standardisé, et utilitaires JWT générique / Hash.
Table des matières
- Installation
- Configuration initiale
- Variables d'environnement
- 1. checkEnv — Validation au démarrage
- 2. IoC Container
- 3. JWT — Payload générique
- 4. Middlewares
- 5. BaseController
- 6. Exceptions HTTP
- 7. RestResponse — Réponses standardisées
- 8. Utilitaires Hash (bcrypt)
- Exemple d'intégration complète
Installation
npm install @sidymohamed_12/package-corePeer dependencies requises
npm install express jsonwebtoken bcryptjs cors
npm install -D @types/express @types/jsonwebtoken @types/bcryptjs @types/cors typescriptConfiguration initiale
Après installation, créez un fichier .env à la racine de votre projet avec les variables suivantes :
NODE_ENV=development
# JWT
JWT_SECRET=remplacez_par_une_cle_longue_et_aleatoire
JWT_EXPIRES_IN=7d
# Bcrypt
BCRYPT_ROUNDS=10
# CORS (origines séparées par des virgules)
CORS_ORIGINS=http://localhost:4200💡 Pour générer une clé JWT sécurisée :
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
Variables d'environnement
| Variable | Défaut | Obligatoire en prod | Description |
| ---------------- | ------------------------- | ------------------- | ------------------------------------------------- |
| JWT_SECRET | change_me_in_production | ✅ Oui | Clé secrète pour signer les tokens JWT |
| JWT_EXPIRES_IN | 7d | Non | Durée de vie du token (1h, 7d, 30d…) |
| BCRYPT_ROUNDS | 10 | Non | Rounds de hachage bcrypt (min. recommandé : 10) |
| CORS_ORIGINS | http://localhost:4200 | Non | Origines CORS autorisées, séparées par , |
1. checkEnv — Validation au démarrage
checkEnv() vérifie que toutes les variables d'environnement critiques sont définies. À appeler en premier, avant tout le reste du bootstrap.
import { checkEnv } from "@sidymohamed_12/package-core";
checkEnv(); // ← toujours en premierComportement :
| Situation | Dev (NODE_ENV=development) | Prod (NODE_ENV=production) |
| -------------------------------- | ---------------------------------- | ------------------------------------------------- |
| Variable absente | ⚠️ console.warn — l'app continue | ❌ console.error + throw — l'app ne démarre pas |
| JWT_SECRET = valeur par défaut | ⚠️ console.warn | ❌ throw |
| BCRYPT_ROUNDS < 10 | ⚠️ console.warn | ⚠️ console.warn |
| Tout est OK | ✅ log de confirmation | ✅ log de confirmation |
Exemple de sortie en développement avec variables manquantes :
[@sidymohamed_12/package-core] ⚠️ JWT_SECRET non défini — valeur par défaut utilisée (dev uniquement). Ajoutez JWT_SECRET=your_secret dans votre .env
[@sidymohamed_12/package-core] ⚠️ CORS_ORIGINS non défini — valeur par défaut utilisée : http://localhost:42002. IoC Container
Le container gère le cycle de vie de vos dépendances. À utiliser dans un fichier de composition root (ex. src/factory.ts).
etape de creation : repo --> service --> controller
import { Container } from "@sidymohamed_12/package-core";
import { UserRepository } from "./modules/user/user.repository";
import { UserService } from "./modules/user/user.service";
import { UserController } from "./modules/user/user.controller";
export function registerDependencies() {
// Singleton — une seule instance partagée pour toute l'app
Container.registerSingleton("UserRepository", () => new UserRepository());
Container.registerSingleton(
"UserService",
() => new UserService(Container.resolve<UserRepository>("UserRepository")),
);
// Transient — nouvelle instance à chaque resolve()
Container.registerTransient(
"UserController",
() => new UserController(Container.resolve<UserService>("UserService")),
);
}
// Résolution
const userService = Container.resolve<UserService>("UserService");Méthodes disponibles :
| Méthode | Description |
| ----------------------------------- | ----------------------------------------------------------- |
| registerSingleton(token, factory) | Instance unique partagée sur toute la durée de vie de l'app |
| registerTransient(token, factory) | Nouvelle instance à chaque appel de resolve |
| resolve<T>(token) | Récupère (ou crée) l'instance |
| has(token) | Vérifie si un token est enregistré |
| reset() | Vide le container (utile pour les tests unitaires) |
| listTokens() | Liste tous les tokens enregistrés |
3. JWT — Payload générique
JwtPayload<T> est un type générique : définissez votre propre interface pour typer vos tokens. BaseJwtPayload (iat, exp) est toujours inclus automatiquement.
Définir son payload
// src/types/auth.payload.ts
export interface AuthPayload {
userId: number;
email: string;
role: "admin" | "user" | "doctor";
}Signer un token
import { signToken } from "@sidymohamed_12/package-core";
import { AuthPayload } from "./types/auth.payload";
const token = signToken<AuthPayload>({
userId: 42,
email: "[email protected]",
role: "admin",
});
// Utilise JWT_SECRET et JWT_EXPIRES_IN depuis le .envVérifier un token
import { verifyToken } from "@sidymohamed_12/package-core";
import { AuthPayload } from "./types/auth.payload";
try {
const decoded = verifyToken<AuthPayload>(token);
console.log(decoded.userId); // number ✅
console.log(decoded.role); // 'admin' | 'user' | 'doctor' ✅
console.log(decoded.exp); // timestamp d'expiration ✅ (BaseJwtPayload)
} catch (err) {
// JsonWebTokenError ou TokenExpiredError
// → intercepté automatiquement par globalExceptionHandler
}Types exportés
import type {
JwtPayload,
BaseJwtPayload,
AuthRequest,
} from "@sidymohamed_12/package-core";
// JwtPayload<T> = T & BaseJwtPayload
// BaseJwtPayload = { iat?: number; exp?: number }
// AuthRequest<T> = Request Express avec req.user?: JwtPayload<T>4. Middlewares
CORS configurable
Deux modes d'utilisation :
Mode simple — via variable d'environnement :
import { corsMiddleware } from "@sidymohamed_12/package-core";
app.use(corsMiddleware);
// Lit CORS_ORIGINS depuis le .env
// Ex: CORS_ORIGINS=https://mon-app.com,https://admin.mon-app.comMode avancé — createCorsMiddleware() :
import { createCorsMiddleware } from "@sidymohamed_12/package-core";
app.use(
createCorsMiddleware({
origins: ["https://mon-app.com", "https://admin.mon-app.com"],
methods: ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization", "x-client-type"],
exposedHeaders: ["X-Total-Count"],
credentials: true,
maxAge: 86400, // cache du preflight en secondes (24h)
}),
);Développement — tout autoriser :
app.use(
createCorsMiddleware({
origins: "*",
credentials: false, // ⚠️ obligatoire avec "*"
}),
);Options disponibles (CorsConfig) :
| Option | Type | Défaut | Description |
| ---------------- | ----------------- | -------------------------------------------------- | ---------------------------------------------- |
| origins | string[] \| "*" | Valeur de CORS_ORIGINS | Origines autorisées |
| methods | string[] | ['GET','POST','PATCH','PUT','DELETE','OPTIONS'] | Méthodes HTTP autorisées |
| allowedHeaders | string[] | ['Content-Type','Authorization','x-client-type'] | Headers autorisés dans les requêtes |
| exposedHeaders | string[] | [] | Headers exposés au client dans la réponse |
| credentials | boolean | true | Autorise les cookies cross-origin |
| maxAge | number | 86400 | Durée de cache du preflight OPTIONS (secondes) |
⚠️
credentials: trueest incompatible avecorigins: "*". Le package désactive automatiquementcredentialsdans ce cas et affiche un warning dans la console.
Auth JWT générique
Deux modes d'utilisation :
Mode simple — authMiddleware (sans typage personnalisé) :
import { authMiddleware, AuthRequest } from "@sidymohamed_12/package-core";
app.get("/profile", authMiddleware, (req: AuthRequest, res) => {
const userId = req.user?.userId as number; // cast manuel nécessaire
res.json({ userId });
});Mode typé — createAuthMiddleware<T>() (recommandé) :
import {
createAuthMiddleware,
AuthRequest,
} from "@sidymohamed_12/package-core";
import { AuthPayload } from "./types/auth.payload";
// Créez votre middleware typé une seule fois, ex. dans app.ts
export const authMiddleware = createAuthMiddleware<AuthPayload>();
// Dans votre router — req.user est pleinement typé
app.get("/profile", authMiddleware, (req: AuthRequest<AuthPayload>, res) => {
const { userId, role } = req.user!; // ✅ aucun cast nécessaire
res.json({ userId, role });
});Le middleware :
- Vérifie la présence du header
Authorization: Bearer <token> - Valide et décode le token JWT avec
JWT_SECRET - Injecte le payload décodé dans
req.user - Lance
UnauthorizedException(401) si le token est absent, expiré ou invalide
BFF — Détection du client
bffMiddleware détecte si la requête vient d'une interface web ou mobile et injecte le résultat dans req.clientType.
import { bffMiddleware } from "@sidymohamed_12/package-core";
app.use(bffMiddleware);
// Dans un controller
app.get("/config", (req, res) => {
res.json({ client: req.clientType }); // 'web' | 'mobile'
});La détection se base sur le header x-client-type envoyé par le client.
GlobalExceptionHandler
Intercepte toutes les exceptions de l'application et formate automatiquement la réponse HTTP. Doit toujours être enregistré en dernier.
import { globalExceptionHandler } from "@sidymohamed_12/package-core";
// ... toutes vos routes ...
app.use(globalExceptionHandler); // ← toujours en dernierFormat de réponse en cas d'erreur :
// Erreur standard (400, 401, 403, 404, 409)
{
"status": 404,
"message": "User avec l'identifiant \"42\" introuvable."
}
// Erreur de validation (422)
{
"status": 422,
"message": "Erreur de validation",
"errors": {
"email": "Email requis",
"name": "Nom requis"
}
}
// Erreur interne non gérée (500)
{
"status": 500,
"message": "Une erreur interne est survenue."
}5. BaseController
Classe abstraite générique à étendre dans vos controllers. Le paramètre T correspond à votre payload JWT — le même T que celui passé à createAuthMiddleware<T>(). Cela garantit que req.user est partout cohérent et pleinement typé.
import { BaseController, AuthRequest } from "@sidymohamed_12/package-core";
import { Response, NextFunction } from "express";
import { AuthPayload } from "../types/auth.payload";
export class UserController extends BaseController<AuthPayload> {
constructor(private readonly userService: UserService) {
super();
}
// GET /users/:id — parse l'id depuis les params de route
getUser = (
req: AuthRequest<AuthPayload>,
res: Response,
next: NextFunction,
): void => {
this.handleAsync(
() => this.userService.findById(this.parseId(req)),
next,
(user) => this.ok(res, user, "User"),
);
};
// POST /users — création, réponse 201
createUser = (
req: AuthRequest<AuthPayload>,
res: Response,
next: NextFunction,
): void => {
this.handleAsync(
() => this.userService.create(req.body),
next,
(user) => this.created(res, user, "User"),
);
};
// GET /me — récupère tout le payload JWT typé
getMyProfile = (
req: AuthRequest<AuthPayload>,
res: Response,
next: NextFunction,
): void => {
const { userId } = this.getUser(req); // ✅ typé AuthPayload, aucun cast
this.handleAsync(
() => this.userService.findById(userId),
next,
(user) => this.ok(res, user, "User"),
);
};
// GET /my-posts — récupère un seul champ du payload
getMyPosts = (
req: AuthRequest<AuthPayload>,
res: Response,
next: NextFunction,
): void => {
const userId = this.getUserField(req, "userId"); // number ✅
this.handleAsync(
() => this.postService.findByUser(userId),
next,
(posts) => this.ok(res, posts, "Post"),
);
};
// DELETE /posts/:id — réponse 204 No Content
deletePost = (
req: AuthRequest<AuthPayload>,
res: Response,
next: NextFunction,
): void => {
const postId = this.parseId(req, "postId");
this.handleAsync(
() => this.postService.delete(postId),
next,
() => this.noContent(res),
);
};
}Méthodes protégées disponibles :
| Méthode | Description |
| ---------------------------------- | ---------------------------------------------------------------------------------------------------- |
| handleAsync(fn, next, onSuccess) | Wrapper async — délègue toutes les erreurs au globalExceptionHandler |
| ok(res, data, type) | Réponse 200 OK |
| created(res, data, type) | Réponse 201 Created |
| noContent(res) | Réponse 204 No Content (ex: DELETE réussi) |
| parseId(req, param?) | Extrait et parse un paramètre de route en number (défaut : 'id') |
| getUser(req) | Retourne req.user entier, typé JwtPayload<T> — lance UnauthorizedException si absent |
| getUserField(req, 'monChamp') | Extrait un champ précis du payload, typé T[K] — lance BadRequestException si le champ est absent |
6. Exceptions HTTP
Toutes les exceptions héritent de AppException et sont interceptées automatiquement par globalExceptionHandler.
import {
NotFoundException,
BadRequestException,
ConflictException,
UnauthorizedException,
ForbiddenException,
ValidationException,
} from '@sidymohamed_12/package-core';
// 404 — Ressource introuvable
async findUser(id: number) {
const user = await this.repo.findById(id);
if (!user) throw new NotFoundException('User', id);
return user;
}
// 409 — Conflit
async register(dto: RegisterDto) {
const existing = await this.repo.findByEmail(dto.email);
if (existing) throw new ConflictException('Cet email est déjà utilisé.');
}
// 401 — Non authentifié
async login(dto: LoginDto) {
if (!isValid) throw new UnauthorizedException('Email ou mot de passe incorrect.');
}
// 403 — Accès interdit
async deleteUser(requesterId: number, targetId: number) {
if (requesterId !== targetId) throw new ForbiddenException();
}
// 400 — Données invalides
async updateRole(role: string) {
if (!ALLOWED_ROLES.includes(role)) {
throw new BadRequestException(`Rôle invalide : ${role}`);
}
}
// 422 — Erreurs de validation par champ
async submitForm(dto: FormDto) {
const errors: Record<string, string> = {};
if (!dto.email) errors.email = 'Email requis';
if (!dto.name) errors.name = 'Nom requis';
if (Object.keys(errors).length) throw new ValidationException(errors);
}Exceptions disponibles :
| Classe | Code HTTP | Description |
| ---------------------------------- | --------- | -------------------------------- |
| BadRequestException(message) | 400 | Données invalides ou mal formées |
| UnauthorizedException(message?) | 401 | Non authentifié |
| ForbiddenException(message?) | 403 | Authentifié mais accès refusé |
| NotFoundException(resource, id?) | 404 | Ressource introuvable |
| ConflictException(message) | 409 | Conflit (ex: email dupliqué) |
| ValidationException(fieldErrors) | 422 | Erreurs de validation par champ |
7. RestResponse — Réponses standardisées
Format de réponse unifié pour toute l'API.
import { RestResponse } from "@sidymohamed_12/package-core";
// Réponse simple
res.status(200).json(RestResponse.response(200, data, "User"));
// → { status: 200, results: { ... }, type: 'User' }
// Réponse paginée
res.status(200).json(
RestResponse.responsePaginate(
200,
items, // données du tableau
page, // page courante
totalPages,
totalItems,
page === 1, // first
page === totalPages, // last
"User",
),
);
// → {
// status: 200,
// results: [...],
// pages: [1, 2, 3],
// currentPage: 1,
// totalPages: 3,
// totalItems: 30,
// first: true,
// last: false,
// type: 'User'
// }8. Utilitaires Hash (bcrypt)
import { hashPassword, comparePassword } from "@sidymohamed_12/package-core";
// Lors de l'inscription — hacher le mot de passe avant de sauvegarder
const hashed = await hashPassword("monMotDePasse123");
await userRepository.save({ email, password: hashed });
// Lors de la connexion — comparer le mot de passe brut avec le hash
const isValid = await comparePassword("monMotDePasse123", hashed); // true | false
if (!isValid)
throw new UnauthorizedException("Email ou mot de passe incorrect.");Le nombre de rounds est configurable via BCRYPT_ROUNDS dans le .env (défaut : 10).
Exemple d'intégration complète
Structure recommandée pour un projet utilisant ce package :
src/
├── app.ts ← bootstrap + montage des middlewares
├── factory.ts ← composition root (IoC)
├── types/
│ └── auth.payload.ts ← définition de votre JwtPayload
└── modules/
└── user/
├── user.entity.ts
├── user.repository.ts
├── user.service.ts
└── user.controller.tssrc/types/auth.payload.ts
export interface AuthPayload {
userId: number;
email: string;
role: "admin" | "user";
}src/factory.ts
import { Container } from "@sidymohamed_12/package-core";
import { UserRepository } from "./modules/user/user.repository";
import { UserService } from "./modules/user/user.service";
import { UserController } from "./modules/user/user.controller";
export function registerDependencies() {
Container.registerSingleton("UserRepository", () => new UserRepository());
Container.registerSingleton(
"UserService",
() => new UserService(Container.resolve<UserRepository>("UserRepository")),
);
Container.registerSingleton(
"UserController",
() => new UserController(Container.resolve<UserService>("UserService")),
);
}src/app.ts
import "reflect-metadata";
import express from "express";
import {
checkEnv,
createCorsMiddleware,
createAuthMiddleware,
bffMiddleware,
globalExceptionHandler,
Container,
} from "@sidymohamed_12/package-core";
import { registerDependencies } from "./factory";
import { UserController } from "./modules/user/user.controller";
import { AuthPayload } from "./types/auth.payload";
// 1. Valider les variables d'environnement (en premier)
checkEnv();
// 2. Initialiser le container IoC
registerDependencies();
const app = express();
// 3. Middlewares globaux (ordre important)
app.use(
createCorsMiddleware({
origins: process.env.CORS_ORIGINS?.split(",") ?? ["http://localhost:4200"],
credentials: true,
}),
);
app.use(express.json());
app.use(bffMiddleware);
// 4. Middleware auth typé avec votre payload
const authMiddleware = createAuthMiddleware<AuthPayload>();
// 5. Routes
const userCtrl = Container.resolve<UserController>("UserController");
app.post("/auth/login", userCtrl.login); // publique
app.get("/users/:id", authMiddleware, userCtrl.getUser); // protégée
app.get("/me", authMiddleware, userCtrl.getMyProfile); // protégée
// 6. GlobalExceptionHandler — toujours en dernier
app.use(globalExceptionHandler);
app.listen(3000, () =>
console.log("🚀 Serveur démarré sur http://localhost:3000"),
);Licence
ISC
