Skip to main content

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.

@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.

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, Contract } from '@omnitron-dev/titan/decorators';

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

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

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(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.

→ Next: Lifecycle Decorators.