Skip to content

Decorators Reference ​

KickJS provides 22 decorators across @forinda/kickjs and @forinda/kickjs-swagger. This page is a complete reference for all of them.

Class Decorators ​

@Controller() ​

Marks a class as an HTTP controller and registers it in the DI container. Takes no arguments — route prefixes are defined by the module's routes().path, the single source of truth for where routes mount. The legacy @Controller('/path') form was removed in v4 — passing a path is now a TypeScript error; see Migration v3 → v4 for the rename map.

ts
import { Controller } from '@forinda/kickjs'

@Controller()
class UserController { ... }

@Service(options?) ​

Marks a class as a service (singleton by default).

ts
import { Service, Scope } from '@forinda/kickjs'

@Service()
class UserService { ... }

@Service({ scope: Scope.TRANSIENT })  // new instance per resolution
class RequestScopedService { ... }

@Injectable(options?) ​

Generic version of @Service. Same behavior, different semantic meaning.

@Component(options?) ​

Alias for @Injectable. Use for classes that don't fit the "service" or "repository" naming.

@Repository(options?) ​

Semantic alias for @Injectable. Use for data access classes.

ts
@Repository()
class UserRepository {
  async findById(id: string) { ... }
}

Method Decorators — HTTP Routes ​

@Get(path?, validation?) ​

@Post(path?, validation?) ​

@Put(path?, validation?) ​

@Delete(path?, validation?) ​

@Patch(path?, validation?) ​

Map a controller method to an HTTP route. Optionally pass Zod schemas for request validation.

ts
import { Controller, Get, Post, Put, Delete, Patch } from '@forinda/kickjs'
import { z } from 'zod'

const createUserSchema = z.object({ name: z.string(), email: z.string().email() })
const updateUserSchema = createUserSchema.partial()

@Controller()
class UserController {
  @Get('/')
  async list(ctx: RequestContext) { ... }

  @Get('/:id')
  async getById(ctx: RequestContext) { ... }

  @Post('/', { body: createUserSchema })
  async create(ctx: RequestContext) { ... }  // ctx.body is validated

  @Put('/:id', { body: updateUserSchema })
  async update(ctx: RequestContext) { ... }

  @Patch('/:id', { body: updateUserSchema })
  async patch(ctx: RequestContext) { ... }

  @Delete('/:id')
  async remove(ctx: RequestContext) { ... }
}

Validation options:

KeyTypeDescription
bodyZod schemaValidate req.body
queryZod schemaValidate req.query
paramsZod schemaValidate req.params

Method Decorators — Lifecycle & Behavior ​

@PostConstruct() ​

Marks a method to be called after the class is instantiated and dependencies are injected.

ts
@Service()
class CacheService {
  @PostConstruct()
  async warmup() {
    await this.loadFromRedis()
  }
}

@PreDestroy() ​

The teardown counterpart. For REQUEST-scoped services it runs when the request's scope closes (response finished or aborted) — release transactions, handles, and subscriptions there. Async hooks are fired without blocking the response; errors are logged and swallowed.

ts
@Service({ scope: Scope.REQUEST })
class TxService {
  @PreDestroy()
  async close() {
    await this.tx.rollbackIfOpen()
  }
}

Method & Class Decorators ​

Route flags — defineRouteFlag(name) ​

A flag is a decorator you define, recording a fact about the route that other things read — auth, CSRF, rate limiting, the OpenAPI spec:

ts
import { defineRouteFlag } from '@forinda/kickjs'

export const Public = defineRouteFlag('auth.public')
export const RateLimit = defineRouteFlag<{ rpm: number }>('rate.limit')
ts
@Public // class level — every route below inherits it
@Controller()
class WebhooksController {
  @Get('/health') health(ctx: RequestContext) {}

  @Public.off // this one opts back in
  @RateLimit({ rpm: 10 }) // flags can carry a value
  @Post('/admin')
  admin(ctx: RequestContext) {}
}

Read them anywhere a route has been matched — a guard, @Middleware(), a contributor, the handler:

ts
if (ctx.route?.flags.has('auth.public')) return next()

ctx.route is optional because it is undefined before route matching: global middleware runs too early to have one. That is about ctx.route, not about flags — pre-match middleware can still read a route's flags through the boot-built policy table, which is how rateLimit({ exemptWhen }) works.

Or let a consumer do the reading: contributors take skipWhen / onlyWhen, and the ctx-style guards take exemptWhen. Full surface in Route Flags.

@Middleware(...handlers) ​

Attach middleware to a class (all routes) or a specific method. Handlers receive (ctx: RequestContext, next: () => void).

ts
import { Controller, Get, Middleware } from '@forinda/kickjs'

// Class-level — applies to all routes
@Controller()
@Middleware(authMiddleware, roleMiddleware('admin'))
class AdminController {
  @Get('/')
  async dashboard(ctx: RequestContext) { ... }

  // Method-level — applies only to this route
  @Get('/danger')
  @Middleware(rateLimitMiddleware)
  async dangerousAction(ctx: RequestContext) { ... }
}

@FileUpload(config) ​

Configures file upload handling. The router builder auto-attaches the upload middleware from this metadata — no need for @Middleware(upload.single(...)).

ts
import { Controller, Post, FileUpload } from '@forinda/kickjs'

@Controller()
class FileController {
  @Post('/avatar')
  @FileUpload({
    mode: 'single',
    fieldName: 'avatar',
    maxSize: 5 * 1024 * 1024,
    allowedTypes: ['jpg', 'png', 'webp'],
  })
  async uploadAvatar(ctx: RequestContext) {
    ctx.json({ file: ctx.file.originalname })
  }

  @Post('/documents')
  @FileUpload({
    mode: 'array',
    fieldName: 'docs',
    maxCount: 10,
    allowedTypes: (mime) => mime === 'application/pdf',
  })
  async uploadDocs(ctx: RequestContext) {
    ctx.json({ count: ctx.files?.length })
  }
}
OptionTypeDefaultDescription
mode'single' | 'array' | 'none'requiredUpload mode
fieldNamestring'file'Form field name
maxCountnumber10Max files (array mode)
maxSizenumber5MBMax file size in bytes
allowedTypesstring[] | functionallAccepts extensions, MIME types, wildcards, or filter function
customMimeMapRecord<string, string>—Extend built-in MIME map

Injection Decorators ​

@Autowired(token?) and @Inject(token?) ​

Inject a dependency by inferred type or explicit token. The two names are interchangeable — same runtime, same types. Pick whichever reads better at the call site. Each works in two positions:

ts
@Controller()
class UserController {
  // Property position — both names work.
  @Autowired() private userService!: UserService // resolved by type
  @Inject(CACHE_TOKEN) private cache!: CacheService // resolved by token

  // Constructor-parameter position — both names work.
  constructor(
    @Inject(MAILER_TOKEN) private mailer: Mailer,
    @Autowired() private logger: Logger,
  ) {}
}

Property-position injections resolve lazily on first access. Constructor-position injections resolve at instantiation. The no-token form (@Autowired() / @Inject()) relies on TypeScript's emitDecoratorMetadata to resolve by the property's declared type or the constructor parameter's type.

@Value(envKey, defaultValue?) ​

Inject an environment variable value. Property-position only. Evaluated lazily at access time.

ts
@Service()
class EmailService {
  @Value('SMTP_HOST') private host!: string
  @Value('SMTP_PORT', 587) private port!: number
}

Class Decorator — Utility ​

@Builder ​

Adds a static builder() method for fluent construction. KickJS uses TypeScript's legacy decorators (experimentalDecorators: true), which cannot widen a class type from a decorator return value, so the runtime side effect and the type opt-in are decoupled. Pick whichever ergonomics you prefer — the runtime is identical.

Decorator form (with type opt-in) ​

ts
import { Builder, type BuilderOf } from '@forinda/kickjs'

@Builder
class UserDto {
  name!: string
  email!: string
  role!: string

  declare static readonly builder: () => BuilderOf<UserDto>
}

const user = UserDto.builder().name('Alice').email('alice@example.com').role('admin').build()
//                            ^? (value: string) => BuilderOf<UserDto>

The declare static readonly line is the one-time opt-in that exposes the chainable setters and .build() to TypeScript. Without it the runtime still works, but you get no autocomplete. readonly silences SonarQube's typescript:S1444 — the runtime assigns target.builder once at decoration time and never reassigns it.

Factory form (zero boilerplate) ​

withBuilder() runs the same runtime under the hood and returns the class intersected with the typed builder() static, so no declare line is needed:

ts
import { withBuilder } from '@forinda/kickjs'

class UserDtoBase {
  name!: string
  email!: string
  role!: string
}

export const UserDto = withBuilder(UserDtoBase)
export type UserDto = InstanceType<typeof UserDto>

const user = UserDto.builder().name('Alice').email('a@b.com').role('admin').build()

Use the decorator form when you want to keep one class declaration; use the factory when you'd rather not maintain the declare static line.

BuilderOf<T> shape ​

ts
type BuilderOf<T> = { [K in keyof T]-?: (value: T[K]) => BuilderOf<T> } & {
  build(): T
}

Swagger Decorators ​

These are from @forinda/kickjs-swagger and generate OpenAPI documentation from your controllers.

@ApiOperation(options) ​

Describe an endpoint for the OpenAPI spec.

ts
import { ApiOperation } from '@forinda/kickjs-swagger'

@Get('/')
@ApiOperation({ summary: 'List all users', description: 'Returns paginated user list' })
async list(ctx: RequestContext) { ... }

@ApiResponse(options) ​

Document a response status code and schema.

ts
import { ApiResponse } from '@forinda/kickjs-swagger'

@Post('/')
@ApiResponse({ status: 201, description: 'User created', schema: userSchema })
@ApiResponse({ status: 400, description: 'Validation failed' })
async create(ctx: RequestContext) { ... }

@ApiTags(...tags) ​

Group endpoints under tags in the Swagger UI. Works on classes or methods.

ts
@Controller()
@ApiTags('Users')
class UserController { ... }

@ApiBearerAuth(name?) ​

Mark endpoints as requiring Bearer token authentication. Convenience over @ApiSecurity('BearerAuth') — also auto-synthesizes a bearer-shaped scheme under the given name (defaults to 'BearerAuth').

ts
@Controller()
@ApiBearerAuth()
class AdminController { ... }

@Controller()
class CustomAuthController {
  @Get('/')
  @ApiBearerAuth('ApiKeyAuth') // custom scheme name; still bearer-shaped
  list() {}
}

@ApiSecurity(requirement) ​

Generic security decorator. Pick this when the scheme isn't bearer-shaped (API key, OAuth2 with scopes, OpenID Connect) or when a route accepts multiple alternative schemes. Class-level cascades to every method; method-level overrides win.

ts
import { ApiSecurity } from '@forinda/kickjs-swagger'

@Controller()
@ApiSecurity('BearerAuth') // class-level default
class UsersController {
  @Get('/me')
  @ApiSecurity({ name: 'OAuth2', scopes: ['users:read'] }) // override + scopes
  me() {}

  @Get('/multi')
  @ApiSecurity(['BearerAuth', { name: 'ApiKey' }]) // alternatives
  multi() {}
}

Three input shapes: a string (single scheme, no scopes), an object ({ name, scopes? } for OAuth2 / OIDC scopes), or an array of either (multiple alternative schemes). Custom scheme names other than 'BearerAuth' must be declared under SwaggerOptions.securitySchemes — see the Swagger guide.

@ApiPublic() ​

Mark a single method as publicly accessible — opts out of any class-level security requirement (set via @ApiSecurity or @ApiBearerAuth).

ts
@Controller()
@ApiSecurity('BearerAuth')
class InternalController {
  @Get('/health')
  @ApiPublic() // overrides class-level BearerAuth
  health() {}
}

Use when a mostly-secured controller exposes a health-check / login / public-stats endpoint that shouldn't carry the inherited security requirement.

@ApiExclude() ​

Hide a controller or method from the generated OpenAPI spec.

ts
@Get('/internal')
@ApiExclude()
async internalEndpoint(ctx: RequestContext) { ... }

@ApiQueryParams(config) ​

Declares the filterable, sortable, and searchable query parameters for an endpoint. This decorator lives in @forinda/kickjs and works with both the query parser and the Swagger spec generator. When @forinda/kickjs-swagger is installed, the declared fields are automatically added as OpenAPI query parameters.

Accepts both string-based configs and column-object configs (e.g., ColumnQueryFieldConfig):

ts
import { Controller, Get, ApiQueryParams } from '@forinda/kickjs'
import { RequestContext } from '@forinda/kickjs'

// String-based config
@Controller()
class TaskController {
  @Get('/')
  @ApiQueryParams({
    filterable: ['status', 'priority', 'assigneeId'],
    sortable: ['createdAt', 'title', 'priority'],
    searchable: ['title', 'description'],
  })
  async list(ctx: RequestContext) {
    return ctx.paginate((parsed) => this.taskService.findPaginated(parsed), {
      filterable: ['status', 'priority', 'assigneeId'],
      sortable: ['createdAt', 'title'],
    })
  }
}

With column-object config — pass a ColumnQueryFieldConfig directly:

ts
import { TASK_QUERY_CONFIG } from '../constants'

@Controller()
class TaskController {
  @Get('/')
  @ApiQueryParams(TASK_QUERY_CONFIG) // Column objects → field names extracted automatically
  async list(ctx: RequestContext) {
    return ctx.paginate((parsed) => this.taskService.findPaginated(parsed), TASK_QUERY_CONFIG)
  }
}
String-based optionColumn-based optionDescription
filterable: string[]columns: Record<string, Column>Fields that clients can filter on via ?filter=field:op:value
sortable: string[]sortable: Record<string, Column>Fields that clients can sort by via ?sort=field:direction
searchable: string[]searchColumns: Column[]Fields included in free-text ?q= search

Summary Table ​

DecoratorTargetPackagePurpose
@ControllerClasscoreMark class as HTTP controller (prefix from module)
@ServiceClasscoreDI-registered service
@InjectableClasscoreGeneric DI registration
@ComponentClasscoreAlias for Injectable
@RepositoryClasscoreData access class
@Get/Post/Put/Delete/PatchMethodcoreHTTP route handler
@PostConstructMethodcorePost-instantiation hook
@PreDestroyMethodcoreTeardown hook (request-scope close)
@MiddlewareClass/MethodcoreAttach middleware
flags via defineRouteFlagClass/MethodcorePer-route facts read by guards, contributors, docs
@FileUploadMethodcoreConfigure file upload
@AutowiredProperty / ParametercoreDependency injection — either position works
@ValuePropertycoreEnv variable injection
@InjectProperty / ParametercoreDependency injection — alias for @Autowired
@BuilderClasscoreFluent builder via static builder() (opt-in type)
withBuilder() (factory)ClasscoreSame runtime as @Builder with inferred typing
@ApiOperationMethodswaggerOpenAPI operation
@ApiResponseMethodswaggerOpenAPI response
@ApiTagsClass/MethodswaggerOpenAPI tags
@ApiBearerAuthClass/MethodswaggerBearer-token scheme (auto-synthesized)
@ApiSecurityClass/MethodswaggerGeneric security requirement (any scheme + scopes)
@ApiPublicMethodswaggerOpt-out from class-level security
@ApiQueryParamsMethodcoreDeclare filterable/sortable/searchable query fields
@ApiExcludeClass/MethodswaggerHide from spec

See also ​

  • Context Decorators — the typed defineContextDecorator() primitive for populating ctx.set/get keys before the handler. Use this instead of @Middleware() when the only job is to compute a value other code reads off ctx.
  • Custom Decorators — patterns for authoring your own decorators using reflect-metadata.

Released under the MIT License. Built with TypeScript — runs on Express, Fastify, or h3.