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

@pardoon/core

v1.0.1

Published

Pardoon Core Framework - Lightweight and modular Express.js framework with TypeScript support

Readme

Pardoon Core

TypeScript-first Express.js framework with NestJS-inspired decorators, automatic dependency injection, and production-ready features.

Features

  • Decorator-based Routing - @Controller, @Get, @Post, @Put, @Patch, @Del
  • Auto Dependency Injection - @Service, @Controller with automatic container binding
  • Parameter Decorators - @Body, @Query, @Param, @User, @Headers, @Ip, @File, @Files
  • Built-in Validation - Zod schema validation with i18n support
  • Dependency Injection - InversifyJS-based DI container
  • Authentication & Authorization - @Authenticated (class/method, extend by default, { override: true } to replace) and @RegisterPermission (class-level, DB-only) for permission registration
  • OpenAPI/Swagger - Auto-generated API documentation
  • Response Builder - Standardized API responses with file/stream support
  • Caching - @Cache and @ClearCache decorators with Redis support
  • Exception Handling - Exception filters with HttpException hierarchy
  • Middleware System - @Middleware decorator and plugin-based middleware architecture
  • Health Checks - /health, /health/live, /health/ready endpoints
  • Graceful Shutdown - SIGTERM/SIGINT handlers with cleanup hooks
  • Security Headers - HSTS, X-Frame-Options, CSP, etc.
  • Rate Limiting - Configurable rate limiter
  • Metrics - Prometheus-compatible metrics endpoint
  • Request Tracing - X-Request-ID header for request correlation
  • Environment Validation - Zod-based validateEnv with typed config
  • Testing Utilities - TestClient, TestingModule for integration tests
  • Base Classes - BaseController, pagination DTOs, paginated response helpers

Installation

npm install @pardoon/core
# or
bun add @pardoon/core

Peer Dependencies

npm install express@^5 inversify@^7 reflect-metadata@^0.2 zod@^4
# Optional
npm install redis@^5 socket.io@^4

Quick Start

import {
	container,
	CORE_TYPES,
	App,
	Controller,
	Get,
	Post,
	Body,
	Query,
	Param,
	User,
	Authenticated,
	Service,
	buildAutoBind,
	inject,
	response,
	validateEnv,
	baseEnvSchema,
} from "@pardoon/core";
import { z } from "zod";

// Validate environment variables
const envSchema = baseEnvSchema.extend({
	DB_HOST: z.string().min(1),
});
const env = validateEnv(envSchema);

// Define schemas
const CreateUserSchema = z.object({
	name: z.string().min(3),
	email: z.string().email(),
});

type ICreateUser = z.infer<typeof CreateUserSchema>;

// Create a service (auto-binds to container as singleton)
@Service("IUserService")
class UserService {
	async findAll(page: number) { /* ... */ }
	async findById(id: string) { /* ... */ }
	async create(data: ICreateUser, userId: number) { /* ... */ }
}

// Create a controller (auto-binds to container)
@Authenticated("read")
@Controller("/users")
class UserController {
	constructor(@inject("IUserService") private userService: UserService) {}

	@Get("/")
	async findAll(@Query("page") page: number = 1) {
		return this.userService.findAll(page);
	}

	@Get("/:id")
	async findOne(@Param("id") id: string) {
		return this.userService.findById(id);
	}

	@Authenticated("create")
	@Post("/")
	async create(
		@Body(CreateUserSchema) body: ICreateUser,
		@User("id") userId: number,
	) {
		return this.userService.create(body, userId);
	}
}

// Setup container
container.bind(CORE_TYPES.IConfig).toConstantValue({
	PORT: env.PORT,
	NODE_ENV: env.NODE_ENV,
});
container.bind(CORE_TYPES.IApp).to(App).inSingletonScope();

// Load auto-bound services and controllers
buildAutoBind(container);

// Bootstrap
const app = container.get<App>(CORE_TYPES.IApp);
app.setAuthMiddleware(() => myAuthMiddleware());
app.setPermissionMiddleware((permissions, packageName) =>
	myPermissionMiddleware(permissions, packageName),
);
await app.init();
await app.afterInit();

Table of Contents


Auto Dependency Injection

Pardoon Core provides automatic dependency injection using decorators. No more manual container.bind() for every service!

@Service Decorator

Automatically binds a class to the DI container as a singleton. The @Service decorator handles both @injectable() and @provide() internally — no need to add them manually.

import { Service, inject } from "@pardoon/core";

// Using string identifier (converts to Symbol.for("IUserService"))
@Service("IUserService")
class UserService implements IUserService {
	constructor(@inject("IConfig") private config: IConfig) {}
	// ...
}

// Using Symbol directly (e.g., from SERVICE_TYPES)
@Service(SERVICE_TYPES.IUserService)
class UserService implements IUserService {
	// ...
}

@TransientService Decorator

Creates a new instance for each injection (non-singleton).

import { TransientService } from "@pardoon/core";

@TransientService("IRequestContext")
class RequestContext implements IRequestContext {
	readonly timestamp = Date.now();
}

@Singleton Alias

Singleton is an alias for @Service for more explicit naming:

import { Singleton } from "@pardoon/core";

@Singleton("IEmailService")
class EmailService implements IEmailService {
	// ...
}

@Controller with Auto-Bind

Controllers are automatically bound to the container by default.

import { Controller } from "@pardoon/core";

// Auto-bind enabled (default)
@Controller("/users")
class UserController {
	// Automatically bound to CORE_TYPES.IController
}

// Auto-bind disabled (manual binding required)
@Controller("/users", { autoBind: false })
class UserController {
	// Must manually bind: container.bind(CORE_TYPES.IController).to(UserController)
}

Building Auto-Bindings

After importing all modules with decorators, call buildAutoBind() to load them into the container.

// inversify.config.ts
import { container, buildAutoBind, CORE_TYPES, App } from "@pardoon/core";

// 1. Manual bindings (config, core services)
container.bind(CORE_TYPES.IConfig).to(Config).inSingletonScope();
container.bind(CORE_TYPES.IApp).to(App).inSingletonScope();

// 2. Import all modules (decorators execute on import)
import "./modules/user/user.service";
import "./modules/user/user.controller";
import "./modules/announcement/announcement.service";
import "./modules/announcement/announcement.controller";
// ... other modules

// 3. Build auto-bindings
buildAutoBind(container);

Dynamic Module Loading (Vite/Bun)

import { loadModules, buildAutoBind, container } from "@pardoon/core";

// Load all services and controllers dynamically
await loadModules(import.meta.glob("./modules/**/*.service.ts"));
await loadModules(import.meta.glob("./modules/**/*.controller.ts"));

// Build bindings
buildAutoBind(container);

Controllers

Basic Controller

import { Controller, Get, Post, Put, Patch, Del } from "@pardoon/core";

@Controller("/api/users")
class UserController {
  @Get("/")
  async findAll() {
    return { users: [] };
  }

  @Get("/:id", "Get User by ID")
  async findOne() { ... }

  @Post("/", "Create User")
  async create() { ... }

  @Put("/:id", "Update User")
  async update() { ... }

  @Patch("/:id", "Partial Update")
  async patch() { ... }

  @Del("/:id", "Delete User")
  async remove() { ... }
}

HTTP Method Decorators with Response Schema

HTTP method decorators accept an optional 3rd argument for OpenAPI response schema:

@Get("/", "List Users", UserArraySchema)
async findAll() { ... }

// Multiple status codes via object map
@Get("/:id", "Get User", { 200: UserSchema, 404: NotFoundSchema })
async findOne() { ... }

Controller Options

// Disable auto-bind for manual binding control
@Controller("/api/users", { autoBind: false })
class UserController { ... }

Nested Controllers

@Controller("/api/settings")
class SettingsController { ... }

@Controller("/email")
@ChildOf(SettingsController)  // Results in /api/settings/email
class EmailSettingsController { ... }

Route ordering follows NestJS-style precedence: static routes → child controller routers → parametric routes (/:param).


Parameter Decorators

Extract and validate request data using decorators:

@Body()

// Extract entire body
@Post("/")
async create(@Body() body: CreateUserDto) { ... }

// Extract specific property
@Post("/")
async create(@Body("email") email: string) { ... }

// With Zod validation
@Post("/")
async create(@Body(CreateUserSchema) body: ICreateUser) { ... }

@Query()

// Extract all query params
@Get("/")
async findAll(@Query() query: FilterDto) { ... }

// Extract specific param
@Get("/")
async findAll(@Query("page") page: number) { ... }

// With Zod validation
@Get("/")
async findAll(@Query(PaginationSchema) query: IPagination) { ... }

@Param()

// Extract route param
@Get("/:id")
async findOne(@Param("id") id: string) { ... }

// Extract all params
@Get("/:userId/posts/:postId")
async findPost(@Param() params: { userId: string; postId: string }) { ... }

@User()

// Get authenticated user
@Get("/profile")
@Authenticated()
async getProfile(@User() user: IUser) { ... }

// Get specific property
@Post("/")
@Authenticated()
async create(@User("id") userId: number) { ... }

@Headers()

// Get specific header
@Get("/")
async findAll(@Headers("authorization") auth: string) { ... }

// Get all headers
@Get("/")
async findAll(@Headers() headers: Record<string, string>) { ... }

@Ip()

@Post("/")
async create(@Ip() clientIp: string) { ... }

@Request() / @Response()

@Get("/download")
async download(@Request() req: Request, @Response() res: Response) {
  res.download("file.pdf");
}

@File()

Extract the uploaded file from req.file (single file upload):

@Post("/upload")
@SingleFileUpload("avatar", true)
async upload(@File() file: Express.Multer.File) {
  console.log(file.originalname, file.size);
}

@Files()

Extract uploaded files from req.files (multiple file upload):

@Post("/upload")
@MultipleFileUpload("photos", true, 5)
async upload(@Files() files: Express.Multer.File[]) {
  for (const file of files) {
    console.log(file.originalname);
  }
}

@Custom()

@Get("/")
async findAll(
  @Custom((req) => req.headers["x-tenant-id"]) tenantId: string
) { ... }

All Parameter Decorators

| Decorator | Type | Description | |-----------|------|-------------| | @Body() | body | Request body or specific property, with optional Zod validation | | @Query() | query | Query parameters or specific param, with optional Zod validation | | @Param() | params | Route parameters or specific param, with optional Zod validation | | @User() | user | Authenticated user object or specific property | | @Headers() | headers | Request headers or specific header | | @Ip() | ip | Client IP address | | @File() | file | Single uploaded file (req.file) | | @Files() | files | Multiple uploaded files (req.files) | | @Request() | request | Express Request object | | @Response() | response | Express Response object | | @Custom(fn) | custom | Custom extractor function |


Validation

Using Parameter Decorators (Recommended)

import { z } from "zod";

const CreateUserSchema = z.object({
  name: z.string().min(3),
  email: z.string().email(),
  age: z.number().min(18).optional(),
});

@Post("/")
async create(@Body(CreateUserSchema) body: z.infer<typeof CreateUserSchema>) {
  // body is validated and typed
}

~~@Validate Decorator~~ (Deprecated)

Deprecated: @Validate is deprecated and will be removed in a future version. Use parameter decorators with Zod schemas instead (e.g., @Body(Schema), @Query(Schema)).

Validation Error Response

{
	"success": false,
	"message": "Validation error",
	"timestamp": "2024-01-29T12:00:00.000Z",
	"path": "/api/users",
	"meta": {
		"error": {
			"message": "Validation error",
			"validationErrors": [
				{ "field": "email", "errors": ["Invalid email format"] },
				{
					"field": "name",
					"errors": ["String must contain at least 3 character(s)"]
				}
			]
		}
	}
}

Authentication & Authorization

Basic Authentication

@Get("/profile")
@Authenticated()  // Requires authentication
async getProfile(@User() user: IUser) { ... }

Class-level Authentication

Apply @Authenticated at class level to protect all routes. Method-level decorators extend (merge with) class permissions by default. Use { override: true } to replace class permissions.

@Authenticated("read")
@Controller("/blogs")
class BlogController extends BaseController {
  @Get("/")
  async list() {}           // requires "read"

  @Get("/:id")
  async getById() {}        // requires "read"

  @Authenticated("create")  // extend (default): "read" + "create"
  @Post("/draft")
  async createDraft() {}

  @Authenticated({ override: true }, "create")  // override: only "create" (replaces "read")
  @Post("/")
  async create() {}
}

Extend (default) vs Override

| Usage | Behavior | |-------|----------| | @Authenticated("create") | Extend – Merges with class permissions (default) | | @Authenticated({ override: true }, "create") | Override – Replaces class permissions |

Permission-based Authorization

@Post("/")
@Authenticated("create")  // Requires "create" permission
async create() { ... }

@Put("/:id")
@Authenticated("update", "admin")  // Requires "update" OR "admin" permission
async update() { ... }

@RegisterPermission (DB-only, no auto-check)

Register permissions for a module without automatic runtime checks. Class-level only. Use with req.user.hasPermission() for manual control in handlers.

@RegisterPermission("create", "read", "update", "delete", "export")
@Controller("/items")
class ItemController extends BaseController {
  constructor() {
    super("items");
  }

  @Authenticated("create")
  @Post("/")
  async create() {}

  @Authenticated()  // JWT only - req.user is set
  @Get("/export")
  async export(req: ICustomRequest) {
    if (!req.user!.hasPermission(this.packageName, "export")) {
      throw response().forbidden("Export izni yok");
    }
    // ...
  }
}

Setup Auth Middleware

app.setAuthMiddleware(() => myAuthMiddleware());
app.setPermissionMiddleware((permissions, packageName) =>
	myPermissionMiddleware(permissions, packageName),
);

Response Builder

Success Responses

import { response } from "@pardoon/core";

// 200 OK
return response().ok(data);

// 201 Created
return response().created(data);

// 202 Accepted
return response().accepted(data);

// 204 No Content
return response().noContent();

// With message
return response().ok(data).withMessage("User created successfully");

// With i18n message key
return response().ok(data).withMessage("user.created");

Error Responses

// 400 Bad Request
return response().badRequest("Invalid input");

// 404 Not Found
return response().notFound("user.not_found");

// 401 Unauthorized
return response().unauthorized();

// 403 Forbidden
return response().forbidden();

// 409 Conflict
return response().conflict("Email already exists");

// 500 Internal Server Error
return response().internalError();

// Custom error
return response().error("Custom error message", 422);

// Error from Error object
return response().errorFromError(error);

File & Stream Responses

// Serve a file (auto-detects MIME type from extension)
return response(filePath).asFile();

// Serve with explicit MIME type
return response(buffer).asFile("application/pdf");

// Download with filename
return response(filePath).download("report.pdf");

// Stream response
return response(readableStream).asFile("text/csv");

Custom Headers

return response().ok(data)
  .setHeader("X-Custom", "value")
  .setHeaders({ "X-A": "1", "X-B": "2" });

Pagination

return response().withPagination(items, {
	page: 1,
	limit: 10,
	totalItems: 100,
});

Paginated Response Helper

Controllers can return IPaginatedResponse objects directly — the framework automatically wraps them:

import { createPaginatedResponse } from "@pardoon/core";

@Get("/")
async findAll(@Query("page") page: number = 1) {
  const { items, total } = await this.service.findAll(page, 10);
  return createPaginatedResponse(items, page, 10, total);
}

Response Format

{
  "success": true,
  "message": "OK",
  "data": { ... },
  "timestamp": "2024-01-29T12:00:00.000Z",
  "path": "/api/users",
  "meta": {
    "pagination": {
      "page": 1,
      "limit": 10,
      "totalItems": 100,
      "totalPages": 10,
      "hasNextPage": true,
      "hasPrevPage": false
    }
  }
}

ResponseHelpers

import { ResponseHelpers } from "@pardoon/core";

ResponseHelpers.isSuccessStatus(200);        // true
ResponseHelpers.isErrorStatus(404);          // true
ResponseHelpers.getStatusCategory(201);      // "success"
ResponseHelpers.getStatusCategory(500);      // "server-error"

Caching

Cache Responses

import { Cache, ClearCache } from "@pardoon/core";

@Get("/")
@Cache({
  query: ["page", "limit"],  // Cache key includes these query params
  ttl: 60,                   // TTL in seconds (default: 300)
  userScoped: true,          // Separate cache per user (default: true)
})
async findAll() { ... }

Cache Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | query | string[] | [] | Query params to include in cache key | | body | string[] | [] | Body properties to include in cache key | | params | string[] | [] | URL params to include in cache key | | ttl | number | 300 | Cache TTL in seconds | | userScoped | boolean | true | Scope cache per user | | extraKey | (req) => Record<string, unknown> | - | Custom cache key parts | | manipulate | (req, data) => Promise<void> | - | Transform cached data before returning |

Clear Cache

@Post("/")
@ClearCache({ paths: ["/api/users"] })
async create() { ... }

@Put("/:id")
@ClearCache({
  dynamicPaths: async (req) => [`/api/users/${req.params.id}`],
  userScoped: true,
})
async update() { ... }

Exception Handling

HTTP Exceptions

import {
  BadRequestException,
  UnauthorizedException,
  ForbiddenException,
  NotFoundException,
  ConflictException,
  InternalServerErrorException,
} from "@pardoon/core";

// Throw exceptions
throw new NotFoundException("User not found");
throw new BadRequestException({ message: "Invalid input", details: {...} });

Custom Exception Filter

import { IExceptionFilter, IExceptionContext, Catch } from "@pardoon/core";

@Catch(CustomException)
class CustomExceptionFilter implements IExceptionFilter<CustomException> {
	catch(exception: CustomException, context: IExceptionContext) {
		const { response } = context;
		response.status(400).json({
			success: false,
			message: exception.message,
			code: exception.code,
		});
	}
}

// Register filter
app.addExceptionFilter(new CustomExceptionFilter(), CustomException);

// Or set a global exception filter (replaces the default)
app.setGlobalExceptionFilter(new MyGlobalFilter());

Middleware

@Middleware Decorator

Add custom Express middleware to specific routes:

import { Middleware } from "@pardoon/core";

// Run before validation (isPre = true)
@Middleware(myPreMiddleware, true, 0)
@Get("/")
async handler() { ... }

// Run after auth/permissions (default: isPre = false)
@Middleware(myPostMiddleware)
@Get("/")
async handler() { ... }

// Multiple middlewares with ordering
@Middleware(firstMiddleware, false, 1)
@Middleware(secondMiddleware, false, 2)
@Post("/")
async create() { ... }

Parameters:

  • handler — Express RequestHandler
  • isPre — If true, runs before validation. Default: false (runs after auth/permissions)
  • order — Ordering within the same group (lower = earlier). Default: 0

Middleware Plugins

Register plugins that generate middleware for all routes:

import { IMiddlewarePlugin, IRouteContext } from "@pardoon/core";

const auditPlugin: IMiddlewarePlugin = {
  name: "audit-log",
  priority: 50,
  forRoute: (ctx: IRouteContext) => {
    if (ctx.route.method === "GET") return null;
    return (req, res, next) => {
      // log mutation requests
      next();
    };
  },
  afterResponse: async (ctx, req, res) => {
    // run after successful response
  },
};

app.usePlugin(auditPlugin);

Global Middleware

// Add global Express middleware
app.use(myGlobalMiddleware);

// Serve static files
app.static("/uploads", "./storage/uploads");

Health Checks

Endpoints

  • GET /health - Full health check with dependency status
  • GET /health/live - Liveness probe (is app alive?)
  • GET /health/ready - Readiness probe (is app ready to serve?)

Register Health Checks

// Database health check
app.registerHealthCheck("database", async () => {
	try {
		await db.execute(sql`SELECT 1`);
		return { status: "healthy", message: "Database connection OK" };
	} catch (error) {
		return { status: "unhealthy", message: error.message };
	}
});

// Redis health check
app.registerHealthCheck("redis", async () => {
	const pong = await redis.ping();
	return {
		status: pong === "PONG" ? "healthy" : "unhealthy",
		message: pong === "PONG" ? "Redis OK" : "Redis failed",
	};
});

Health Response

{
	"status": "healthy",
	"timestamp": "2024-01-29T12:00:00.000Z",
	"uptime": 3600,
	"checks": {
		"database": {
			"status": "healthy",
			"message": "Database connection OK",
			"responseTime": 5
		},
		"redis": {
			"status": "healthy",
			"message": "Redis OK",
			"responseTime": 2
		}
	}
}

Graceful Shutdown

Register Shutdown Hooks

// Stop cron jobs
app.onShutdown("cron-jobs", async () => {
	cronService.stopAllJobs();
});

// Close database connections
app.onShutdown("database", async () => {
	await db.close();
});

// Close Redis
app.onShutdown("redis", async () => {
	await redis.quit();
});

// Close Socket.IO
app.onShutdown("socket-io", async () => {
	io.close();
});

Shutdown Flow

  1. Receive SIGTERM/SIGINT/SIGUSR2 signal
  2. Stop accepting new connections
  3. Mark app as not ready (/health/ready returns 503)
  4. Execute shutdown hooks in parallel
  5. Wait for timeout (configurable via shutdownTimeout)
  6. Exit process

Security

Security Headers

Automatically applied security headers:

| Header | Default Value | | ------------------------- | ----------------------------------- | | X-Content-Type-Options | nosniff | | X-Frame-Options | DENY | | X-XSS-Protection | 1; mode=block | | Strict-Transport-Security | max-age=31536000; includeSubDomains | | Referrer-Policy | strict-origin-when-cross-origin |

Configuration

const config = {
	securityHeaders: {
		enabled: true,
		frameOptions: "SAMEORIGIN", // or "DENY" or false
		noSniff: true,
		xssFilter: true,
		hsts: { maxAge: 31536000, includeSubDomains: true, preload: true },
		referrerPolicy: "strict-origin-when-cross-origin",
		contentSecurityPolicy: {
			"default-src": ["'self'"],
			"script-src": ["'self'", "'unsafe-inline'"],
		},
		crossOriginEmbedderPolicy: true,
		crossOriginOpenerPolicy: "same-origin",
		crossOriginResourcePolicy: "same-origin",
	},
};

CORS Configuration

const config = {
	cors: {
		origin: ["https://example.com", "https://app.example.com"],
		methods: ["GET", "POST", "PUT", "PATCH", "DELETE"],
		allowedHeaders: ["Content-Type", "Authorization"],
		exposedHeaders: ["X-Request-ID"],
		credentials: true,
		maxAge: 86400,
	},
};

Rate Limiting

Configuration

const config = {
	rateLimit: {
		enabled: true,
		windowMs: 60000, // 1 minute window
		max: 100, // 100 requests per window
		message: "Too many requests, please try again later.",
		skip: (req) => req.ip === "127.0.0.1",
		keyGenerator: (req) => req.ip || "unknown",
	},
};

Response Headers

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1706529600

Rate Limit Exceeded Response (429)

{
	"success": false,
	"message": "Too many requests, please try again later.",
	"error": "Too Many Requests"
}

Metrics

Enable Metrics

const config = {
	metrics: {
		enabled: true,
		path: "/metrics",
		format: "prometheus", // or "json"
		includeProcessMetrics: true,
		prefix: "myapp",
		excludePaths: [/\/metrics/, /\/health/],
	},
};

Custom Metrics

const registry = app.getMetricsRegistry();
if (registry) {
	const customCounter = registry.createCounter("custom_events_total", "Custom events");
	customCounter.inc();
}

Available Metrics

| Metric | Type | Description | | ----------------------------- | --------- | ------------------------- | | http_requests_total | Counter | Total HTTP requests | | http_request_duration_seconds | Histogram | Request duration | | http_active_requests | Gauge | Currently active requests | | http_errors_total | Counter | Total HTTP errors | | process_memory_bytes | Gauge | Process memory usage | | process_cpu_seconds_total | Gauge | CPU time |

Prometheus Format

# HELP http_requests_total Total number of HTTP requests
# TYPE http_requests_total counter
http_requests_total{method="GET",path="/api/users",status="200"} 1523

# HELP http_request_duration_seconds HTTP request duration in seconds
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{method="GET",path="/api/users",status="200",le="0.1"} 1400
http_request_duration_seconds_bucket{method="GET",path="/api/users",status="200",le="0.5"} 1500
http_request_duration_seconds_sum{method="GET",path="/api/users",status="200"} 45.23
http_request_duration_seconds_count{method="GET",path="/api/users",status="200"} 1523

File Uploads

Single File

import { SingleFileUpload, File } from "@pardoon/core";

@Post("/upload")
@SingleFileUpload("file", true)  // fieldName, isRequired
async upload(@File() file: Express.Multer.File) {
  console.log(file.originalname, file.mimetype, file.size);
}

Multiple Files

import { MultipleFileUpload, Files } from "@pardoon/core";

@Post("/upload")
@MultipleFileUpload("files", true, 10)  // fieldName, isRequired, maxCount
async upload(@Files() files: Express.Multer.File[]) {
  for (const file of files) {
    console.log(file.originalname);
  }
}

Multi-Field File Upload

import { MultiFieldFileUpload, File, Files } from "@pardoon/core";

@Post("/upload")
@MultiFieldFileUpload([
  { fieldName: "avatar", isRequired: true },
  { fieldName: "cover", isRequired: false },
])
async upload(@Files() files: Record<string, Express.Multer.File[]>) {
  const avatar = files["avatar"]?.[0];
  const cover = files["cover"]?.[0];
}

Combining with Other Parameter Decorators

@Post("/profile/avatar")
@Authenticated()
@SingleFileUpload("avatar", true)
async updateAvatar(
  @File() file: Express.Multer.File,
  @User("id") userId: number,
  @Body("caption") caption: string,
) {
  return this.profileService.updateAvatar(userId, file, caption);
}

Persist Uploads

Automatically persist uploaded files to storage:

import { PersistUploads, SingleFileUpload } from "@pardoon/core";

@Post("/profile")
@SingleFileUpload("avatar", true)
@PersistUploads([
  { field: "avatar", target: "avatarPath", subdir: "avatars" },
])
async updateProfile(req: Request) {
  // req.body.avatarPath contains the persisted file path
}

Setup File Upload Middleware

app.setFileUpload({
  single: (fieldName, isRequired) => multerMiddleware.single(fieldName),
  multiple: (fieldName, maxCount, isRequired) => multerMiddleware.array(fieldName, maxCount),
  fields: (fields) => multerMiddleware.fields(fields),
  persist: (mapping) => persistMiddleware(mapping),
  errorHandler: () => multerErrorHandler(),
});

OpenAPI / Swagger

Enable Swagger UI

Swagger UI is automatically available at /api-docs when SwaggerService is configured.

Response Schema (New Way)

Pass the response schema as the 3rd argument to HTTP method decorators:

import { OpenApiTag } from "@pardoon/core";

@Controller("/api/users")
@OpenApiTag("Users")
class UserController {
  // Single schema (defaults to 200)
  @Get("/", "List Users", UserArraySchema)
  async findAll() { ... }

  // Multiple status codes
  @Get("/:id", "Get User", { 200: UserSchema, 404: NotFoundSchema })
  async findOne() { ... }
}

Deprecated: The @ResponseSchema decorator is deprecated. Use the 3rd argument of HTTP method decorators instead.


Environment Validation

Validate and type environment variables at startup using Zod:

import { validateEnv, baseEnvSchema, envTransformers } from "@pardoon/core";
import { z } from "zod";

const envSchema = baseEnvSchema.extend({
  DB_HOST: z.string().min(1),
  DB_PORT: envTransformers.port.default(5432),
  DB_PASSWORD: z.string().min(1),
  DEBUG: envTransformers.toBoolean.default(false),
  ALLOWED_ORIGINS: envTransformers.toArray.default([]),
  FEATURE_FLAGS: envTransformers.toJson(z.record(z.boolean())).optional(),
});

const config = validateEnv(envSchema);
// config is fully typed: { NODE_ENV, PORT, DB_HOST, DB_PORT, ... }

Available Transformers

| Transformer | Description | |-------------|-------------| | envTransformers.toNumber | Parse string to number | | envTransformers.toBoolean | Parse "true", "1", "yes" to boolean | | envTransformers.toOptionalBoolean | Optional boolean | | envTransformers.toOptionalNumber | Optional number | | envTransformers.toArray | Comma-separated string to array | | envTransformers.toJson(schema) | Parse JSON string with Zod schema | | envTransformers.port | Port number (1–65535) | | envTransformers.url | URL string | | envTransformers.nonEmpty | Non-empty string | | envTransformers.optionalNonEmpty | Optional non-empty string |

Config from Schema

Create a typed config class for InversifyJS binding:

import { createConfigFromSchema } from "@pardoon/core";

const Config = createConfigFromSchema(envSchema);
container.bind(CORE_TYPES.IConfig).to(Config).inSingletonScope();

Print Config Summary

import { printEnvSummary } from "@pardoon/core";

printEnvSummary(config);
// Automatically masks keys containing PASSWORD, SECRET, TOKEN, KEY, CREDENTIALS

Base Classes & DTOs

BaseController

All controllers that need module permission registration should extend BaseController:

import { BaseController, Controller, Authenticated } from "@pardoon/core";

@Authenticated("read")
@Controller("/users")
class UserController extends BaseController {
  constructor() {
    super("users"); // packageName (must be camelCase)
  }

  @Get("/")
  async findAll() { ... }
}

BaseController provides:

  • packageName — module identifier (camelCase enforced)
  • registerModulePermissions() — auto-registers all route + @RegisterPermission permissions
  • getModulePermissions() — collects unique permissions from routes and class metadata

Built-in DTO Schemas

import {
  PaginationQuerySchema,
  IdParamsSchema,
  SearchQuerySchema,
} from "@pardoon/core";

// Pagination: { page?: number, limit?: number }
@Get("/")
async findAll(@Query(PaginationQuerySchema) query: IPaginationQuery) { ... }

// ID params: { id: number }
@Get("/:id")
async findOne(@Param(IdParamsSchema) params: IIdParams) { ... }

// Search: pagination + { search?, sortBy?, sortOrder? }
@Get("/search")
async search(@Query(SearchQuerySchema) query: ISearchQuery) { ... }

Paginated Response

import { createPaginatedResponse, IPaginatedResponse } from "@pardoon/core";

async findAll(page: number, limit: number): Promise<IPaginatedResponse<User>> {
  const [items, total] = await db.findAndCount({ skip: (page - 1) * limit, take: limit });
  return createPaginatedResponse(items, page, limit, total);
}

Testing

Pardoon Core includes testing utilities for integration tests:

import {
  TestingModule,
  TestClient,
  TestAppBuilder,
  createMockRequest,
  createMockResponse,
  createMockNext,
  assertions,
} from "@pardoon/core";

// Integration test setup
const testModule = new TestingModule(async () => {
  // create and return your App instance
  return app;
});

await testModule.init();
await testModule.start();

const client = testModule.getClient();
client.setAuthToken("test-jwt-token");

// Make requests
const res = await client.get("/api/users", {
  query: { page: "1", limit: "10" },
});

assertions.expectStatus(res, 200);
assertions.expectBodyContains(res, "data");

// Cleanup
await testModule.close();

Mock Service Builder

const builder = new TestAppBuilder();
builder.setContainer(container);
builder.mockService<IUserService>("IUserService", {
  findAll: async () => [],
  findById: async (id) => ({ id, name: "Test" }),
});
builder.applyMocks();

Mock Helpers

const req = createMockRequest({ method: "POST", body: { name: "Test" } });
const { response, getStatus, getBody } = createMockResponse();
const { next, wasCalled, getError } = createMockNext();

Configuration

Environment Variables

# Server
PORT=3000
NODE_ENV=development

# API
API_VERSION=v1
API_PREFIX=/api
REQUEST_TIMEOUT=30000

# Security
CORS_ORIGINS=https://example.com,https://app.example.com

# Rate Limiting
RATE_LIMIT_ENABLED=true
RATE_LIMIT_WINDOW_MS=60000
RATE_LIMIT_MAX=100

# Metrics
METRICS_ENABLED=true
METRICS_PATH=/metrics

# Shutdown
SHUTDOWN_TIMEOUT=30000

# Redis (optional)
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=

# Cache (optional)
DEV_CLOSE_CACHE=false
NO_CACHE_SECRET=
CACHE_DEFAULT_TTL=300

# i18n (optional)
I18N_LOCALES_PATH=src/locales
I18N_DEFAULT_LOCALE=tr
I18N_SUPPORTED_LOCALES=tr,en

# Swagger (optional)
SWAGGER_ENABLED=true
SWAGGER_TITLE=My API
SWAGGER_VERSION=1.0.0
SWAGGER_DESCRIPTION=API Documentation
SWAGGER_BASE_PATH=/api-docs

IConfig Interface

interface IConfig {
	NODE_ENV: string;
	PORT: number;

	// Redis
	REDIS_HOST?: string;
	REDIS_PORT?: number;
	REDIS_PASSWORD?: string;

	// Cache
	DEV_CLOSE_CACHE?: boolean;
	NO_CACHE_SECRET?: string;
	CACHE_DEFAULT_TTL?: number;

	// i18n
	I18N_LOCALES_PATH?: string;
	I18N_DEFAULT_LOCALE?: string;
	I18N_SUPPORTED_LOCALES?: string;

	// Swagger
	SWAGGER_ENABLED?: boolean;
	SWAGGER_TITLE?: string;
	SWAGGER_VERSION?: string;
	SWAGGER_DESCRIPTION?: string;
	SWAGGER_BASE_PATH?: string;
	SWAGGER_AUTH_ENDPOINT_PATTERN?: string;
	SWAGGER_BASIC_AUTH_USER?: string;
	SWAGGER_BASIC_AUTH_PASSWORD?: string;
}

IAppConfig Interface

interface IAppConfig extends IConfig {
	applicationName?: string;
	host?: string;

	// Server
	logFormat?: string;          // Morgan format. Default: "dev"
	bodyLimit?: string;          // Request body limit. Default: "10mb"
	poweredBy?: string;          // X-Powered-By header. Default: "Pardoon Core"

	// API
	apiVersion?: string;         // e.g., "v1" → "/api/v1/..."
	apiPrefix?: string;          // Default: "/api"
	requestTimeout?: number;     // ms. Default: 30000. Set 0 to disable.

	// Security
	securityHeaders?: ISecurityHeadersConfig;
	cors?: ICorsConfig;
	rateLimit?: IRateLimitConfig;

	// Features
	enableRequestId?: boolean;   // Default: true
	shutdownTimeout?: number;    // ms. Default: 30000
	metrics?: {
		enabled?: boolean;
		path?: string;               // Default: "/metrics"
		format?: "prometheus" | "json";
		excludePaths?: RegExp[];
		includeProcessMetrics?: boolean;
		prefix?: string;
	};
}

App Methods Reference

| Method | Description | |--------|-------------| | init() | Initialize routes, Swagger, i18n and start listening | | afterInit() | Register error handlers (call after init) | | use(middleware) | Add global Express middleware | | static(path, dir) | Serve static files | | usePlugin(plugin) | Register middleware plugin | | setAuthMiddleware(factory) | Set JWT auth middleware | | setPermissionMiddleware(factory) | Set permission check middleware | | setLoggingMiddleware(factory) | Set request logging middleware | | setFileUpload(factory) | Set file upload middleware | | addExceptionFilter(filter, ...types) | Add custom exception filter | | setGlobalExceptionFilter(filter) | Replace default exception filter | | onShutdown(name, hook) | Register shutdown hook | | registerHealthCheck(name, fn) | Register health check | | getApp() | Get Express app instance | | getHttpServer() | Get HTTP server instance | | getApiBasePath() | Get resolved API path (e.g., /api/v1) | | getMetricsRegistry() | Get metrics registry for custom metrics | | getDefaultMetrics() | Get default HTTP metrics | | isReady() | Check if app is ready | | isAlive() | Check if app is alive | | shutdown(timeout?) | Gracefully shutdown |


License

ISC © MYTECH TEKNOLOJİ