@pardoon/core
v1.0.1
Published
Pardoon Core Framework - Lightweight and modular Express.js framework with TypeScript support
Maintainers
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,@Controllerwith 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 -
@Cacheand@ClearCachedecorators with Redis support - Exception Handling - Exception filters with HttpException hierarchy
- Middleware System -
@Middlewaredecorator and plugin-based middleware architecture - Health Checks -
/health,/health/live,/health/readyendpoints - 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
validateEnvwith typed config - Testing Utilities -
TestClient,TestingModulefor integration tests - Base Classes -
BaseController, pagination DTOs, paginated response helpers
Installation
npm install @pardoon/core
# or
bun add @pardoon/corePeer Dependencies
npm install express@^5 inversify@^7 reflect-metadata@^0.2 zod@^4
# Optional
npm install redis@^5 socket.io@^4Quick 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
- Controllers
- Parameter Decorators
- Validation
- Authentication & Authorization
- Response Builder
- Caching
- Exception Handling
- Middleware
- Health Checks
- Graceful Shutdown
- Security
- Rate Limiting
- Metrics
- File Uploads
- OpenAPI / Swagger
- Environment Validation
- Base Classes & DTOs
- Testing
- Configuration
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:
@Validateis 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— ExpressRequestHandlerisPre— Iftrue, 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 statusGET /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
- Receive SIGTERM/SIGINT/SIGUSR2 signal
- Stop accepting new connections
- Mark app as not ready (
/health/readyreturns 503) - Execute shutdown hooks in parallel
- Wait for timeout (configurable via
shutdownTimeout) - 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: 1706529600Rate 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"} 1523File 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
@ResponseSchemadecorator 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, CREDENTIALSBase 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 +@RegisterPermissionpermissionsgetModulePermissions()— 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-docsIConfig 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İ
