Overview

How decorators work in MiiaJS - TC39 native, Symbol.metadata, and the catalogue of built-ins.

MiiaJS is a decorator-driven framework. @Controller, @Get, @UseGuard, @ValidateBody - every declarative knob you reach for is a decorator that attaches metadata to a class or method. The framework reads that metadata later (during app.init(), in middleware, or in tools like Swagger spec generation) and turns it into runtime behaviour.

The decorator system is built on TC39 native decorators - no reflect-metadata, no experimental compiler flags, no WeakMap-based shadow stores. The Symbol.metadata polyfill ships as the first import of @miiajs/core, so it's ready everywhere your code runs.

Mental model

The whole story is two steps:

  1. Decorators attach metadata at class definition time. Each decorator runs once, when the class is declared, and writes structured data onto Class[Symbol.metadata] keyed by symbols.
  2. The framework reads that metadata later. app.init() walks every controller, pulls routes, middleware, guards, status codes, and validation schemas off the metadata, and wires them into the router. Per-request middleware can also read metadata to make decisions.

If you internalise this, every other piece of the framework - and your own custom decorators - falls out for free.

Built-in decorators

Every decorator below is built with the same public factory API that's available to you in Custom.

DecoratorKindWhat it doesSee
@InjectableclassMarks a class as DI-resolvableProviders
@ControllerclassRoute prefix + controller markerControllers
@ModuleclassGroups controllers, providers, and importsModules
@Get @Post @Put @Patch @Delete @Head @OptionsmethodBind an HTTP route to a handlerRouting
@StatusmethodDefault status code for the handler's responseControllers
@ValidateBody @ValidateQuery @ValidateParamsmethodZod-like schema validation, transparently replaces the parsed valueControllers
@Useclass or methodApply Koa-style middlewareMiddleware
@UseGuardclass or methodApply one or more guardsGuards
@SkipGuardclass or methodExclude a previously-applied guard from a routeGuards

The class/method dual decorators (@Use, @UseGuard, @SkipGuard) inspect their TC39 context and branch - applied to a class they affect every handler in the controller, applied to a method they affect just that handler.

Storage model

Metadata lives on the class constructor under Symbol.metadata, keyed by Symbols exported from @miiajs/core:

import { ROUTES, getMeta } from '@miiajs/core'

class UserController {
  @Get('/')
  list() { /* ... */ }
}

const routes = getMeta(UserController, ROUTES)
// → [{ method: 'GET', path: '/', handlerName: 'list' }]

A few properties to remember:

  • Class-level scope. Metadata is attached to the constructor, not to instances. Every instance shares the same metadata.
  • Symbol keys. Always use Symbols, never strings - string keys would collide with user-land properties on the metadata object.
  • Lifetime = module lifetime. The metadata exists for as long as the class is reachable. There's no per-request reset.

Execution model

Decorators run once, at class definition, in bottom-up order when stacked:

@A   // runs second
@B   // runs first
class Foo {}

This matters in exactly one situation: when one decorator reads metadata that another just wrote. In practice most decorators only write, so order is irrelevant - but if you're building decorators that read each other's output, remember the rule.

Decorators are never invoked per request. If you need request-time behaviour, write middleware - the decorator's job is to declare intent, the middleware's job is to act on it.

Extending the framework

Everything in the catalogue above is built with the four factory functions and five metadata helpers that @miiajs/core exposes publicly. The same API powers @miiajs/swagger's @ApiTag, @ApiOperation, @ApiSecurity, etc., and it's available to you for any project-specific decorator you need.

For higher-level recipes that always apply together - e.g. @AdminOnly() that bundles auth, a role guard, and a status code - compose them with applyDecorators. No metadata plumbing required; it just stacks existing decorators into one.

See Custom Decorators for the factories, helpers, walkthroughs, and the applyDecorators recipe.