Decorators
ExisJS provides ECMAScript and TypeScript decorators for building backend applications in the Object-Oriented Programming (OOP) paradigm.
Decorators attach metadata to classes and methods using JavaScript Symbol keys. They do not depend on external reflection packages such as reflect-metadata.
Import decorators directly from exisjs/decorators:
import { Controller, Get, Post, Put, Patch, Delete, Body, Param, Query, Headers, Injectable, Use, UseGuards, UsePipes, ParseIntPipe, Cron, Interval, Cacheable, Transactional} from 'exisjs/decorators'1. Application & Boundary Decorators
@Server(options)
Configures root providers, plugins, cron schedules, and lifecycle hooks on an application entry class:
import { Server } from 'exisjs/decorators'import type { App } from 'exisjs'
@Server({ providers: [['LOGGER_TOKEN', { useValue: console }]], plugins: []})export default class RootServer { async onStart(app: App) { console.log('Server started') }
async onClose(app: App) { console.log('Server stopping') }}@Boundary(options)
Configures directory-scoped middleware chains, CORS policies, response headers, and route exclusions inside boundary.ts:
import { Boundary } from 'exisjs/decorators'import type { Request, Response, Next, BoundaryContext } from 'exisjs/router'import { UnauthorizedError } from 'exisjs/error'
@Boundary({ cors: { origin: 'https://admin.example.com', credentials: true }, headers: { 'X-Admin-Api-Version': '2.0' }, exclude: ['/admin/health']})export default class AdminBoundary { // Middleware handler identified by (req, res, next) signature requireApiKey(req: Request, res: Response, next: Next) { if (!req.headers['x-api-key']) { throw new UnauthorizedError('Missing admin API key') } next() }
// Request wrapper identified by handle(ctx, next) signature async handle(ctx: BoundaryContext, next: Next) { const start = Date.now() const result = await next() ctx.res.setHeader('X-Response-Time', `${Date.now() - start}ms`) return result }}2. Controller & Routing Decorators
@Controller(prefixOrOptions)
Registers a class as a route controller. Accepts a string path prefix or a configuration object:
import { Controller, Get, Post, Put, Patch, Delete } from 'exisjs/decorators'import { UserParamsSchema, CreateUserSchema } from './schema'
@Controller('/users')export default class UsersController { @Get('/') async listUsers() { return { users: [] } }
@Get('/:id', UserParamsSchema) async getUser() { return { id: 1, name: 'Alice' } }
@Post('/', CreateUserSchema) async createUser() { return { success: true } }
@Delete('/:id') async deleteUser() { return { deleted: true } }}Routing Methods Table
| Decorator | HTTP Method | Arguments | Description |
|---|---|---|---|
@Get(path?, schema?) | GET | path?: string, schema?: RouteSchema | Maps handler to GET requests. |
@Post(path?, schema?) | POST | path?: string, schema?: RouteSchema | Maps handler to POST requests. |
@Put(path?, schema?) | PUT | path?: string, schema?: RouteSchema | Maps handler to PUT requests. |
@Patch(path?, schema?) | PATCH | path?: string, schema?: RouteSchema | Maps handler to PATCH requests. |
@Delete(path?, schema?) | DELETE | path?: string, schema?: RouteSchema | Maps handler to DELETE requests. |
@Options(path?, schema?) | OPTIONS | path?: string, schema?: RouteSchema | Maps handler to OPTIONS requests. |
@Head(path?, schema?) | HEAD | path?: string, schema?: RouteSchema | Maps handler to HEAD requests. |
@All(path?, schema?) | Any Verb | path?: string, schema?: RouteSchema | Catches any HTTP verb matching the path. |
@Connect(path?, schema?) | CONNECT | path?: string, schema?: RouteSchema | Maps handler to CONNECT requests. |
@Trace(path?, schema?) | TRACE | path?: string, schema?: RouteSchema | Maps handler to TRACE requests. |
@Query(path?, schema?) | QUERY | path?: string, schema?: RouteSchema | Maps handler to RFC QUERY requests. |
3. Parameter Decorators
Parameter decorators extract values from the incoming request and pass them into method arguments:
import { Controller, Post, Body, Param, Query, Headers, Req, Res, Ip, Session, UploadedFile} from 'exisjs/decorators'import type { Request, Response, ExisFile } from 'exisjs'
@Controller('/users')export default class UsersController { @Post('/:id/avatar') async uploadAvatar( @Param('id') id: string, @Query('quality') quality: string, @Body() metadata: any, @Headers('authorization') token: string, @UploadedFile() file: ExisFile, @Ip() clientIp: string, @Req() req: Request, @Res({ passthrough: true }) res: Response ) { return { id, quality, metadata, clientIp, file: file?.filename } }}Parameter Decorators Reference
| Decorator | Injected Value | Example Usage |
|---|---|---|
@Body(fieldOrPipe?) | Request body or specific property | @Body() dto: CreateUserDto, @Body('email') email: string |
@Param(nameOrPipe?) | Path parameter string | @Param('id', ParseIntPipe) id: number |
@Query(nameOrPipe?) / @QueryParam(...) | Parsed query parameter or query map | @Query('search') search: string |
@Headers(nameOrPipe?) | Request header value (case-insensitive) | @Headers('x-api-key') apiKey: string |
@Cookies(nameOrPipe?) / @Cookie(...) | Parsed cookie map or specific cookie string | @Cookie('session_id') sessionId: string |
@Req() | The incoming ExisRequest instance | @Req() req: Request |
@Res(options?) | The outgoing ExisResponse instance ({ passthrough?: boolean }) | @Res({ passthrough: true }) res: Response |
@Ip() | Client IP address string (reads trustProxy setting) | @Ip() ip: string |
@Session() | Active session state object | @Session() session: any |
@Next() | The next() pipeline callback | @Next() next: Next |
@State(keyOrPipe?) | Request context state | @State('user') user: AuthUser |
@UploadedFile(nameOrPipe?) | Single parsed file object | @UploadedFile('avatar') file: ExisFile |
@UploadedFiles(nameOrPipe?) | Array of parsed file objects | @UploadedFiles('photos') files: ExisFile[] |
@Fields(nameOrPipe?) | Non-file multipart form fields | @Fields() fields: Record<string, any> |
@HostParam(nameOrPipe?) | Subdomain or host parameter | @HostParam('subdomain') domain: string |
@AppCtx() | Root Application instance | @AppCtx() app: App |
Custom Parameter Decorators (createParamDecorator)
import { createParamDecorator } from 'exisjs/decorators'
export const CurrentUser = createParamDecorator((data: string | undefined, ctx) => { return data ? ctx.req.user?.[data] : ctx.req.user})4. Transformation & Validation Pipes
Pipes transform parameter inputs or validate input types:
import { Controller, Get, Param, Query, UsePipes, ParseIntPipe, ParseFloatPipe, ParseBoolPipe, ParseUUIDPipe, ParseArrayPipe, ValidationPipe} from 'exisjs/decorators'
@Controller('/users')@UsePipes(new ValidationPipe({ transform: true }))export default class UsersController { @Get('/:id') async getUser(@Param('id', ParseIntPipe) id: number) { return { id, isNumber: typeof id === 'number' } }
@Get('/filter') async filter( @Query('page', ParseIntPipe) page: number, @Query('active', ParseBoolPipe) active: boolean, @Query('tags', new ParseArrayPipe({ separator: ',' })) tags: string[] ) { return { page, active, tags } }
@Get('/uuid/:uuid') async getByUuid(@Param('uuid', new ParseUUIDPipe({ version: '4' })) uuid: string) { return { uuid } }}Pipes Reference
| Pipe Class | Description | Options |
|---|---|---|
ParseIntPipe | Converts input string or number to integer using Math.trunc | { errorHttpStatusCode?: number, exceptionFactory?: (msg) => any } |
ParseFloatPipe | Converts input string or number to floating point | { errorHttpStatusCode?: number, exceptionFactory?: (msg) => any } |
ParseBoolPipe | Converts 'true', '1', true to true, 'false', '0', false to false | { errorHttpStatusCode?: number, exceptionFactory?: (msg) => any } |
ParseUUIDPipe | Validates UUID strings | { version?: '3' | '4' | '5' | 'all', errorHttpStatusCode?: number } |
ParseArrayPipe | Parses comma-separated strings or arrays with sub-item transformation | { items?: any, separator?: string, optional?: boolean } |
ValidationPipe | Validates DTO classes or schemas implementing validate() or Standard Schema | { transform?: boolean, whitelist?: boolean, errorHttpStatusCode?: number } |
@UsePipes(...pipes) | Attaches one or more pipes to a controller class or method handler | Array of Pipe instances or Pipe classes |
5. Middleware, Guards & Lifecycle Pipeline Decorators
Attach guards, interceptors, exception filters, timeouts, and traffic policies to classes or route methods:
import { Controller, Post, UseGuards, UseInterceptors, UseFilters, RateLimit, Timeout, Idempotent, Cors, Compress, Dedupe, IpFilter, BlockProbes} from 'exisjs/decorators'import { AuthGuard } from '../../common/guards/auth.guard'import { LoggingInterceptor } from '../../common/interceptors/logging'import { HttpExceptionFilter } from '../../common/filters/http.filter'
@Controller('/checkout')@UseGuards(AuthGuard)@UseInterceptors(LoggingInterceptor)@UseFilters(HttpExceptionFilter)@Cors({ origin: 'https://pay.example.com', credentials: true })@RateLimit({ max: 20, windowMs: 60000 })@IpFilter({ allow: ['10.0.0.0/8', '127.0.0.1'] })@BlockProbes()export default class CheckoutController { @Post('/') @Timeout(15000) @Idempotent({ ttlMs: 86400000 }) @Dedupe({ keyGenerator: (req) => req.user?.id ?? req.ip }) @Compress() async processPayment() { return { status: 'processed' } }}Traffic & Middleware Decorators
| Decorator | Scope | Description |
|---|---|---|
@Use(...items) | Class / Method | Attaches middlewares, guards (canActivate), interceptors (intercept), or filters (catch). |
@UseGuards(...guards) | Class / Method | Attaches route guards implementing the CanActivate interface. |
@UseInterceptors(...interceptors) | Class / Method | Attaches request/response interceptors implementing Interceptor. |
@UseFilters(...filters) | Class / Method | Attaches exception filters implementing ExceptionFilter. |
@Catch(...exceptions) | Class | Declares the exception types handled by an ExceptionFilter class. |
@Timeout(msOrOptions) | Class / Method | Sets request execution timeout in milliseconds. |
@Idempotent(options?) | Method / Class | Caches responses matching the Idempotency-Key header in an LRU memory store. |
@RateLimit(options?) | Class / Method | Enforces rate limits using a token bucket algorithm. |
@Cors(options?) | Class / Method | Applies CORS headers to a specific controller or method. |
@IpFilter(options) | Class / Method | Restricts access using CIDR IP allowlists and denylists. |
@Compress() | Class / Method | Enables Gzip and Brotli response compression. |
@Dedupe(options) | Method / Class | Deduplicates parallel in-flight requests matching the same key. |
@BlockProbes(options?) / @BlockSuspiciousProbes() | Class / Method | Drops vulnerability scanner probes (.env, wp-admin, /.git). |
@Upload(fieldNameOrOptions) / @UseUpload(options) | Method / Parameter | Configures multipart file upload middleware. |
6. Security & RBAC Decorators
Declare role requirements, permission checks, and authentication bypasses:
import { Controller, Get, Post, Body, UseGuards, Roles, Permissions, Public} from 'exisjs/decorators'import { AuthGuard } from '../../common/guards/auth.guard'
@Controller('/roles')@UseGuards(AuthGuard)export default class RolesController { @Get('/public') @Public() // Instructs AuthGuard to skip credential verification async getPublicInfo() { return { status: 'open' } }
@Get('/') @Roles('admin', 'security_officer') async listRoles() { return { roles: ['admin', 'manager', 'viewer'] } }
@Post('/') @Permissions('roles:create', 'roles:write') async createRole(@Body() body: any) { return { created: true } }}Security Decorators Reference
| Decorator | Target | Description |
|---|---|---|
@Roles(...roles: string[]) | Class / Method | Requires req.user to have one of the roles (user.roles or user.role). No user: 401; wrong role: 403. |
@Permissions(...perms: string[]) | Class / Method | Requires all listed permissions in req.user.permissions. A role is never counted as a permission. No user: 401; missing permission: 403. |
@Public() / @IsPublic() / @AllowAnonymous() | Class / Method | Marks route as unauthenticated. Wins over @Roles / @Permissions on the same route (a warning is logged at startup). |
7. Cron & Scheduled Background Tasks
Schedule recurring tasks directly on @Injectable() service classes:
import { Injectable, Cron, Interval, TimeoutTask } from 'exisjs/decorators'
@Injectable()export class CleanupService { // Executes daily at midnight UTC with overlap protection @Cron('0 0 * * *', { name: 'nightly-purge', timezone: 'UTC', preventOverlap: true }) async purgeOldSessions() { console.log('Purging expired sessions') }
// Executes every 60 seconds @Interval(60_000, { name: 'health-heartbeat' }) async pingHeartbeat() { console.log('Heartbeat ping') }
// Executes once 5 seconds after server startup @TimeoutTask(5_000, { name: 'cache-warmup' }) async warmupCaches() { console.log('Caches warmed up') }}Cron Decorators Table
| Decorator | Schedule Parameter | Description |
|---|---|---|
@Cron(schedule, options?) / @Scheduled(...) | 5-field cron expression string | Schedules a recurring cron task. |
@Interval(ms, options?) | Milliseconds interval | Runs repeatedly at the specified interval. |
@TimeoutTask(ms, options?) / @ScheduledTimeout(...) | Milliseconds delay | Runs once after a delay following server start. |
8. Service Enhancements: Caching, Transactions & Profiling
import { Injectable, Cacheable, CacheEvict, Transactional, Profile, type OnInit, type OnDestroy} from 'exisjs/decorators'
@Injectable()export class OrderService implements OnInit, OnDestroy { async onInit() { console.log('OrderService initialized') }
async onDestroy() { console.log('OrderService destroyed') }
// Caches result in memory for 300 seconds @Cacheable({ ttl: 300, key: (id: string) => `order:${id}` }) @Profile({ thresholdMs: 50, warnSlow: true }) async getOrder(id: string) { return this.db.query('SELECT * FROM orders WHERE id = $1', [id]) }
// Executes inside a database transaction and evicts the cached key @Transactional() @CacheEvict({ key: (id: string) => `order:${id}` }) async updateOrder(id: string, data: any) { return this.db.update('orders', id, data) }}Service & Lifecycle Reference
| Decorator / Interface | Target | Description |
|---|---|---|
@Cacheable(options?) | Method | Caches return value. Options: { ttl?: number, key?: string | ((...args) => string) }. |
@CacheEvict(options) | Method | Evicts cache keys on method completion. Options: { key: string | ((...args) => string) }. |
@Transactional(options?) | Method | Wraps execution in a database transaction (Prisma, Drizzle, Knex, SQL). Inside the call, the service's client property (db, database, prisma, dataSource or connection) is the transaction, including in methods it calls; concurrent requests using the same service keep the normal client. |
@Profile(options?) | Method | Measures execution time using performance.now(). |
OnInit (onInit()) | Class Interface | Invoked once after service instantiation. |
OnDestroy (onDestroy()) | Class Interface | Invoked once during graceful shutdown. |
OnModuleInit / OnModuleDestroy | Class Interface | Invoked during parent module initialization and destruction. |
OnApplicationBootstrap | Class Interface | Invoked when all modules are initialized and server starts listening. |
OnApplicationShutdown | Class Interface | onApplicationShutdown(signal?) runs during graceful shutdown; signal is 'SIGTERM' or 'SIGINT' when a signal triggered it. |
9. Response, Header & OpenAPI Decorators
Response & Host Modifiers
import { Controller, Post, Get, HttpCode, Header, Redirect, Hosts, Returns} from 'exisjs/decorators'import { ItemResponseSchema } from './schema'
@Controller('/items')@Hosts('api.example.com', 'internal.example.com')export default class ItemsController { @Post('/') @HttpCode(201) @Header('X-Resource-Version', '1.0') @Returns(ItemResponseSchema) async create() { return { created: true } }
@Get('/legacy') @Redirect('/items', 301) async legacyRedirect() {}}OpenAPI & Swagger Decorators
All OpenAPI decorators are available from exisjs/decorators:
import { Controller, Get, ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiSecurity, ApiExclude, ApiProperty} from 'exisjs/decorators'
export class UserDto { @ApiProperty({ description: 'User ID', example: 'usr_123', required: true }) id: string
@ApiProperty({ description: 'Full name', example: 'Alice Smith' }) name: string}
@ApiTags('Users')@ApiBearerAuth()@Controller('/users')export default class UsersController { @Get('/:id') @ApiOperation({ summary: 'Get User By ID', description: 'Returns a user record' }) @ApiResponse({ status: 200, description: 'User retrieved successfully' }) @ApiResponse({ status: 404, description: 'User not found' }) async getUser() { return { id: 'usr_123', name: 'Alice Smith' } }
@Get('/internal/health') @ApiExclude() async health() { return { ok: true } }}| Decorator | Scope | Description |
|---|---|---|
@ApiTags(...tags) / @Tags(...) | Class / Method | Assigns OpenAPI documentation tags. |
@ApiOperation(options) / @Operation(...) | Method | Configures summary, description, operationId, deprecated, externalDocs. |
@ApiResponse(options) | Method | Defines status code documentation (status, description, schema). |
@ApiBearerAuth(name?) / @BearerAuth(...) | Class / Method | Marks route as requiring Bearer JWT authentication in OpenAPI output. |
@ApiSecurity(name, scopes?) | Class / Method | Declares custom security scheme requirement. |
@ApiExclude() | Class / Method | Hides route or controller from OpenAPI / Swagger output. |
@ApiProperty(options) | Property | Annotates class DTO properties with description, examples, types, and enums. |
10. Dependency Injection & Modules
@Injectable(options?)
import { Injectable } from 'exisjs/decorators'
@Injectable({ scope: 'singleton' }) // 'singleton' | 'transient' | 'request'export class UserService { async findOne(id: string) { return { id, name: 'Alice' } }}@Inject(token) & @Optional()
import { Controller, Get, Param } from 'exisjs/decorators'import { Inject, Optional } from 'exisjs/di'import { UserService } from './service'
@Controller('/users')export default class UsersController { constructor( @Inject(UserService) private userService: UserService, @Inject('DATABASE_URL') private dbUrl: string, @Optional() @Inject('ANALYTICS_SERVICE') private analytics?: any ) {}
@Get('/:id') async getUser(@Param('id') id: string) { return this.userService.findOne(id) }}@Module(options) & @Global()
import { Module, Global } from 'exisjs/decorators'import UsersController from './route'import { UserService } from './service'
@Global()@Module({ controllers: [UsersController], providers: [UserService], exports: [UserService]})export class UsersModule {}11. JavaScript Runtime Utilities (Decorate & BindParam)
For JavaScript projects without a build step or TypeScript compiler, use Decorate and BindParam to apply decorator metadata programmatically:
import { Decorate, BindParam, Controller, Get, Post, Body, Param } from 'exisjs/decorators'
class UsersController { list() { return [] }
create(body) { return { created: body.name } }}
export default Decorate( UsersController, [Controller('/users')], { list: [Get('/')], create: [ Post('/'), BindParam(0, Body()) // Injects @Body() into the first argument (index 0) ] })