OpenAPI

Return Types

Response types, @Returns decorator, error responses, and SSE endpoints.

tsgonest reads your method return types to generate OpenAPI response schemas. In most cases, a TypeScript return type annotation is all you need.

Automatic Return Type Inference

The method's return type annotation is used directly as the response schema:

@Get()
findAll(): UserDto[] { ... }
// Response: 200 with array of UserDto

@Get(':id')
findOne(@Param('id') id: string): UserDto { ... }
// Response: 200 with UserDto

@Post()
create(@Body() body: CreateUserDto): UserDto { ... }
// Response: 201 with UserDto (POST defaults to 201)

Promise Unwrapping

Promise<T> is automatically unwrapped to T:

@Get()
async findAll(): Promise<UserDto[]> { ... }
// Response schema: array of UserDto (Promise is stripped)

Void Responses

Methods that return void produce a response without a content body:

@Delete(':id')
@HttpCode(204)
remove(@Param('id') id: string): void { ... }
// Response: 204 with no content

If no return type annotation is present, tsgonest falls back to TypeScript's type checker inference. This works but can be slower on large codebases. Always prefer explicit return type annotations.

The @Returns Decorator

The @Returns<T>() decorator lets you explicitly specify the response type for OpenAPI generation. This is particularly useful for routes that use @Res() or need to override the inferred type.

Basic Usage

@Get(':id/avatar')
@Returns<Buffer>({ contentType: 'image/png' })
getAvatar(@Param('id') id: string, @Res() res: Response): void {
  // Manual response handling
  const buffer = this.service.getAvatar(id);
  res.set('Content-Type', 'image/png').send(buffer);
}

With Description and Status

@Post()
@Returns<UserDto>({ description: 'The newly created user', status: 201 })
create(@Body() body: CreateUserDto): UserDto { ... }

Options

OptionTypeDescription
contentTypestringOverride the response content type (default: application/json)
descriptionstringDescription for the response in OpenAPI
statusnumberOverride the response status code

@Res() and @Response() Routes

When a route uses @Res() or @Response(), NestJS delegates response handling to the developer. tsgonest cannot infer the response type from the method body, so it defaults to void (no response body in OpenAPI).

// ⚠ OpenAPI response will be empty — tsgonest can't infer what res.json() sends
@Get(':id/download')
download(@Param('id') id: string, @Res() res: Response): void {
  const file = this.service.getFile(id);
  res.sendFile(file.path);
}

To document the actual response, add @Returns<T>():

// ✓ OpenAPI response correctly documents the file response
@Get(':id/download')
@Returns<Buffer>({ contentType: 'application/pdf', description: 'The document PDF' })
download(@Param('id') id: string, @Res() res: Response): void {
  const file = this.service.getFile(id);
  res.sendFile(file.path);
}

tsgonest emits a warning when @Res() is used without @Returns. If you intentionally handle the response manually and don't want to document it, suppress the warning with:

/**
 * @tsgonest-ignore uses-raw-response
 */
@Get('health')
health(@Res() res: Response): void { ... }

StreamableFile Responses

NestJS's StreamableFile is automatically detected and produces an application/octet-stream binary response:

import { StreamableFile } from '@nestjs/common';

@Get('download')
download(): StreamableFile {
  const file = createReadStream(join(process.cwd(), 'report.pdf'));
  return new StreamableFile(file);
}
{
  "200": {
    "description": "OK",
    "content": {
      "application/octet-stream": {
        "schema": { "type": "string", "format": "binary" }
      }
    }
  }
}

To override the content type (e.g., for PDF files), use @Returns:

@Get('invoice')
@Returns<Buffer>({ contentType: 'application/pdf' })
getInvoice(): StreamableFile {
  return new StreamableFile(createReadStream('invoice.pdf'));
}

The @Returns content type always takes precedence over the auto-detected application/octet-stream.

Array Responses

Array return types produce array schemas in the response:

@Get()
findAll(): UserDto[] { ... }
{
  "200": {
    "description": "OK",
    "content": {
      "application/json": {
        "schema": {
          "type": "array",
          "items": { "$ref": "#/components/schemas/UserDto" }
        }
      }
    }
  }
}

Generic wrapper types work the same way:

@Get()
findAll(): PaginatedResponse<UserDto> { ... }
// Produces the full PaginatedResponse schema with UserDto items

Error Responses via @throws

Use the @throws JSDoc tag to document error responses:

/**
 * @throws {400} ValidationError
 * @throws {404} NotFoundError
 * @throws {403} ForbiddenError
 */
@Get(':id')
findOne(@Param('id') id: string): UserDto { ... }

Each @throws adds a response entry to the operation:

{
  "400": {
    "description": "Bad Request",
    "content": {
      "application/json": {
        "schema": { "$ref": "#/components/schemas/ValidationError" }
      }
    }
  },
  "404": {
    "description": "Not Found",
    "content": {
      "application/json": {
        "schema": { "$ref": "#/components/schemas/NotFoundError" }
      }
    }
  },
  "403": {
    "description": "Forbidden",
    "content": {
      "application/json": {
        "schema": { "$ref": "#/components/schemas/ForbiddenError" }
      }
    }
  }
}

The type name after the status code must reference a class or interface in your codebase. tsgonest resolves it and generates the corresponding schema in components/schemas.

SSE Endpoints

Routes decorated with @EventStream() produce text/event-stream responses with discriminated oneOf schemas. Each event variant has a typed contentSchema for the data payload, and an error variant is appended automatically:

import { EventStream, SseEvents } from '@tsgonest/runtime';

type OrderEvents = SseEvents<{
  created: OrderDto;
  cancelled: { id: string; reason: string };
}>;

@EventStream('events')
async *events(): AsyncGenerator<OrderEvents> { ... }
{
  "200": {
    "description": "Server-Sent Events stream",
    "content": {
      "text/event-stream": {
        "schema": {
          "type": "string",
          "format": "event-stream",
          "itemSchema": {
            "oneOf": [
              {
                "type": "object",
                "required": ["data", "event"],
                "properties": {
                  "event": { "type": "string", "const": "created" },
                  "data": {
                    "type": "string",
                    "contentMediaType": "application/json",
                    "contentSchema": { "$ref": "#/components/schemas/OrderDto" }
                  }
                }
              },
              {
                "type": "object",
                "required": ["data", "event"],
                "properties": {
                  "event": { "type": "string", "const": "error" },
                  "data": { "type": "string" }
                }
              }
            ],
            "discriminator": { "propertyName": "event" }
          }
        }
      }
    }
  }
}

See the Server-Sent Events page for full documentation on event types, heartbeat, error handling, and more.

@Sse (legacy)

Routes decorated with NestJS's @Sse() map to GET operations with text/event-stream response content type and use the OpenAPI 3.2 itemSchema field:

@Sse('events')
stream(): Observable<MessageEvent> { ... }
{
  "200": {
    "description": "OK",
    "content": {
      "text/event-stream": {
        "schema": {
          "type": "string",
          "format": "event-stream",
          "itemSchema": { "$ref": "#/components/schemas/MessageEvent" }
        }
      }
    }
  }
}

Custom SSE Types (legacy @Sse)

If you return a custom DTO instead of the built-in MessageEvent with @Sse(), tsgonest adds contentMediaType and contentSchema to describe the event data payload:

interface OrderUpdate {
  orderId: string;
  status: 'pending' | 'shipped' | 'delivered';
  updatedAt: string;
}

@Sse('orders/updates')
orderUpdates(): Observable<OrderUpdate> { ... }

This gives clients and code generators full type information about the SSE event data structure.

Complete Example

documents.controller.ts
@Controller('documents')
export class DocumentsController {
  /**
   * List all documents for the current user.
   * @summary List documents
   * @security bearer
   */
  @Get()
  findAll(@Query() query: DocumentFilterDto): DocumentDto[] { ... }

  /**
   * Get document metadata.
   * @summary Get document
   * @security bearer
   * @throws {404} NotFoundError
   */
  @Get(':id')
  findOne(@Param('id') id: string): DocumentDto { ... }

  /**
   * Download the document file.
   * @summary Download document
   * @security bearer
   * @throws {404} NotFoundError
   */
  @Get(':id/download')
  @Returns<Buffer>({ contentType: 'application/pdf', description: 'The document file' })
  download(@Param('id') id: string, @Res() res: Response): void {
    const doc = this.service.getDocument(id);
    res.set('Content-Type', doc.mimeType).send(doc.buffer);
  }

  /**
   * Create a new document.
   * @summary Create document
   * @security bearer
   * @throws {400} ValidationError
   */
  @Post()
  create(@Body() body: CreateDocumentDto): DocumentDto { ... }

  /**
   * Subscribe to real-time document updates.
   * @summary Document update stream
   * @security bearer
   */
  @Sse(':id/updates')
  updates(@Param('id') id: string): Observable<DocumentUpdate> { ... }

  /**
   * Delete a document permanently.
   * @summary Delete document
   * @security bearer
   * @throws {404} NotFoundError
   */
  @Delete(':id')
  @HttpCode(204)
  remove(@Param('id') id: string): void { ... }
}

On this page