Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@master4n/decorators

Package Overview
Dependencies
Maintainers
1
Versions
22
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@master4n/decorators

AI-friendly TypeScript decorators for Node/backend apps, in ten families: Inject (config & value injection), Guard (validation), Shape (transforms), Shield (access control & secret redaction), Flow (resilience — retry/timeout/circuit-breaker/rate-limit/ca

Source
npmnpm
Version
2.0.3
Version published
Weekly downloads
243
-71%
Maintainers
1
Weekly downloads
 
Created
Source

@master4n/decorators

CI npm version npm downloads License Types Owner

AI-friendly TypeScript decorators for Node/backend apps. One self-documenting decorator replaces a block of boilerplate. Designed so coding agents emit one correct line instead of ten repetitive ones.

Decorator families

Every decorator belongs to one of ten families — a quick mental map for picking the right one:

FamilyPurposeExamples
Injectpull values into fields@Value @Env @Secret @Config @Default @Configured
Guardreject invalid input (throws)@NotNull @Pattern @Min @Max @Range @Email @URL @UUID @Enum @Size @NotBlank @Past @Future @AssertTrue @Digits
Shapenormalize values on assign@Trim @Lowercase @Uppercase @Coerce @Clamp
Shieldaccess control & secret redaction@Role @Authorize @Secret + redact()
Flowresilience & control flow@Timeout @Retry @Cache @Dedupe @Fallback @RateLimit @Concurrency @CircuitBreaker @Debounce @Throttle @Once
Insightobservability@Log @Trace @Audit @LogErrors @Measure @Deprecated
Modeldata/domain classes@Data @ToString @Equals @With @Immutable @Readonly @Builder @GenerateID @Counter @Synchronized
RouteHTTP REST controllers@Controller @Get @Post @Param @Query @Body @HttpCode @Use
AgentLLM tools & safety@Tool @Validate @Guardrail @Idempotent @Meter + getTools() / invokeTool() / getMetrics()
Craftclass & method ergonomics@Bind @Lazy @Sealed @Mixin @OnChange

Recipes for AI agents

Real, copy-paste compositions. Each replaces a page of hand-written plumbing with a stack of declarations.

A bullet-proof DTO (Shape + Guard)

import { Configured, Trim, Lowercase, Email, NotBlank, Size, Coerce, Range } from '@master4n/decorators';

@Configured
class SignupDto {
  @Trim @Lowercase @Email()         email!: string;   // normalized, then validated
  @Trim @NotBlank() @Size(3, 20)    username!: string;
  @Coerce('number') @Range(18, 120) age!: number;     // "21" -> 21, bounded
}
// Assigning a bad value throws ValidationError at the source — no manual checks.

A resilient upstream client (Flow)

import { Fallback, CircuitBreaker, Retry, Timeout, Cache } from '@master4n/decorators';

class UserApi {
  @Fallback(null)                // OUTERMOST = last resort: catch after everything else
  @CircuitBreaker({ failureThreshold: 5, resetMs: 30_000 })
  @Retry(3, { delayMs: 200 })    // retry the timed call
  @Timeout(5_000)
  @Cache(60_000)                 // INNERMOST: memoize the actual fetch
  async getUser(id: string) {
    return (await fetch(`/users/${id}`)).json();
  }
}

Decorator order matters. Stacks apply bottom-up: the decorator nearest the method wraps the original first, and the top decorator runs first / sees the final outcome. So put recovery (@Fallback) outermost (top) — otherwise it swallows the error before @Retry/@CircuitBreaker ever see a failure, and retries silently never happen.

A safe, observable AI tool (Agent + Insight)

import { Tool, Validate, Idempotent, Guardrail, Meter, Trace, getTools, invokeTool, getMetrics } from '@master4n/decorators';

class BookingTools {
  @Tool({
    description: 'Book a room for a guest',
    parameters: {
      type: 'object',
      properties: { guest: { type: 'string' }, nights: { type: 'number' } },
      required: ['guest', 'nights'],
    },
  })
  @Trace()                                            // correlation-id traced
  @Meter('book_room')                                 // counts + timing -> getMetrics()
  @Idempotent((args) => `${args.guest}:${args.nights}`) // safe to retry
  @Validate((args) => (args[0] as any)?.nights > 0)   // reject bad tool input
  @Guardrail((res: { confirmed: boolean }) => res.confirmed, { retries: 1 }) // verify output
  async bookRoom(args: { guest: string; nights: number }) {
    return { confirmed: true, ref: 'BK-123' };
  }
}

const svc = new BookingTools();
const tools = getTools();                       // -> hand to the LLM
// model picks a tool ...
await invokeTool(svc, 'bookRoom', { guest: 'Asha', nights: 2 });
getMetrics().book_room;                          // { calls, errors, avgMs, ... }

An immutable domain model (Model)

import { Data, Immutable, builder } from '@master4n/decorators';

@Immutable
@Data                                            // toString + equals + with
class Money { constructor(public amount = 0, public currency = 'INR') {} }

const a = new Money(100, 'INR');
const b = (a as any).with({ amount: 250 });      // frozen copy, original untouched
const c = builder(Money).amount(50).currency('USD').build();  // typed builder

Less boilerplate (Craft)

import { Configured, Bind, Lazy, OnChange } from '@master4n/decorators';

@Configured                                 // required for property decorators under modern TS
class Editor {
  @Lazy((self) => buildHeavyIndex(self))  index!: Index;   // computed once, on first read
  @OnChange((v) => autosave(v))            content = '';     // reacts to real changes
  @Bind                                    onClick() { return this.content; } // safe to detach
}

@Lazy and @OnChange are property decorators — like all of them, add @Configured to the class when you compile with useDefineForClassFields: true (the modern default), or they silently no-op (see TypeScript setup). @Bind is a method decorator and needs no @Configured.

Installation

npm install @master4n/decorators

The core has zero runtime dependencies. Two features are gated behind optional peer dependencies — install them only if you use those features:

npm i config    # for @Value / @Config from "@master4n/decorators/config"
npm i winston   # for redactFormat from "@master4n/decorators/winston"

TypeScript setup

This is a legacy-decorator library. A complete, known-good tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

The one rule that matters: put @Configured on any class that uses a property decorator (@Value/@Env/@Secret/@Config/@Default, every Guard like @Email/@Min/@Pattern, every Shape like @Trim, and Craft's @Lazy/@OnChange/@Readonly). With modern TS (useDefineForClassFields: true, the default for target >= ES2022) a class field shadows the decorator's prototype accessor, so without @Configured those decorators silently no-op — no error, just nothing happens. @Configured materializes them as own instance properties so they work under any setting. Method and class decorators (@Get, @Retry, @Bind, @Data, @Tool, …) don't need it.

Inject — config & value injection (flagship)

Stop hand-writing process.env.X ?? config.get(...) ?? default plus type coercion, for every field.

// BEFORE — written by hand (or by an agent) for every single field:
class DbConfig {
  url: string;
  port: number;
  ssl: boolean;
  constructor() {
    this.url = process.env.DB_URL ?? config.get('db.url') ?? 'sqlite://memory';
    const p = process.env.DB_PORT;
    this.port = p ? parseInt(p, 10) : 5432;            // string -> number
    this.ssl = (process.env.DB_SSL ?? 'false') === 'true'; // string -> boolean
  }
}
// AFTER — declarative, coerced, and fails loud on missing required keys:
import { Configured, Env, Secret } from '@master4n/decorators';
import { Value } from '@master4n/decorators/config'; // node-config-backed

@Configured
class DbConfig {
  @Value('db.url', 'sqlite://memory') url!: string;
  @Env('DB_PORT', 5432)               port!: number;   // "5432" -> 5432
  @Env('DB_SSL', false)               ssl!: boolean;    // "true" -> true
  @Secret('DB_PASSWORD')              password!: string; // required; tracked for redaction
}

Zero runtime dependencies. The main entry point pulls in no third-party packages. The two decorators that read config files — @Value/@Config — live in the @master4n/decorators/config subpath and require node-config as an optional peer dependency (npm i config). Everything else, including @Env/@Secret, is dependency-free.

Sources: @Value/@Config read YAML/JSON via node-config; @Env/@Secret read process.env. Node does not load .env files automatically — if you keep config in a .env file, load it first (node --env-file=.env ... or the dotenv package) so the variables are present in process.env.

Injection decorators

DecoratorSourceNotes
@Value(key, default?)config files (YAML/JSON)via node-config. Required (throws) when no default is given.
@Env(name, default?)process.envcoerces to the default's type (number/boolean/array).
@Secret(name, default?)process.envlike @Env; marks the field as secret so redact() / redactFormat() mask it (see Secret redaction).
@Config(path)config filesinjects a whole config subtree/object (required).
@Default(value)literalinjects a constant.
@Configuredclass decoratormaterializes the above as own instance props (robust mode).

Missing required values throw MissingConfigError. With @Configured, that happens at construction — so misconfiguration fails at startup, not deep in a request.

Secret redaction

@Secret doesn't just track names — it makes those values disappear from logs.

  • redact(value, options?) returns a deep copy with sensitive values masked ('[REDACTED]'). Sensitive = @Secret-marked property names ∪ a built-in list (DEFAULT_SENSITIVE_KEYS: password, token, apiKey, authorization, …) ∪ options.keys. Matching is case- and _/--insensitive and matches secret stems as substrings, so compound names like jwtSecret, apiToken, and userPassword are masked too. Nested objects, arrays, Map/Set, and circular references are handled; values past maxDepth (12) become '[Truncated]' so deep secrets can't leak.
  • redactFormat(options?) is a winston format, exported from the optional @master4n/decorators/winston subpath (winston is an optional peer dependency). Add it to your logger's format.combine(...) to protect your logs too. redact() itself is dependency-free and lives on the main entry.

Redaction is key-based — it masks object fields whose name is sensitive. It cannot mask a secret passed as a positional primitive (e.g. login(rawToken)) or one embedded in an error message/stack. Pass secrets as named object fields, and don't put them in error messages.

@Secret field names are registered process-globally, so redact() masks that field name everywhere — pick distinctive secret field names (jwtSecret, not id) to avoid over-masking unrelated fields.

import { redact } from '@master4n/decorators';
import { redactFormat } from '@master4n/decorators/winston'; // optional peer: winston
import winston from 'winston';

const logger = winston.createLogger({
  format: winston.format.combine(redactFormat(), winston.format.json()),
});

logger.info('Loaded config', redact(appConfig)); // jwtSecret -> [REDACTED]

Guard & Shield — validation & access

Guards throw on invalid input — misuse fails fast instead of slipping through.

DecoratorTargetThrowsDescription
@NotNullmethodValidationErrorrejects null/undefined arguments.
@ValidDatemethodValidationErrorfirst arg must be a valid { DD, MM, YYYY } date.
@Pattern(regex, opts?)propertyValidationErroronly matches the regex. ⚠️ ReDoS: set { maxLength } for untrusted input.
@Min(n) / @Max(n)propertyValidationErrorstring/array length ≥ n / ≤ n, or number value.
@Range(min, max)propertyValidationErrorinclusive bounds on string/array length or number value.
@Email @URL @UUIDpropertyValidationErrorformat checks for email / URL / UUID.
@Enum(values)propertyValidationErrorvalue must be one of values.
@NonEmptypropertyValidationErrorrejects null/undefined/''/[].
@Integer @PositivepropertyValidationErrornumber must be an integer / greater than zero.
@NotBlankpropertyValidationErrorstring with a non-whitespace char (asserts presence).
@Size(min, max)propertyValidationErrorstring/array length bounds.
@Negative @PositiveOrZero @NegativeOrZeropropertyValidationErrornumber sign constraints.
@Past @Future @PastOrPresent @FutureOrPresentpropertyValidationErrordate is before/after now.
@AssertTrue @AssertFalsepropertyValidationErrorboolean must be true / false.
@Digits(int, frac)propertyValidationErrormax integer + fractional digits.

Transforms normalize the value on assignment (and run before validators, whatever the stacking order):

DecoratorEffect
@Trimtrim whitespace from assigned strings.
@Lowercase / @Uppercasechange case of assigned strings.
@Coerce(type)coerce to 'number'/'boolean'/'string'.
@Clamp(min, max)clamp an assigned number into [min, max].
@Configured
class Signup {
  @Trim @Lowercase @Email()       email!: string;  // "  A@B.CO " -> "a@b.co", validated
  @Coerce('number') @Range(18, 120) age!: number;  // "21" -> 21, bounded
}

| @Role(...roles) | method | ForbiddenError | allows only if the principal has one of the roles. | | @Authorize(predicate) | method | ForbiddenError | allows only if predicate(ctx) is truthy. |

@Role/@Authorize are auth-agnostic. Register how to find the principal once:

import { Role, Authorize, setRoleResolver } from '@master4n/decorators';

setRoleResolver((ctx) => (ctx.instance as any).user?.roles ?? []);

class AdminApi {
  @Role('admin', 'owner')
  deleteUser(id: string) { /* ... */ }

  @Authorize((ctx) => (ctx.instance as any).user?.can('billing'))
  refund(orderId: string) { /* ... */ }
}

Resolvers/predicates may be async (the guarded call then returns a promise).

@Pattern guards a property: assignments that don't match the regex throw and the previous value is kept. Add @Configured so it works under any useDefineForClassFields setting (like the injection decorators).

import { Configured, Pattern } from '@master4n/decorators';

@Configured
class User {
  @Pattern(/^[^@\s]+@[^@\s]+\.[^@\s]+$/, { message: 'invalid email' })
  email!: string;

  @Pattern(/^\d{6}$/, { coerce: true }) // accepts 560001 or "560001"
  pincode!: string;
}

new User().email = 'not-an-email'; // throws ValidationError

@Min/@Max/@Range are polymorphic — they check string/array length or a number's value — and compose with each other and @Pattern:

@Configured
class Account {
  @Pattern(/^[a-z0-9_]+$/) @Min(3) @Max(20)
  username!: string;            // lowercase, 3–20 chars

  @Range(0, 100) score!: number; // 0..100
}

Utility decorators

DecoratorTargetDescription
@GenerateIDclass propertyassigns a lazy UUIDv4 (via crypto.randomUUID()).
@Counterstatic propertyauto-incrementing counter on each read.
@Log(opts?)methodlogs entry/exit; { args, result } also log redacted args/return; { level } sets the level.
@Retry(n, opts?)methodretries on failure (sync/async); opts.delayMs for async.
@Memoizemethodcaches results by argument JSON, per instance.
@Deprecated(msg)methodlogs a one-time deprecation warning.
@Measuremethodlogs execution time (sync/async).

Model — data classes

DecoratorAdds
@ToString(opts?)a toString() listing fields — with @Secret/sensitive fields redacted. only/exclude options.
@Equals(...keys?)an equals(other) (same-constructor, field-wise).
@Withwith(patch) → shallow copy with overrides (frozen-preserving).
@Data@ToString + equals() + with() in one.
@ImmutableObject.freeze each instance (immutable value object). Pairs with @With.
@Readonlyfield: assignable once, then throws (like final).
@Synchronizedmethod: serialize concurrent async calls per instance (mutex).
@Builder / builder(Class)fluent builder. builder() is fully typed (no codegen).
import { Data, Immutable, With, builder } from '@master4n/decorators';

@Immutable
@Data                                   // toString + equals + with
class Money { constructor(public amount = 0, public currency = 'INR') {} }

const a = new Money(100, 'INR');
const b = (a as any).with({ amount: 250 });   // frozen copy
const c = builder(Money).amount(50).currency('USD').build(); // typed builder

Route — REST controllers

Build Express routes declaratively — @Controller + @GetMapping + @PathVariable/@RequestParam/@RequestBody — then wire them in with registerControllers. Framework-agnostic (no express dependency); works with any Express-compatible app/Router.

import express from 'express';
import {
  Controller, Get, Post, Param, Query, Body, HttpCode, Use, registerControllers,
} from '@master4n/decorators';

@Use(authMiddleware)            // controller-level middleware
@Controller('/users')
class UserController {
  @Get('/:id')
  getUser(@Param('id') id: string, @Query('expand') expand?: string) {
    return this.service.find(id, expand);   // returned value -> res.json(...) (200)
  }

  @Post('/')
  @HttpCode(201)
  create(@Body() dto: CreateUserDto) {
    return this.service.create(dto);          // -> 201 + JSON
  }
}

const app = express();
app.use(express.json());
registerControllers(app, [new UserController()]);

Returned values are sent as JSON with the configured status; throw and it's routed to next(err). Inject @Res() to take over the response yourself.

ConciseAliasPurpose
@Controller(base)@RestControllerclass: base path + controller middleware.
@Get @Post @Put @Patch @Delete @Options @Head @All@GetMapping@RequestMappingroute a method.
@Param(n)@PathVariablepath variable.
@Query(n)@RequestParamquery-string value.
@Body(n?)@RequestBodyrequest body (or one field).
@Header(n)@RequestHeaderrequest header.
@Cookie(n) @Req() @Res() @Next()cookie / raw req / res / next.
@HttpCode(code)@ResponseStatussuccess status code.
@ContentType(t)@Producesresponse content-type.
@Redirect(url) @Use(...mw)redirect / attach middleware (class or route).

Agent — LLM tools

Expose class methods as LLM-callable tools, then dispatch the model's tool call back to the method — the whole agent loop, declaratively.

import { Tool, getTools, invokeTool } from '@master4n/decorators';

class WeatherService {
  @Tool({
    description: 'Get the current temperature for a city',
    parameters: {
      type: 'object',
      properties: { city: { type: 'string' } },
      required: ['city'],
    },
  })
  getTemperature(args: { city: string }) { /* ... */ }
}

const tools = getTools();
// -> [{ name: 'getTemperature', description: '...', parameters: {...} }]
// Pass `tools` to your LLM (OpenAI `tools`/`parameters`, Anthropic `input_schema`).

// When the model returns a tool call:
const result = invokeTool(new WeatherService(), call.name, call.arguments);

parameters is an explicit JSON Schema — TypeScript parameter types are erased at runtime, so the library does not (and cannot honestly) infer it.

Tool names live in a process-global registry and must be unique — a duplicate name overwrites the earlier one (so invokeTool would target the wrong method). Give colliding tools an explicit unique name.

Agent power-ups (method decorators)

Wrap the methods an agent calls so they're validated, safe to retry, verified, and measured:

DecoratorDoes
@Validate(check)reject bad input args before running (throws ValidationError).
@Guardrail(check, opts?)verify the output; retry up to opts.retries, else GuardrailError.
@Idempotent(keyFn?)cache the result by an idempotency key — safe to retry (no TTL; failures aren't cached).
@Meter(name?)record calls / errors / timing; read with getMetrics().

Craft — class & method ergonomics

Kill the small repeated boilerplate.

DecoratorDoes
@Bindauto-bind a method to its instance — no more .bind(this) for callbacks.
@Lazy(factory)compute a property once on first access, then cache.
@SealedObject.seal each instance (no added/removed props; values stay writable).
@Mixin(...src)copy members from other objects/classes onto the class.
@OnChange(fn)run fn(new, old, instance) when a property actually changes (first set initializes silently).

Insight — observability

DecoratorDescription
@Trace(opts?)structured entry/exit/error logs with a correlation id threaded through nested calls (AsyncLocalStorage). Args/results redacted. getTraceId() reads the current id.
@Audit(action?)logs actor + action + redacted args. Set the "who" via setAuditResolver.
@LogErrors()logs errors (redacted args + stack) and rethrows (sync/async).
import { Trace, Audit, setAuditResolver, getTraceId } from '@master4n/decorators';

setAuditResolver((ctx) => (ctx.instance as any).user?.id ?? 'system');

class OrderService {
  @Trace({ result: true })           // one trace id flows through the whole call tree
  @Audit('order.refund')
  async refund(orderId: string) { /* getTraceId() to tag your own logs */ }
}

Flow — resilience & control flow

DecoratorDescription
@Timeout(ms)reject an async method with TimeoutError if it exceeds ms.
@Oncerun once per instance; cache that result forever.
@Cache(ttlMs)memoize with a TTL (vs @Memoize, which never expires).
@Dedupecoalesce concurrent identical async calls (single-flight).
@Fallback(value|fn)on error, return a fallback instead of throwing (sync/async).
@RateLimit(limit, ms)throw RateLimitError past limit calls per rolling ms.
@Concurrency(max)cap concurrent async executions; queue the rest.
@CircuitBreaker(opts)open after N failures, fast-fail with CircuitOpenError, auto-reset.
@Debounce(ms)void methods: collapse rapid calls, run on the trailing edge.
@Throttle(ms)void methods: run on the leading edge, ignore for ms.
import { Timeout, Retry, CircuitBreaker, Fallback } from '@master4n/decorators';

class Upstream {
  @Fallback(null)               // OUTERMOST = last resort (see ordering note above)
  @CircuitBreaker({ failureThreshold: 5, resetMs: 30_000 })
  @Retry(3, { delayMs: 200 })
  @Timeout(5_000)
  async fetchUser(id: string) { /* ... */ }
}
import { GenerateID, Counter, Log, Retry, Memoize } from '@master4n/decorators';

class Job {
  @GenerateID id!: string;        // unique per instance
  @Counter static runs: number;   // increments on each read

  @Retry(3, { delayMs: 200 })
  @Log({ args: true, result: true }) // logged args/result are redacted
  async run() { /* ... */ }

  @Memoize
  score(input: string): number { /* expensive, pure */ return input.length; }
}

Breaking change in 2.0.0

@NotNull now throws ValidationError for null/undefined arguments (it only logged in 1.x). @ValidDate is fixed (it was a no-op) and now throws on an invalid date. See KNOWN_ISSUES.md for the history.

Changelog

See CHANGELOG.md.

Credits

Written by Master4Novice.

Keywords

decorators

FAQs

Package last updated on 31 May 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts