npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@sidymohamed_12/package-core

v1.1.0

Published

Noyau partagé : IoC Container, BaseController, Exceptions, Middlewares, RestResponse, utils JWT/Hash

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

npm install @sidymohamed_12/package-core

Peer dependencies requises

npm install express jsonwebtoken bcryptjs cors
npm install -D @types/express @types/jsonwebtoken @types/bcryptjs @types/cors typescript

Configuration 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 premier

Comportement :

| 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:4200

2. 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 .env

Vé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.com

Mode 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: true est incompatible avec origins: "*". Le package désactive automatiquement credentials dans 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 dernier

Format 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.ts

src/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