Skip to main content

introduction

What is a Decorator?

A TypeScript decorator is a special kind of function that can attach behavior or metadata to classes, methods, properties, accessors, or parameters. Conceptually, it is similar to “annotations” in languages like Java or “attributes” in C#. It does not change TypeScript’s type system; it runs at runtime (JavaScript execution time), not during type checking.

In practical terms, a decorator is a function that receives information about the thing it decorates (for example, a class or method) and can modify it or store metadata about it. To enable them, experimentalDecorators must be true in tsconfig.json.

OpenCore uses TypeScript decorators to declare runtime behavior in a way that is:

  • explicit
  • discoverable
  • testable
  • hard to misuse

1. What is a decorator in OpenCore?

Decorators are the primary “wiring language” of the framework. In OpenCore, a decorator is a declaration that attaches meaning to code.

Examples:

  • “this class is a controller” (@Server.Controller())
  • “this method handles a net event” (@Server.OnNet('event:name'))
  • “this method is a command” (@Server.Command({ ... }))

The important point:

  • decorators describe intent
  • the framework turns that intent into runtime bindings during bootstrap

2. What problem do decorators solve?

Without decorators, most FiveM codebases drift into:

  • hidden globals
  • ad-hoc event registration scattered across files
  • implicit lifecycle assumptions
  • duplicated validation/security

OpenCore decorators solve this by:

  • making entry points explicit (controllers)
  • centralizing binding/registration into the bootstrap phase
  • making security and validation declarative

3. When do decorators execute?

There are two distinct moments:

3.1 Decorator evaluation (module load time)

Decorators execute when the module is evaluated (imported).

If a module is never imported:

  • its decorators never run
  • the framework cannot discover it

3.2 Framework binding (bootstrap scan)

Even after decorators have stored metadata, the framework becomes “live” only after bootstrap scanning.

Reference:

  • see docs/lifecycle.md (section: “Decorator binding moment”).

4. Guarantees (what decorators give you)

Decorators provide a stable contract:

  • Consistent entry points
    • handlers receive a Server.Player context where required
  • Centralized validation and security
    • schemas and security decorators run before your logic
  • Explicit runtime wiring
    • you can reason about behavior by reading the controller class

5. Non-guarantees (what decorators do NOT do)

Decorators do not guarantee:

  • that your controller is imported
  • that your handler will run before bootstrap scan completes
  • that payloads are safe unless you declare a schema or rely on the built-in pipelines
  • that gameplay invariants are enforced (that is your domain responsibility or domain libraries)

6. Practical mental model

Think of decorators as:

  • “configuration attached to code”

And the bootstrap as:

  • “the compiler that turns that configuration into live runtime bindings”.

If you remember only one rule:

  • If a controller is not imported before bootstrap scanning, it does not exist for the framework.

7. Decorators List

Class Decorators

NameDescription
@Server.Controller()Marks a server-side class as a Controller. Controllers are entry points for gameplay logic and can handle commands, events, and framework callbacks. The class is automatically registered and instantiated by the framework.
@Client.Controller()Same concept as @Server.Controller, but for the client-side. Used to handle UI interactions, player input, client events, and client-only logic.
@Server.Bind()Registers a class as injectable in the dependency container. By default, it is treated as a singleton. Useful for shared utilities, managers, adapters, or low-level services that are not Controllers.
@Server.Service()Semantic alias for @Server.Bind(). Indicates that the class contains domain or business logic. Improves readability and architectural clarity.
@Server.Repo()Semantic alias for @Server.Bind(). Intended for data access and persistence layers (repositories, stores, database adapters). Helps enforce a clean separation of concerns.

Method Decorators

NameDescription
@Server.Command()Registers the method as a chat/console command handler. The method is executed when the command is invoked by a player. Supports argument parsing and validation.
@Server.OnNet()Subscribes the method to a network event (onNet). Used for client → server or server → client communication via FiveM networking.
@Server.OnTick()Executes the method on every server tick. Intended for lightweight, high-frequency logic. Heavy operations should be avoided to prevent performance issues.
@Server.OnFrameworkEvent()Listens to internal framework events. Useful for reacting to lifecycle hooks, module events, or cross-system communication inside the framework.
@Server.OnFivemEvent()Subscribes the method to a native FiveM event (e.g. playerJoining, playerDropped). Acts as a clean abstraction over on() for FiveM events.
@Server.RequiresState()Ensures the player is in a specific state before executing the method. If the requirement is not met, the method is skipped or rejected. Commonly used for gameplay state validation (logged in, spawned, etc.).
@Server.Throttle()Applies rate limiting to the method. Prevents spam or abuse by limiting how frequently the method can be executed per player or context.
@Server.Export()Exposes the method as a FiveM export, making it callable from other resources. Useful for inter-resource APIs and modular architectures.
@Server.Guard()Applies access control rules to the method. Can enforce permissions, roles, ranks, or custom authorization logic before execution.
@Server.Public()Marks the method as explicitly public and callable without restrictions. Useful for APIs that should bypass guards or internal access checks.
@Client.OnNet()Subscribes the method to a network event (onNet) on the client. Typically used for server → client communication and synchronized gameplay events.
@Client.LocalEvent()Subscribes the method to a local (client-only) event. Useful for internal communication between client systems without involving networking.
@Client.GameEvent()Listens to a GTA V game-native event (gameEventTriggered). Allows reacting to low-level engine events such as damage, explosions, or entity interactions.
@Client.OnTick()Executes the method on every client tick. Intended for lightweight, frame-based logic. Heavy computations should be avoided to prevent FPS drops.
@Client.Interval()Executes the method at a fixed time interval (in milliseconds). Useful for periodic logic that does not need to run every tick (polling, checks, UI sync).
@Client.Key()Binds the method to a keyboard key press. Executes when the specified key is pressed by the player. Ideal for gameplay shortcuts or UI interactions.
@Client.Nui()Registers the method as a NUI callback handler. Executed when the UI (HTML/JS) sends a message to the client script. Acts as the bridge between UI and gameplay logic.
@Client.Export()Exposes the method as a FiveM client export, allowing other resources to call it directly. Useful for inter-resource client APIs.
@Client.ResourceLifecycle()Subscribes the method to resource lifecycle events (start, stop, restart). Useful for initialization and cleanup logic on the client side.

Design Notes (Client Side)

  • Client Controllers are event-driven, not request-driven.
  • Prefer Interval over OnTick when possible to reduce CPU usage.
  • LocalEvent is the recommended way to communicate between client systems.
  • GameEvent is powerful but low-level—use it only when FiveM-native events are insufficient.
  • NUI communication is explicit and isolated through @Client.Nui().
  • This keeps the client runtime predictable, performant, and clean, even as UI and gameplay complexity grow.