How to Add Custom Decorators for Middleware Functionality in TypeScript

You can add custom decorators for middleware functionality by creating higher-order functions that intercept method descriptors, wrap the original implementation with cross-cutting logic like logging or authentication, and return the modified descriptor while preserving the this context.

The castrozan/tcc repository (Instagit) demonstrates a robust decorator-based middleware pattern for HTTP controllers that extend OpenAPIRoute. This architecture allows you to inject reusable concerns—such as error handling, request logging, and authentication—into controller methods without cluttering business logic. Understanding how to create custom decorators for middleware functionality enables you to extend this pattern for any application-specific cross-cutting concern.

Understanding the Decorator-Based Middleware Pattern

The core pattern relies on TypeScript's method decorators, which receive three arguments: the target prototype, the property key, and the property descriptor. In professionals-dummy-app/src/presentation/decorators/handleErrors.ts, the withErrorHandling decorator captures the original method from descriptor.value and replaces it with a wrapper function that implements middleware logic.

This wrapper executes before and after the original method, enabling pre-processing (validation, logging) and post-processing (error transformation, response formatting). The pattern preserves the original method's signature while injecting reusable behavior, making it ideal for middleware functionality in controller classes extending OpenAPIRoute.

Creating a Custom Decorator for Middleware Functionality

Step 1 - Define the Higher-Order Function

Create a function that accepts the standard decorator arguments: target, propertyKey, and descriptor. The function must return a PropertyDescriptor to properly modify the method behavior. Place your decorator in a dedicated file, such as src/presentation/decorators/logRequest.ts, following the project's organizational structure.

Step 2 - Wrap the Original Method

Inside the decorator, store a reference to descriptor.value (the original method). Then assign a new function to descriptor.value that implements your middleware logic. This new function executes when the decorated method is called, allowing you to intercept arguments and modify return values.

Step 3 - Preserve Context and Async Behavior

Use originalMethod.apply(this, args) to maintain the correct this context (the controller instance). If the original method is asynchronous, declare the wrapper as async and await the original call. This ensures that middleware functionality works correctly with both sync and async controller methods, as implemented in professionals-dummy-app/src/presentation/controllers/professionals/CreateProfessionalController.ts.

Practical Examples of Custom Middleware Decorators

Logging Decorator

Implement request logging by creating a decorator that records method entry, arguments, execution time, and exit. This example demonstrates the pattern used in the repository for cross-cutting concerns:

// src/presentation/decorators/logRequest.ts
export function logRequest(
  target: object,
  propertyKey: string,
  descriptor: PropertyDescriptor
): PropertyDescriptor {
  const original = descriptor.value;

  descriptor.value = async function (...args: unknown[]) {
    const start = Date.now();
    console.log(`[${new Date().toISOString()}] → ${propertyKey}`, { args });

    try {
      const result = await original.apply(this, args);
      console.log(
        `[${new Date().toISOString()}] ← ${propertyKey} (took ${Date.now() - start}ms)`,
        { result }
      );
      return result;
    } catch (err) {
      console.error(
        `[${new Date().toISOString()}] ✖ ${propertyKey} (took ${Date.now() - start}ms)`,
        { error: err }
      );
      throw err;
    }
  };

  return descriptor;
}

Usage in a controller:

import { withErrorHandling } from 'presentation/decorators';
import { logRequest } from 'presentation/decorators/logRequest';

export class UpdateProfessionalController extends OpenAPIRoute {
  @logRequest
  @withErrorHandling
  async handle(): Promise<object> {
    // business logic
    return { success: true };
  }
}

Authentication Decorator

Add JWT verification to protect specific controller methods. This decorator checks for valid tokens before executing the original method, similar to the error-handling pattern in handleErrors.ts:

// src/presentation/decorators/requireAuth.ts
import { z } from 'zod';

export function requireAuth(
  target: object,
  propertyKey: string,
  descriptor: PropertyDescriptor
): PropertyDescriptor {
  const original = descriptor.value;

  descriptor.value = async function (this: any, ...args: unknown[]) {
    const request = await this.getRequest();
    const authHeader = request.headers['authorization'];

    if (!authHeader) {
      return { 
        success: false, 
        message: 'Missing Authorization header', 
        statusCode: 401 
      };
    }

    const token = authHeader.replace('Bearer ', '');
    try {
      const payload = z.object({ sub: z.string() }).parse(
        JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString())
      );
      this.user = payload;
    } catch {
      return { 
        success: false, 
        message: 'Invalid or expired token', 
        statusCode: 401 
      };
    }

    return original.apply(this, args);
  };

  return descriptor;
}

Usage:

import { withErrorHandling } from 'presentation/decorators';
import { requireAuth } from 'presentation/decorators/requireAuth';

export class DeleteEquipmentController extends OpenAPIRoute {
  @requireAuth
  @withErrorHandling
  async handle(): Promise<object> {
    // this.user is now populated with JWT payload
    return { success: true, deletedBy: this.user.sub };
  }
}

Chaining Multiple Decorators

Stack decorators to combine middleware functionality. When multiple decorators are applied to a single method, they execute from bottom to top (the decorator closest to the method runs first):

export class FindAllEquipmentController extends OpenAPIRoute {
  @logRequest
  @requireAuth
  @withErrorHandling
  async handle(): Promise<object> {
    // Execution order:
    // 1. withErrorHandling (innermost)
    // 2. requireAuth
    // 3. logRequest (outermost)
    // 4. original handle method
    return { success: true, data: [] };
  }
}

Key Implementation Details from the Source Code

The castrozan/tcc repository implements this pattern consistently across multiple applications. The withErrorHandling decorator in professionals-dummy-app/src/presentation/decorators/handleErrors.ts serves as the reference implementation, wrapping controller methods to catch errors and return standardized error responses.

Controllers such as CreateProfessionalController in professionals-dummy-app/src/presentation/controllers/professionals/CreateProfessionalController.ts and CreateEquipmentController in equipments-dummy-app/src/presentation/controllers/equipments/CreateEquipmentController.ts demonstrate real-world usage. These classes extend OpenAPIRoute and apply the @withErrorHandling decorator to their handle methods, ensuring consistent error processing across the application layer.

When creating custom decorators for middleware functionality, follow the established project structure by placing decorator files in src/presentation/decorators/ and exporting them for use in controller files. This maintains consistency with the existing architecture and ensures that middleware logic remains reusable and testable.

Summary

  • Custom decorators for middleware functionality in TypeScript are higher-order functions that intercept method descriptors to inject cross-cutting concerns like logging, authentication, and error handling.
  • The castrozan/tcc repository demonstrates this pattern through the withErrorHandling decorator in professionals-dummy-app/src/presentation/decorators/handleErrors.ts.
  • To create a custom decorator, define a function accepting target, propertyKey, and descriptor, store the original method, assign a wrapper function to descriptor.value, and return the descriptor.
  • Always preserve the this context using originalMethod.apply(this, args) and support asynchronous methods by using async/await in the wrapper.
  • Stack multiple decorators to combine functionality; they execute from bottom to top (closest to the method first).
  • Place custom decorators in src/presentation/decorators/ to maintain project consistency.

Frequently Asked Questions

What is the execution order when multiple decorators are applied to a single method?

When you stack multiple decorators on a method, they execute from bottom to top, meaning the decorator closest to the method definition runs first. For example, if you apply @logRequest, @requireAuth, and @withErrorHandling from top to bottom, the execution order is: withErrorHandling (innermost), then requireAuth, then logRequest (outermost), and finally the original method. This layering allows each decorator to wrap the next, creating a middleware pipeline where the innermost decorator has the final say before the actual business logic executes.

How do I access the HTTP request object inside a custom decorator?

To access the HTTP request object inside a custom decorator, reference the controller instance via this after preserving context with originalMethod.apply(this, args). In the castrozan/tcc codebase, controllers extend OpenAPIRoute, which provides framework-specific helpers like getRequest(). Inside your decorator wrapper, call await this.getRequest() to access the request object, then inspect headers, body, or query parameters as needed. This approach works because the decorator preserves the class context, allowing you to access all instance methods and properties of the controller.

Can I use custom decorators with synchronous controller methods?

Yes, custom decorators work with both synchronous and asynchronous methods. When implementing the wrapper function in descriptor.value, you can omit the async keyword if the original method is synchronous. However, in the castrozan/tcc architecture, controller methods typically return Promises because they handle I/O operations. To ensure maximum compatibility, implement your wrapper as async and use await originalMethod.apply(this, args), which works for both sync and async methods since awaiting a non-Promise value resolves immediately. This approach ensures your custom decorators for middleware functionality work consistently across all controller methods.

Where should I place custom decorator files in the project structure?

Following the architecture established in castrozan/tcc, place custom decorator files in the src/presentation/decorators/ directory. This location keeps middleware logic organized within the presentation layer, separate from business logic and infrastructure concerns. Create individual files for each decorator (e.g., logRequest.ts, requireAuth.ts) and export the decorator function as the default or a named export. Import these decorators into your controller files located in src/presentation/controllers/. This structure maintains consistency with the existing withErrorHandling decorator and ensures that middleware logic remains reusable, testable, and easy to locate.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →