title: 'Custom decorators · Nael Framework' description: 'Implement metadata and parameter decorators using SetMetadata, createHttpParamDecorator, and createGraphqlParamDecorator.'

Decorators

Custom decorators

Extend the framework with the same primitives NestJS documents—metadata decorators via SetMetadata and transport-aware parameter decorators with createHttpParamDecorator and createGraphqlParamDecorator. These helpers integrate with the router and schema builder so your decorators behave like first-party ones.

Metadata decorators

Wrap class and handler definitions with domain-specific metadata. Under the hood SetMetadata stores values using reflect-metadata, allowing guards, interceptors, or any runtime component to look them up.

Define a @Roles decorator
import { SetMetadata } from '@nl-framework/core';import { Controller, Get, UseGuards } from '@nl-framework/http';export const Roles = (...roles: string[]) => SetMetadata('roles', roles);@Controller('/admin')@Roles('admin')export class AdminController {  @Get()  @Roles('moderator')  dashboard() {    return { ok: true };  }}
Consume metadata inside a guard
import 'reflect-metadata';import { CanActivate } from '@nl-framework/core';import { HttpExecutionContext } from '@nl-framework/http';export class RolesGuard implements CanActivate {  canActivate(context: HttpExecutionContext) {    const handler = context.getHandler();    const controller = context.getClass();    const handlerRoles = Reflect.getMetadata('roles', handler) ?? [];    const controllerRoles = Reflect.getMetadata('roles', controller) ?? [];    if (!handlerRoles.length && !controllerRoles.length) {      return true;    }    const request = context.getRequest();    const currentRole = request.headers.get('x-role');    const allowed = [...controllerRoles, ...handlerRoles];    return currentRole ? allowed.includes(currentRole) : false;  }}

HTTP parameter decorators

Use createHttpParamDecorator() to capture values from RequestContext. Decorators participate in the same metadata pipeline as @Param() or @Body(), so pipes and validation keep working.

@CurrentUser for REST
import { Controller, Get, createHttpParamDecorator } from '@nl-framework/http';export const CurrentUser = createHttpParamDecorator((property, ctx) => {  const user = {    id: ctx.headers.get('x-user-id'),    role: ctx.headers.get('x-user-role'),  };  if (!property) {    return user;  }  return user[property];});@Controller('/profile')export class ProfileController {  @Get()  me(@CurrentUser('id') id: string, @CurrentUser() user: Record<string, unknown>) {    return { id, user };  }}

GraphQL parameter decorators

GraphQL resolvers can define decorators with createGraphqlParamDecorator(). The factory receives the parent value, sanitized args, the shared context object, and GraphQLResolveInfo, mirroring NestJS's ExecutionContext.

@CurrentTenant inside resolvers
import { ObjectType, Field } from '@nl-framework/graphql';import { Resolver, Query, createGraphqlParamDecorator } from '@nl-framework/graphql';export const CurrentTenant = createGraphqlParamDecorator((property, ctx) => {  const tenant = (ctx.context?.tenant ?? {}) as Record<string, unknown>;  if (!property) {    return tenant;  }  return tenant[property];});@ObjectType()class TenantInfo {  @Field()  id!: string;  @Field()  plan!: string;}@Resolver(() => TenantInfo)export class TenantResolver {  @Query(() => TenantInfo)  info(@CurrentTenant('plan') plan: string, @CurrentTenant('id') id: string) {    return { id, plan };  }}
Tips
  • Metadata decorators work for both legacy and stage-3 decorators—the framework adds initializers automatically.
  • Custom parameter decorators can still leverage pipes; any data you pass into the decorator is forwarded to pipe metadata.
  • GraphQL factories run after arguments are transformed, so you never have to manually validate DTOs.