Decorators
Please note that this section only covers decorators that are not described separately, such as @Response or the core parameter decorators introduced in Getting started. For a full overview, please check out the API Reference. Relevant API reference: @Security, @NoSecurity, @Tags, @OperationId, @Deprecated, @Validate, @SpecPath, @Hidden, @Request, @RequestProp, @Inject, @Produces, and @Consumes.
Security
The @Security decorator can be used above controller methods to indicate that there should be authentication before running those methods. As described above, the authentication is done in a file that's referenced in tsoa's configuration. The scheme names are user-defined and must match the names in your OpenAPI security config and authentication module. When using the @Security decorator, you can choose between having one or multiple authentication methods. If you choose to have multiple authentication methods, you can choose between having to pass one of the methods (OR):
@Security('jwt', ['write:pets', 'read:pets'])
@Security('api_key')
@Get('OauthOrAPIkey')
public async GetWithOrSecurity(@Request() request: express.Request): Promise<any> {
}or having to pass all of them (AND):
@Security({
jwt: ['write:pets', 'read:pets'],
api_key: [],
})
@Get('OauthAndAPIkey')
public async GetWithAndSecurity(@Request() request: express.Request): Promise<any> {
}NoSecurity
Use @NoSecurity() when a controller or action should clear inherited or API-wide security requirements.
import { Controller, Get, NoSecurity, Route, Security } from 'tsoa-next'
@Route('users')
@Security('api_key')
export class UsersController extends Controller {
@Get('private')
public async privateEndpoint(): Promise<string> {
return 'private'
}
@Get('public')
@NoSecurity()
public async publicEndpoint(): Promise<string> {
return 'public'
}
}Tags
Tags are defined with the @Tags('tag1', 'tag2', ...) decorator in the controllers and/or in the methods like in the following examples.
import { Controller, Get, Request, Response, Route, Tags } from 'tsoa-next'
@Route('users')
@Tags('User')
export class UsersController extends Controller {
@Get('UserInfo')
@Tags('Info', 'Get')
@Response<{ message: string }>('default', 'Unexpected error')
public async userInfo(@Request() request: { user: { id: number; name: string } }): Promise<{ id: number; name: string }> {
return Promise.resolve(request.user)
}
@Get('EditUser')
@Tags('Edit')
public async editUser(): Promise<string> {
return 'ok'
}
}If you have a project that needs a description and/or external docs for tags, you can configure the internal generators to use the correct tags definitions and external docs by providing a tags property to spec property in tsoa.json.
{
"spec": {
"tags": [
{
"name": "User",
"description": "Operations about users",
"externalDocs": {
"description": "Find out more about users",
"url": "http://swagger.io"
}
}
],
...
},
"routes": {
...
}
}OperationId
Set operationId under an operation's path. Useful for use with OpenAPI code generation tool since this parameter is used to name the function generated in the client SDK.
@Get()
@OperationId('findDomain')
public async find(): Promise<any> {
}Deprecated
OpenAPI allows you to deprecate operations, parameters, and schemas. This lets you indicate that certain endpoint/formats/etc. should no longer be used, while allowing clients time to migrate to the new approach.
To deprecate parts of your API, you can attach the @Deprecated decorator to class properties, methods, and parameters. For constructs that don't support decorators (e.g. interfaces and type aliases), you can use a @deprecated JSDoc annotation. Some examples:
Operations
@Get()
@Deprecated()
public async find(): Promise<any> {
}Parameters (OpenAPI 3+ only)
@Get("v2")
public async findV2(
@Query() text: string,
@Deprecated() @Query() dontUse?: string
): Promise<any> {
}interface QueryParams {
text: string;
sort?: string;
page?: number;
}
@Get("v2")
public async findV2(
@Queries() queryParams: QueryParams
): Promise<any> {
}Schemas (OpenAPI 3+ only)
class CreateUserRequest {
name: string;
@Deprecated() firstName?: string;
constructor(
public emailAddress: string,
@Deprecated() public icqHandle?: string
) {}
}
interface CreateUserResponse {
/** @deprecated */ durationMs?: number;
details: UserDetails;
}
type UserDetails = {
name: string;
/** @deprecated */ firstName?: string;
};Validate
The external schema decorator is named @Validate(...). Use it on controller method parameters when you want a supported external schema library to replace built-in runtime validation for that parameter subtree.
- Supported forms:
@Validate(schema),@Validate('zod', schema),@Validate({ kind: 'zod', schema }) - Supported libraries:
zod,joi,yup,superstruct,io-ts - Supported parameter decorators:
@Body,@BodyProp,@Query,@Queries,@Path,@Header,@FormField,@UploadedFile,@UploadedFiles - OpenAPI generation still comes from your TypeScript types;
@Validate(...)only changes runtime validation
import { Body, Controller, Post, Route, Validate } from 'tsoa-next'
import { z } from 'zod'
type CreateUser = {
name: string
tags: string[]
}
const CreateUserSchema = z.object({
name: z.string().min(3),
tags: z.array(z.string()).min(1),
})
@Route('users')
export class UsersController extends Controller {
@Post()
public create(@Body() @Validate(CreateUserSchema) payload: CreateUser): CreateUser {
return payload
}
}For complete setup notes and examples for every supported validator library, see External Validators.
SpecPath
Use @SpecPath(...) on a controller when you want that controller to expose a spec or documentation endpoint at runtime without reading a generated spec file from local disk.
@SpecPath()defaults to a JSON endpoint at/<controller-path>/spec- Built-in targets:
json,yaml,swagger,redoc,rapidoc - Built-in targets require route generation to have access to the spec config, such as the standard
tsoa spec-and-routesworkflow or a routes config that embedsruntimeSpecConfig - A controller can declare multiple
@SpecPath(...)decorators as long as the resolved paths do not collide - Built-in documentation targets lazy-load optional peer dependencies:
swagger-ui-expressfor Expressswagger-ui-koafor Koahapi-swaggerfor Hapiredocfor Redocrapidocfor RapiDoc
- Custom handlers can return either a
stringor aReadable - Use
@SpecPath(path, options?)to configureSpecPathOptionssuch astarget,cache, and an optionalgate gatecan be a boolean or a function that receives theSpecRequestContextand returns whether the spec should be served for that request- Cache can be disabled with
'none', kept in-process with'memory', or delegated to a customSpecCacheHandler @SpecPath(...)routes are auxiliary and are not added to the generated OpenAPI document
import { Controller, Get, Route, SpecPath } from 'tsoa-next'
@Route('users')
@SpecPath()
@SpecPath('openapi.yaml', { target: 'yaml' })
@SpecPath('docs', { target: 'swagger' })
export class UsersController extends Controller {
@Get()
public list(): string[] {
return []
}
}In that example:
GET /users/specserves the OpenAPI document as JSONGET /users/openapi.yamlserves the same document as YAMLGET /users/docsserves Swagger UI if the runtime-specific peer dependency is installed
You can also provide a custom handler and external cache implementation:
import { Readable } from 'node:stream'
import { Controller, Get, Route, SpecCacheHandler, SpecPath, SpecRequestContext } from 'tsoa-next'
const cacheStore = new Map<string, string>()
const cache: SpecCacheHandler = {
async get(context) {
return cacheStore.get(context.cacheKey)
},
async set(context, value) {
cacheStore.set(context.cacheKey, value)
},
}
async function customDocs(context: SpecRequestContext) {
return Readable.from([await context.getSpecString('json')])
}
@Route('internal')
@SpecPath('spec.json', { target: customDocs, cache })
export class InternalController extends Controller {
@Get('status')
public status() {
return { ok: true }
}
}You can also gate a spec route:
@SpecPath('docs', {
gate: context => {
const headers = (context.request as { headers?: Record<string, string | string[] | undefined> } | undefined)?.headers
return headers?.['x-allow-spec'] === 'true'
},
target: 'swagger',
})When caching is enabled and a custom handler returns a stream, tsoa-next buffers the stream to a string before storing it through the cache handler.
Hidden
Use @Hidden on methods to exclude an endpoint from the generated OpenAPI Specification document.
@Get()
@Hidden()
public async find(): Promise<any> {
}Use @Hidden on controllers to exclude all of their endpoints from the generated OpenAPI Specification document.
import { Controller, Get, Hidden, Post, Route } from 'tsoa-next'
@Route('hidden')
@Hidden()
export class HiddenController extends Controller {
@Get()
public async find(): Promise<any> {}
@Post()
public async create(): Promise<any> {}
}Use on @Query parameters to exclude query params from the generated OpenAPI Specification document. The parameter must either allow undefined or have a default value to be hidden.
@Get()
public async find(
@Query() normalParam: string,
@Query() @Hidden() defaultSecret = true,
@Query() @Hidden() optionalSecret?: string
): Promise<any> {
}Request
To access the request object of express in a controller method use the @Request decorator:
// src/users/usersController.ts
import * as express from 'express'
import { Controller, Get, Path, Request, Route } from 'tsoa-next'
@Route('users')
export class UsersController extends Controller {
@Get('{userId}')
public async getUser(
@Path() userId: number,
@Request() request: express.Request
): Promise<{ id: number; requestedBy?: string }> {
// TODO: implement some code that uses the request as well
return {
id: userId,
requestedBy: request.header('x-requested-by'),
}
}
}To access Koa's request object (which has the ctx object) in a controller method use the @Request decorator:
// src/users/usersController.ts
import * as koa from 'koa'
import { Controller, Get, Path, Request, Route } from 'tsoa-next'
@Route('users')
export class UsersController extends Controller {
@Get('{userId}')
public async getUser(
@Path() userId: number,
@Request() request: koa.Request
): Promise<{ id: number; path: string }> {
const ctx = request.ctx;
return {
id: userId,
path: ctx.path,
}
}
}DANGER
Note that the parameter request does not appear in your OAS file. Use @RequestProp(...) when the value already lives on the underlying runtime request object. Use @Inject() when a parameter is supplied entirely by your own route template or wrapper code and should be omitted from spec generation.
RequestProp
@RequestProp(...) binds a single property from the underlying runtime request object.
import { Controller, Post, RequestProp, Route } from 'tsoa-next'
@Route('request-props')
export class RequestPropsController extends Controller {
@Post('body')
public async getBody(@RequestProp('body') body: { name: string }): Promise<{ name: string }> {
return body
}
}Produces
The @Produces decorator is used to define custom media types for the responses of controller methods in the OpenAPI generator. It allows you to specify a specific media type for each method, without overwriting the default Content-Type response.
Here's an example of how to use the @Produces decorator:
@Route('MediaTypeTest')
@Produces('application/vnd.mycompany.myapp+json')
export class MediaTypeTestController extends Controller {
@Get('users/{userId}')
public async getDefaultProduces(@Path() userId: number): Promise<{ id: number; name: string }> {
this.setHeader('Content-Type', 'application/vnd.mycompany.myapp+json')
return Promise.resolve({
id: userId,
name: 'foo',
})
}
@Get('custom/security.txt')
@Produces('text/plain')
public async getCustomProduces(): Promise<string> {
const securityTxt = 'Contact: mailto: security@example.com\nExpires: 2012-12-12T12:37:00.000Z'
this.setHeader('Content-Type', 'text/plain')
return securityTxt
}
}DANGER
Please note that using @Produces only affects the generated OpenAPI Specification. You must also ensure that you send the correct header using this.setHeader('Content-Type', 'MEDIA_TYPE') in your controller methods.
Consumes
Use @Consumes(...) when an action accepts a non-default request body media type.
import { Body, Consumes, Controller, Post, Response, Route, SuccessResponse } from 'tsoa-next'
@Route('MediaTypeTest')
export class MediaTypeTestController extends Controller {
@Post('custom')
@Consumes('application/vnd.mycompany.myapp.v2+json')
@SuccessResponse('202', 'Accepted', 'application/vnd.mycompany.myapp.v2+json')
@Response<{ message: string }>('400', 'Bad Request', undefined, 'application/problem+json')
public async postCustomConsumes(@Body() body: { name: string }): Promise<{ id: number; name: string }> {
this.setStatus(202)
return {
id: body.name.length,
name: body.name,
}
}
}