Documentation/titan/Decorators
In this section

Decorators

Titan's decorators come from two locations:

Import pathSurface
@omnitron-dev/titanCore set: Service, Injectable, Inject, Optional, Singleton, Transient, Module, PostConstruct, PreDestroy
@omnitron-dev/titan/decoratorsFull set: all of the above plus Public, Auth, RateLimit, Cache, Scoped, Request, Global, Memoize, Retry, Deprecated, Validate, Contract, NoValidation, ValidateInput, ValidateOutput, contract, Timeout, Retryable, Log, Monitor

For day-to-day code, import the core set from the main entry. Reach for the /decorators subpath when you need the extras.

Class-level

@Module(options)

Marks a class as a module. Without it, the class is invisible to the module registry.

@Module({
  imports:   [LoggerModule, ConfigModule],
  providers: [UsersService, UsersRepository],
  exports:   [UsersService],
})
export class UsersModule {}

→ Defining Modules

@Injectable(options?)

Marks a class as DI-resolvable. The container can construct it; its constructor parameters are resolved as dependencies.

@Injectable()
export class UsersRepository {
  constructor(private readonly db: Database) {}
}

@Service implies @Injectable(). Use @Injectable() for non-service helpers (repositories, mappers, internal utilities).

→ Providers

@Service(options?)

Marks a class as a Netron RPC service.

// String form — name + optional version
@Service('users@1.0.0')
class UsersService {}

// Object form
@Service({ name: 'users', version: '1.0.0' })
class UsersService {}

// Just a name (version becomes empty / unversioned)
@Service('users')
class UsersService {}

// Defaults — derived from class name
@Service()
class UsersService {}

The qualified identifier is name@version. The name must match /^[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)*$/ — Latin letters, numbers, and dots only (dots for namespacing). The version must be valid semver if provided.

@Service implies @Injectable().

→ Netron Services

Scope shorthands

import { Singleton, Transient, Scoped, Request } from '@omnitron-dev/titan/decorators';

@Singleton()
class CacheService {}

@Transient()
class RequestBuilder {}

@Request()
class RequestContext {}

Singleton and Transient are also re-exported from the main entry; Scoped and Request live in /decorators.

→ Scopes

@Global()

Marks a module as global — its exports are visible to every module in the application without explicit imports.

@Global()
@Module({
  providers: [LoggerService],
  exports:   [LoggerService],
})
class LoggerModule {}

Use sparingly. Reserved for framework infrastructure.

Method-level — RPC

@Public(options?)

Exposes a method (or property) over Netron. The options object can carry auth, rateLimit, cache, audit, prefetch, transports, readonly configuration. Methods without @Public are visible to the process but not RPC-callable.

@Service('users@1.0.0')
class UsersService {
  // Plain exposure.
  @Public()
  async findById(id: string) { /* … */ }

  // Inline configuration of everything @Public knows about.
  @Public({
    auth:      { roles: ['admin'] },
    rateLimit: { limit: 100, window: 60_000 },
    cache:     { ttl: 30_000 },
    transports: ['ws', 'tcp'],
  })
  async heavyMethod(query: string) { /* … */ }

  private internalHelper() { /* not exposed */ }
}

@Public is opt-in. The default is internal — methods that look fine inside the process should not silently become RPC-callable without an explicit decision.

A member's transports list narrows the service's own list; it cannot widen it. Omitted means unrestricted and [] denies all remote transports. The same rule covers methods and properties, HTTP and persistent transports. See transport boundaries. @Public({ transports: [...] }) and a separate @Auth(...) compose in either decorator order; transport options never erase the authorization requirements.

@Auth(config)

Configures authentication and authorisation for a method. Use alongside @Public() for cleaner separation of concerns. The shape of AuthConfig:

@Public()
@Auth({
  roles:       ['admin'],          // ANY role grants access
  permissions: ['users:read'],     // ALL permissions required
  scopes:      ['user.read'],      // ALL OAuth2 scopes required
  policies:    ['policy.name'],    // or { all: [...] } / { any: [...] } / expression
  allowAnonymous: false,
  inherit:        true,
  override:       false,
})
async findById(id: string) { /* … */ }

The policy framework (see BuiltInPolicies in @omnitron-dev/titan/netron/auth) provides reusable requireRole, requireAnyRole, requireAllRoles, requirePermission, and similar policies.

→ Netron Authentication

@RateLimit(config)

Configures rate limiting. The basic shape:

@Public()
@RateLimit({ limit: 100, window: 60_000 })   // 100 requests per minute
async searchDocuments(query: string) { /* … */ }

Advanced (tier-based) shape:

@Public()
@RateLimit({
  defaultTier: { name: 'free',    limit: 10,  burst: 20  },
  tiers:       { premium:        { limit: 100, burst: 150 } },
  window:      60_000,
})
async searchDocuments(query: string) { /* … */ }

The actual RateLimitConfig lives in @omnitron-dev/titan/netron/auth/rate-limiter.ts.

This decorator RECORDS the limit: it writes metadata that Netron's authorization layer reads. A second decorator of the same name, exported from @omnitron-dev/titan-ratelimit, WRAPS the method and enforces the limit itself. They are told apart by the import path and by their options — window here, windowMs there — so a stack that mixes them compiles and does something other than what it reads like.

→ titan-ratelimit

@Cache(config)

Caches the method result. The shape:

@Public()
@Cache({
  ttl:          30_000,                          // ms
  keyGenerator: (args) => `user:${args[0]}`,     // optional; default uses args hash
  invalidateOn: ['user:updated'],                // events that bust this cache
  maxSize:      1_000,
})
async findById(id: string) { /* … */ }

→ titan-cache

@Validate(schema, options?) and @Contract(contract)

Validate input (and optionally output) against Zod schemas.

import { z } from '@omnitron-dev/titan/validation';
import { Validate, ValidateInput, Contract } from '@omnitron-dev/titan/decorators';

@Public()
@Validate({ input: CreateUserSchema })
async create(input: z.infer<typeof CreateUserSchema>) {
  // input is parsed and trusted here.
}

// Input-only shorthand — equivalent to @Validate({ input: schema })
@Public()
@ValidateInput(CreateUserSchema)
async createAlt(input: z.infer<typeof CreateUserSchema>) { /* … */ }

@Public()
@Contract(FindByIdContract)
async findById(input: { id: string }) { /* input + output validated */ }

@Validate is a method decorator. It takes a MethodContract options object ({ input?, output?, errors?, stream?, options? }) of Zod schemas — not a bare schema, and not a parameter decorator.

Other validation decorators: ValidateInput, ValidateOutput, NoValidation, Contracts (class-level), contract (builder).

→ Validation

Method-level — utilities

@Memoize()

Per-instance memoisation of method results, keyed by JSON.stringify of arguments. In-memory only; lives as long as the instance.

@Memoize()
private computeHash(input: string) { /* … */ }

@Retry({ attempts?, delay? })

Retries the method on failure with a fixed delay between attempts. attempts defaults to 3, delay to 1000 ms.

@Retry({ attempts: 3, delay: 200 })
async fetchUpstream() { /* … */ }

For exponential backoff and classifier-driven decisions, use the computeBackoff helper in @omnitron-dev/titan/utils with your own retry loop. See Resilience / Retry.

@Deprecated({ message?, version? })

Marks the method as deprecated. Metadata only; the framework does not log warnings.

@Deprecated({ message: 'Use findById instead', version: '2.0.0' })
async getUser(id: string) { /* … */ }

@Timeout({ ms }), @Retryable(...), @Log({...}), @Monitor({...})

Additional utility interceptors in @omnitron-dev/titan/decorators/utility.ts. Less commonly used; see the source for current signatures.

Parameter-level

@Inject(token)

Specifies the DI token for this parameter. Required for symbol tokens and multi-tokens; optional for class tokens (auto-detected from constructor metadata).

constructor(
  @Inject(LOGGER_TOKEN) private readonly logger: ILogger,
  private readonly db: Database,                            // class token; @Inject not needed
) {}

@Optional()

Makes the dependency optional. If no provider is registered, the parameter receives undefined.

constructor(
  @Optional() @Inject(METRICS_TOKEN) private readonly metrics?: IMetrics,
) {}

@InjectAll(token), @InjectMany(token)

Inject all providers registered against a multi-token.

@Value(path, default?), @InjectEnv(key, default?), @InjectConfig(path)

Inject from configuration sources. @Value resolves through a config tree; @InjectEnv reads process.env; @InjectConfig reads through ConfigService (see Configuration).

@Lazy(tokenFactory)

Inject a token resolved lazily on first use. Used to break circular dependencies.

→ Circular Dependencies

Lifecycle decorators

@PostConstruct(), @PreDestroy()

Mark methods that run during the onInit / onDestroy lifecycle phases.

@PostConstruct()
async warm() { /* … */ }

@PreDestroy()
async drain() { /* … */ }

Equivalent to implementing OnInit / OnDestroy interfaces. See Lifecycle Decorators.

Decorator order

When stacking decorators, outermost-first in declaration order; execution at call time is inside-out:

@Public()                                              //  ← outer
@Auth({ roles: ['admin'] })                            //
@RateLimit({ limit: 10, window: 60_000 })              //
@Validate({ input: InputSchema })                      //  ← inner, runs first
async dangerousOp(input: Input) { /* … */ }

→ See Method Traits for stacking guidance.

Anti-patterns

  • Decorating private methods with @Public. Private methods are not meant to be called from outside; making them RPC-callable is a security risk.

  • Inline configuration vs decorators. @Public({ auth: …, rateLimit: …, cache: … }) is equivalent to stacking @Public() @Auth(…) @RateLimit(…) @Cache(…). Pick one style per project and stick to it.

  • Using @Memoize for cross-instance caching. Memoisation is per-instance. For shared caching across calls and clients, use @Cache.

  • Decorating constructors. Decorators on constructors do nothing in Titan — the framework reads class-level decorators, not constructor-level. Move the metadata to the class.