Skip to main content
tsoa supports multiple OpenAPI (formerly Swagger) specification versions. Understanding the differences and how to configure them is important for API documentation and client generation.

Supported Versions

tsoa supports three major OpenAPI versions:
  • OpenAPI 3.1 (Latest) - Released 2021, JSON Schema compatible
  • OpenAPI 3.0 - Released 2017, widely adopted
  • OpenAPI 2.0 (Swagger) - Original specification, deprecated but still supported

Configuring Spec Version

Set the OpenAPI version in your tsoa.json:
tsoa.json
{
  "spec": {
    "outputDirectory": "./build",
    "specVersion": 3.1
  }
}

Version Comparison

OpenAPI 3.1 vs 3.0

openapi: 3.1.0
info:
  title: My API
  version: 1.0.0
  
components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        email:
          type: [string, "null"]  # Nullable in 3.1
      required:
        - id
        - name

Key Differences

FeatureOpenAPI 3.1OpenAPI 3.0Swagger 2.0
JSON SchemaFull compatibilityPartialExtended subset
Nullabletype: [string, "null"]nullable: truex-nullable: true
Examplesexamplesexampleexample
Webhooks✅ Supported❌ Not supported❌ Not supported
const✅ Supported❌ Not supported❌ Not supported
$schema✅ Allowed❌ Not allowed❌ Not allowed

OpenAPI 3.1 Features

Full JSON Schema Support

OpenAPI 3.1 is fully compatible with JSON Schema 2020-12:
interface User {
  id: number;
  /**
   * User's email address
   * @format email
   * @pattern ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$
   */
  email: string;
  
  /**
   * User's age
   * @minimum 18
   * @maximum 120
   */
  age: number;
  
  /**
   * User's role
   * @default "user"
   */
  role: 'admin' | 'user' | 'guest';
}

Nullable Types

In OpenAPI 3.1, use union types for nullable:
interface Product {
  id: number;
  name: string;
  description: string | null;  // Can be string or null
  price?: number;              // Optional (can be undefined)
}
Generated spec:
Product:
  type: object
  properties:
    id:
      type: integer
    name:
      type: string
    description:
      type:
        - string
        - "null"
    price:
      type: number
  required:
    - id
    - name
    - description

Const Values

interface Config {
  /**
   * @const "v1"
   */
  version: string;
  
  /**
   * @const 443
   */
  port: number;
}

OpenAPI 3.0 Features

Request Bodies

Explicit request body objects:
import { Controller, Post, Route, Body } from 'tsoa';

interface CreateUserRequest {
  name: string;
  email: string;
}

@Route('users')
export class UserController extends Controller {
  /**
   * Create a new user
   * @param requestBody User creation data
   */
  @Post()
  public async createUser(
    @Body() requestBody: CreateUserRequest
  ): Promise<User> {
    return createUser(requestBody);
  }
}
Generated spec:
paths:
  /users:
    post:
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateUserRequest'
      responses:
        '200':
          description: Ok
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'

Multiple Response Types

import { Controller, Get, Route, Response } from 'tsoa';

interface ErrorResponse {
  message: string;
  code: string;
}

@Route('users')
export class UserController extends Controller {
  /**
   * Get user by ID
   */
  @Get('{id}')
  @Response<ErrorResponse>(404, 'Not Found')
  @Response<ErrorResponse>(500, 'Internal Server Error')
  public async getUser(id: number): Promise<User> {
    return getUser(id);
  }
}

Security Schemes

tsoa.json
{
  "spec": {
    "securityDefinitions": {
      "jwt": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT"
      },
      "api_key": {
        "type": "apiKey",
        "name": "X-API-Key",
        "in": "header"
      },
      "oauth2": {
        "type": "oauth2",
        "flows": {
          "authorizationCode": {
            "authorizationUrl": "https://example.com/oauth/authorize",
            "tokenUrl": "https://example.com/oauth/token",
            "scopes": {
              "read:users": "Read user data",
              "write:users": "Modify user data"
            }
          }
        }
      }
    }
  }
}

Swagger 2.0 Support

While deprecated, Swagger 2.0 is still supported for legacy systems:
tsoa.json
{
  "spec": {
    "outputDirectory": "./build",
    "specVersion": 2,
    "host": "api.example.com",
    "basePath": "/v1",
    "schemes": ["https"],
    "securityDefinitions": {
      "api_key": {
        "type": "apiKey",
        "name": "api_key",
        "in": "header"
      }
    }
  }
}

Swagger 2.0 Limitations

Swagger 2.0 has several limitations:
  • No oneOf, anyOf, or allOf support
  • Limited nullable type support
  • No cookie parameters
  • No request body content types
  • No callback support

Migrating Between Versions

From 2.0 to 3.0

1

Update Configuration

{
  "spec": {
    "specVersion": 3  // Changed from 2
  }
}
2

Update Security Definitions

// Before (Swagger 2.0)
{
  "securityDefinitions": {
    "Bearer": {
      "type": "apiKey",
      "name": "Authorization",
      "in": "header"
    }
  }
}

// After (OpenAPI 3.0)
{
  "securityDefinitions": {
    "Bearer": {
      "type": "http",
      "scheme": "bearer",
      "bearerFormat": "JWT"
    }
  }
}
3

Update File Uploads

File upload parameters change in 3.0:
// Works in both versions
@Post('upload')
public async upload(
  @UploadedFile() file: Express.Multer.File
): Promise<any> {
  return { filename: file.originalname };
}

From 3.0 to 3.1

1

Update Version

tsoa.json
{
  "spec": {
    "specVersion": 3.1  // Changed from 3
  }
}
2

Update Nullable Types

// Before (3.0 style)
interface User {
  /**
   * @nullable
   */
  email: string;
}

// After (3.1 style)
interface User {
  email: string | null;
}
3

Regenerate Spec

npm run tsoa spec

Spec Customization

Basic Information

tsoa.json
{
  "spec": {
    "outputDirectory": "./build",
    "specVersion": 3.1,
    "name": "My API",
    "version": "1.0.0",
    "description": "API for managing users and products",
    "license": "MIT",
    "contact": {
      "name": "API Support",
      "email": "[email protected]",
      "url": "https://example.com/support"
    }
  }
}

Servers

Define multiple server environments:
tsoa.json
{
  "spec": {
    "servers": [
      {
        "url": "https://api.example.com/v1",
        "description": "Production server"
      },
      {
        "url": "https://staging-api.example.com/v1",
        "description": "Staging server"
      },
      {
        "url": "http://localhost:3000/v1",
        "description": "Development server"
      }
    ]
  }
}

Tags

Organize endpoints by tags:
tsoa.json
{
  "spec": {
    "tags": [
      {
        "name": "Users",
        "description": "User management endpoints"
      },
      {
        "name": "Products",
        "description": "Product catalog endpoints"
      }
    ]
  }
}

External Documentation

tsoa.json
{
  "spec": {
    "externalDocs": {
      "description": "Full API Documentation",
      "url": "https://docs.example.com"
    }
  }
}

Output Formats

Generate both JSON and YAML:
tsoa.json
{
  "spec": {
    "outputDirectory": "./build",
    "specVersion": 3.1,
    "yaml": true  // Generate YAML in addition to JSON
  }
}
This creates both:
  • build/swagger.json
  • build/swagger.yaml

Viewing Specifications

Swagger UI

Serve your spec with Swagger UI:
import express from 'express';
import swaggerUi from 'swagger-ui-express';
import * as swaggerDocument from './build/swagger.json';

const app = express();

app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));

app.listen(3000);
Access at http://localhost:3000/api-docs

Redoc

Use Redoc for a cleaner documentation UI:
import express from 'express';
import { redoc } from 'redoc-express';

const app = express();

app.get('/docs', redoc({
  title: 'API Documentation',
  specUrl: '/swagger.json'
}));

app.get('/swagger.json', (req, res) => {
  res.json(require('./build/swagger.json'));
});

app.listen(3000);

Client Generation

Use your OpenAPI spec to generate clients:

OpenAPI Generator

# Install
npm install -g @openapitools/openapi-generator-cli

# Generate TypeScript client
openapi-generator-cli generate \
  -i build/swagger.json \
  -g typescript-axios \
  -o client/

# Generate Python client
openapi-generator-cli generate \
  -i build/swagger.json \
  -g python \
  -o client-python/

Swagger Codegen

npx @apidevtools/swagger-cli bundle build/swagger.json \
  --outfile build/swagger-bundled.json \
  --type json

Best Practices

Use OpenAPI 3.0 or 3.1 for new projects. Only use Swagger 2.0 if required for legacy tooling.
Include version in URL or headers, and maintain separate specs for each major version.
Use JSDoc comments for descriptions, examples, and constraints.
Use tools like Spectral or swagger-cli to validate your spec:
npx @stoplight/spectral-cli lint build/swagger.json
Commit generated specs to track API changes over time.

Troubleshooting

Spec Generation Fails

  1. Check TypeScript compilation
  2. Verify tsoa configuration
  3. Check for syntax errors in decorators

Invalid Spec

Validate your spec:
npx swagger-cli validate build/swagger.json

Missing Types

Ensure all referenced types are exported:
// Export all types used in controllers
export interface User { ... }
export interface CreateUserRequest { ... }

Next Steps

Custom Templates

Customize route generation templates

API Documentation

Learn about API documentation

Build docs developers (and LLMs) love