Skip to main content
tsoa provides a powerful extensibility mechanism that allows you to create custom route generators. This enables you to integrate tsoa with any Node.js framework or customize the route generation logic to match your specific needs.

Overview

Custom route generators extend the AbstractRouteGenerator class and implement the GenerateCustomRoutes method. This gives you full control over how routes are generated while still leveraging tsoa’s metadata generation capabilities.
The AbstractRouteGenerator provides helper methods for building models, properties, and parameters from the metadata generated by tsoa’s TypeScript analysis.

Creating a Custom Route Generator

Basic Structure

A custom route generator must extend AbstractRouteGenerator and implement the GenerateCustomRoutes method:
import { AbstractRouteGenerator, ExtendedRoutesConfig } from '@tsoa/cli';
import { Tsoa } from '@tsoa/runtime';

export class CustomRouteGenerator extends AbstractRouteGenerator<ExtendedRoutesConfig> {
  constructor(
    metadata: Tsoa.Metadata,
    options: ExtendedRoutesConfig
  ) {
    super(metadata, options);
  }

  public async GenerateCustomRoutes(): Promise<void> {
    // Your custom route generation logic here
    const context = this.buildContext();
    const models = this.buildModels();
    
    // Generate routes based on your framework's requirements
    await this.generateRoutesFile(context, models);
  }

  private async generateRoutesFile(context: any, models: any): Promise<void> {
    // Implement your custom file generation logic
  }
}

Accessing Metadata

The AbstractRouteGenerator provides access to complete metadata about your controllers:
public async GenerateCustomRoutes(): Promise<void> {
  // Access controller metadata
  this.metadata.controllers.forEach(controller => {
    console.log(`Controller: ${controller.name}`);
    console.log(`Path: ${controller.path}`);
    console.log(`Location: ${controller.location}`);
    
    // Access method metadata
    controller.methods.forEach(method => {
      console.log(`  Method: ${method.name}`);
      console.log(`  HTTP Method: ${method.method}`);
      console.log(`  Path: ${method.path}`);
      console.log(`  Success Status: ${method.successStatus}`);
      
      // Access parameter metadata
      method.parameters.forEach(param => {
        console.log(`    Parameter: ${param.name}`);
        console.log(`    Type: ${param.type.dataType}`);
        console.log(`    In: ${param.in}`);
      });
    });
  });
}

Building Context

The buildContext() method provides a structured context object with all necessary information:
public async GenerateCustomRoutes(): Promise<void> {
  const context = this.buildContext();
  
  // Context includes:
  // - authenticationModule: Path to authentication module
  // - iocModule: Path to IoC container module
  // - basePath: Base path for all routes
  // - controllers: Array of controller metadata
  // - models: Generated model schemas
  // - useSecurity: Whether any routes use security
  // - useFileUploads: Whether any routes handle file uploads
  // - multerOpts: Multer configuration for file uploads
}

Building Models

The buildModels() method generates runtime model schemas from TypeScript types:
public async GenerateCustomRoutes(): Promise<void> {
  const models = this.buildModels();
  
  // models is a dictionary of TsoaRoute.Models
  // Each model includes validation schemas for:
  // - refObject: Object types with properties
  // - refEnum: Enum types
  // - refAlias: Type aliases
  
  Object.keys(models).forEach(modelName => {
    const model = models[modelName];
    
    if (model.dataType === 'refObject') {
      console.log(`Object Model: ${modelName}`);
      console.log(`Properties:`, Object.keys(model.properties));
      console.log(`Additional Properties:`, model.additionalProperties);
    }
  });
}

Configuration

Registering Your Custom Generator

Specify your custom route generator in tsoa.json:
{
  "entryFile": "src/server.ts",
  "noImplicitAdditionalProperties": "throw-on-extras",
  "routes": {
    "routesDir": "src/generated",
    "routeGenerator": "./src/generators/customRouteGenerator.ts"
  },
  "spec": {
    "outputDirectory": "api-docs",
    "specVersion": 3
  }
}
The routeGenerator path should point to a file that exports your custom generator class as the default export.

Using Custom Templates

For simpler customizations, you can use a custom Handlebars template:
{
  "routes": {
    "routesDir": "src/generated",
    "middleware": "express",
    "middlewareTemplate": "./templates/custom-express.hbs"
  }
}

Advanced Examples

Example 1: Fastify Integration

import { AbstractRouteGenerator, ExtendedRoutesConfig } from '@tsoa/cli';
import { Tsoa } from '@tsoa/runtime';
import { writeFile } from 'fs/promises';

export class FastifyRouteGenerator extends AbstractRouteGenerator<ExtendedRoutesConfig> {
  public async GenerateCustomRoutes(): Promise<void> {
    const context = this.buildContext();
    const models = this.buildModels();
    
    let routeCode = `import { FastifyInstance } from 'fastify';\n`;
    routeCode += `import { TsoaRoute } from '@tsoa/runtime';\n\n`;
    
    // Import controllers
    context.controllers.forEach(controller => {
      const importPath = this.getRelativeImportPath(controller.location);
      routeCode += `import { ${controller.name} } from '${importPath}';\n`;
    });
    
    routeCode += `\nexport function RegisterRoutes(app: FastifyInstance) {\n`;
    
    // Generate routes for each controller
    context.controllers.forEach(controller => {
      controller.actions.forEach(action => {
        routeCode += `  app.${action.method}('${action.fullPath}', async (request, reply) => {\n`;
        routeCode += `    const controller = new ${controller.name}();\n`;
        routeCode += `    const result = await controller.${action.name}(\n`;
        
        // Map parameters
        Object.keys(action.parameters).forEach((paramName, index) => {
          const param = action.parameters[paramName];
          let value = '';
          
          switch (param.in) {
            case 'path':
              value = `request.params['${param.name}']`;
              break;
            case 'query':
              value = `request.query['${param.name}']`;
              break;
            case 'body':
              value = `request.body`;
              break;
            case 'header':
              value = `request.headers['${param.name}']`;
              break;
          }
          
          const comma = index < Object.keys(action.parameters).length - 1 ? ',' : '';
          routeCode += `      ${value}${comma}\n`;
        });
        
        routeCode += `    );\n`;
        routeCode += `    reply.status(${action.successStatus || 200}).send(result);\n`;
        routeCode += `  });\n\n`;
      });
    });
    
    routeCode += `}\n`;
    
    const outputPath = `${this.options.routesDir}/routes.ts`;
    await writeFile(outputPath, routeCode);
  }
}

Example 2: Custom Validation Logic

import { AbstractRouteGenerator, ExtendedRoutesConfig } from '@tsoa/cli';
import { Tsoa, TsoaRoute } from '@tsoa/runtime';

export class CustomValidationGenerator extends AbstractRouteGenerator<ExtendedRoutesConfig> {
  public async GenerateCustomRoutes(): Promise<void> {
    const context = this.buildContext();
    const models = this.buildModels();
    
    // Generate custom validation schemas
    const validationSchemas = this.generateValidationSchemas(models);
    
    // Generate routes with custom validation
    await this.generateRoutesWithValidation(context, validationSchemas);
  }
  
  private generateValidationSchemas(models: TsoaRoute.Models): Map<string, any> {
    const schemas = new Map<string, any>();
    
    Object.keys(models).forEach(modelName => {
      const model = models[modelName];
      
      if (model.dataType === 'refObject') {
        // Convert to your preferred validation library schema
        // (e.g., Zod, Yup, Joi)
        const schema = {
          type: 'object',
          properties: {},
          required: []
        };
        
        Object.keys(model.properties).forEach(propName => {
          const prop = model.properties[propName];
          schema.properties[propName] = {
            type: prop.dataType,
            required: prop.required
          };
          
          if (prop.required) {
            schema.required.push(propName);
          }
        });
        
        schemas.set(modelName, schema);
      }
    });
    
    return schemas;
  }
  
  private async generateRoutesWithValidation(
    context: any,
    validationSchemas: Map<string, any>
  ): Promise<void> {
    // Implement custom route generation with validation
  }
}

Example 3: Testing Route Generator

Here’s a minimal example used in tsoa’s own tests:
import { AbstractRouteGenerator } from '@tsoa/cli';

export class DummyRouteGenerator extends AbstractRouteGenerator<any> {
  private static CALL_COUNT = 0;

  GenerateCustomRoutes(): Promise<void> {
    DummyRouteGenerator.CALL_COUNT += 1;
    return Promise.resolve(undefined);
  }

  public static getCallCount(): number {
    return this.CALL_COUNT;
  }
}

Helper Methods

The AbstractRouteGenerator provides several utility methods:

Path Transformation

protected pathTransformer(path: string): string {
  // Convert Express-style paths (:id) to other formats
  // Override this method to customize path transformation
  return convertBracesPathParams(path);
}

Import Path Resolution

protected getRelativeImportPath(fileLocation: string): string {
  // Generates correct relative import paths
  // Handles ESM/CommonJS extensions automatically
  // Returns path like './controllers/userController.js'
}

Property and Parameter Builders

protected buildPropertySchema(source: Tsoa.Property): TsoaRoute.PropertySchema;
protected buildParameterSchema(source: Tsoa.Parameter): TsoaRoute.ParameterSchema;
protected buildProperty(type: Tsoa.Type): TsoaRoute.PropertySchema;
These methods handle complex types including unions, intersections, arrays, nested objects, and reference types.

File Writing

The base class provides a helper for conditional file writing:
protected async shouldWriteFile(fileName: string, content: string): Promise<boolean> {
  // Returns false if noWriteIfUnchanged is enabled and content matches existing file
  // Helps avoid unnecessary rebuilds in watch mode
}

Best Practices

1

Start with the default generator

Examine DefaultRouteGenerator in the tsoa source code to understand the patterns and helper methods available.
2

Use type-safe access

Leverage TypeScript’s type system when accessing metadata to catch errors early.
3

Handle all parameter types

Ensure your generator properly maps path, query, body, header, and request parameters.
4

Support security

If your application uses authentication, implement proper security middleware integration.
5

Test thoroughly

Create integration tests that verify your generated routes work correctly with your target framework.

Reference Implementation

tsoa includes built-in route generators for Express, Koa, and Hapi. You can find them in:
  • packages/cli/src/routeGeneration/defaultRouteGenerator.ts
  • packages/cli/src/routeGeneration/routeGenerator.ts
  • packages/cli/src/routeGeneration/templates/*.hbs
Study these implementations for patterns on handling:
  • File uploads with Multer
  • Authentication middleware
  • IoC container integration
  • Validation error handling
  • Success status codes
  • Response header management

Middleware

Learn about adding middleware to controllers and routes

Configuration

Complete tsoa configuration reference

Build docs developers (and LLMs) love