--- url: 'https://tsoa-next.dev/introduction.md' --- # Introduction `tsoa-next` is the continuation of the original [`tsoa`](https://github.com/lukeautry/tsoa) project, building on the stable foundation established there by Luke Autry and contributors. It is a framework with an integrated OpenAPI compiler to build Node.js server-side applications using TypeScript. It can target express, hapi, koa and more frameworks at runtime. `tsoa-next` applications are type-safe by default and handle runtime validation seamlessly. In the guides below, `tsoa` usually refers to the CLI command and underlying architecture that `tsoa-next` continues. ## Goal * TypeScript controllers and models as the single source of truth for your API * A valid OpenAPI (formerly Swagger) spec (2.0 or 3.0) is generated from your controllers and models, including: * Paths (e.g. GET /Users) * Definitions based on TypeScript interfaces (models) * Parameters/model properties marked as required or optional based on TypeScript (e.g. myProperty?: string is optional in the OpenAPI spec) * jsDoc supported for object descriptions (most other metadata can be inferred from TypeScript types) * Routes are generated for middleware of choice * Express, Hapi, and Koa currently supported, other middleware can be supported using a simple handlebars template * Seamless runtime validation ## Philosophy * Rely on TypeScript type annotations to generate API metadata if possible * If regular type annotations aren't an appropriate way to express metadata, use decorators * Use jsdoc for pure text metadata (e.g. endpoint descriptions) * Minimize boilerplate * Models are best represented by interfaces (pure data structures), but can also be represented by classes * Runtime validation of `tsoa-next` should behave as closely as possible to the specifications that the generated OpenAPI 2/3 schema describes. Any differences in validation logic are clarified by logging warnings during the generation of the OpenAPI Specification (OAS) and/or the routes. * Please note that by enabling OpenAPI 3 you minimize the chances of divergent validation logic since OpenAPI 3 has a more expressive schema syntax. --- --- url: 'https://tsoa-next.dev/getting-started.md' --- # Getting started **What we will talk about:** \[\[toc]] Relevant API reference: [`Controller`](../reference/tsoa-next/classes/Controller.md), [`@Route`](../reference/tsoa-next/functions/Route.md), [`@Get`](../reference/tsoa-next/functions/Get.md), [`@Path`](../reference/tsoa-next/functions/Path.md), [`@Query`](../reference/tsoa-next/functions/Query.md), [`@Post`](../reference/tsoa-next/functions/Post.md), [`@Body`](../reference/tsoa-next/functions/Body.md), and [`@SuccessResponse`](../reference/tsoa-next/functions/SuccessResponse.md). ::: warning COMPATIBILITY NOTE This guide targets [express](https://expressjs.com) and assumes `tsoa-next`'s current support policy: Node.js 22 or newer. We verify support on Node.js 22, 24, and 26 in CI. Examples below include `npm`, `pnpm`, and `yarn` variants where the command differs. ::: ## Initializing our project ```shell # Create a new folder for our project mkdir tsoa-project cd tsoa-project # Initialize git git init ``` Create a `package.json` and `tsconfig.json` with your package manager of choice: ::: code-group ```shell [npm] npm init -y npm exec tsc -- --init ``` ```shell [pnpm] pnpm init pnpm exec tsc --init ``` ```shell [yarn] yarn init -y yarn exec tsc --init ``` ::: Install the app and TypeScript dependencies with your package manager of choice: ::: code-group ```shell [npm] npm i tsoa-next express npm i -D typescript @types/node @types/express ``` ```shell [pnpm] pnpm add tsoa-next express pnpm add -D typescript @types/node @types/express ``` ```shell [yarn] yarn add tsoa-next express yarn add -D typescript @types/node @types/express ``` ::: Generated routes import from `tsoa-next`, so the package your application installs is also the package used by controllers and generated `RegisterRoutes` files. You can also find the published package on [npm](https://www.npmjs.com/package/tsoa-next). ## Configuring tsoa and typescript ```js // tsoa.json { "entryFile": "src/app.ts", "noImplicitAdditionalProperties": "throw-on-extras", "controllerPathGlobs": ["src/**/*Controller.ts"], "spec": { "outputDirectory": "build", "specVersion": 3 }, "routes": { "routesDir": "build" } } ``` Let's take a look at what we are telling tsoa here: First, we specify where the entry point to our application will be. Most likely, this file will be called `index.ts` or `app.ts`. We will create this file in a second. Afterwards, the top-level `controllerPathGlobs` setting tells tsoa where it can look for controllers so we don't manually have to import them. Next, we tell tsoa how strict excess property checking (to use the TypeScript term) or additionalProperty checking (to use OpenAPI terminology) should be. We can choose to "ignore" additional Properties (the OpenAPI default), remove them during validation ("silently-remove-extras"), or throw an Error back to the Client ("throw-on-extras"). Next, we set the output directory for out OpenAPI specification (OAS) and our `routes.ts` file, which we will talk about later. We set the `specVersion` to `3` so tsoa will generate an OpenAPI v3 specification. You can also use `3.1` when you want OpenAPI 3.1 output. For a full list of all the possible config, take a look at the [API Reference](../reference/tsoa-next/interfaces/Config.md) ::: tip While the default ts config will work for this guide, an improved tsconfig.json would look something like this: ::: details ```jsonc { "compilerOptions": { /* Basic Options */ "incremental": true, "target": "es2022", "module": "commonjs", "outDir": "build", /* Strict Type-Checking Options */ "strict": true, "noImplicitAny": true, "strictNullChecks": true, "strictFunctionTypes": true, "strictBindCallApply": true, "strictPropertyInitialization": true, "noImplicitThis": true, "alwaysStrict": true, /* Additional Checks */ "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, /* Module Resolution Options */ "moduleResolution": "node10", "esModuleInterop": true, /* Experimental Options */ "experimentalDecorators": true, // emitDecoratorMetadata is not needed by tsoa-next itself /* Advanced Options */ "forceConsistentCasingInFileNames": true, }, } ``` ::: ## Defining our first model If you already have an OpenAPI Specification, you can use existing OpenAPI tooling to generate your Models or Interfaces. Otherwise, let's define a `User` Interface in `src/users/user.ts`. ```typescript export interface User { id: number email: string name: string status?: 'Happy' | 'Sad' phoneNumbers: string[] } ``` Before we start defining our Controller, it's usually a good idea to create a Service that handles interaction with our Models instead of shoving all that logic into the controller layer. ```ts // src/users/usersService.ts import { User } from './user' // A post request should not contain an id. export type UserCreationParams = Pick export class UsersService { public get(id: number, name?: string): User { return { id, email: 'jane@doe.com', name: name ?? 'Jane Doe', status: 'Happy', phoneNumbers: [], } } public create(userCreationParams: UserCreationParams): User { return { id: Math.floor(Math.random() * 10000), // Random status: 'Happy', ...userCreationParams, } } } ``` ## Defining a simple controller ```typescript {15,17,19,20,25,26,28} // src/users/usersController.ts import { Body, Controller, Get, Path, Post, Query, Route, SuccessResponse } from 'tsoa-next' import { User } from './user' import { UsersService, UserCreationParams } from './usersService' @Route('users') export class UsersController extends Controller { @Get('{userId}') public async getUser(@Path() userId: number, @Query() name?: string): Promise { return new UsersService().get(userId, name) } @SuccessResponse('201', 'Created') // Custom success response @Post() public async createUser(@Body() requestBody: UserCreationParams): Promise { this.setStatus(201) // set return status 201 new UsersService().create(requestBody) return } } ``` Let's take a step back and talk about what's going on here. As you can hopefully already tell, we are defining a `/users/` route using the [`@Route()`](../reference/tsoa-next/functions/Route.md) decorator above our controller class. Additionally, we define 2 methods: `getUser` and `createUser`. The [`@Get()`](../reference/tsoa-next/functions/Get.md) decorator in combination with our base route `/users/` will tell tsoa to invoke this method for every *GET* request to `/users/{{userId}}`, where *{userId}* is a template. ::: tip OpenAPI Path Templating Routing in tsoa is closely mirroring OpenAPI's path templating for compatibility reasons. Path templating refers to the usage of template expressions, delimited by curly braces ({}), to mark a section of a URL path as replaceable using path parameters. ::: Under the hood, this would be like defining `app.get('users/:userId')`. While express allows you to use regex-ish route definitions, we prefer to split the routing and the validation more clearly. Because you're asking for the *id* to be a *number* by using the [`@Path()`](../reference/tsoa-next/functions/Path.md) decorator with an `userId` of type number, tsoa will reject passing i.e. a *string* here. Similarly, if you want to accept a *string* with a certain pattern, you can do that using JSON Schema annotations. You can learn more about that [here](#what-s-next). tsoa-next supports the usual path, query, header, and body decorators, and also supports multipart form-data decorators such as [`@FormField()`](../reference/tsoa-next/functions/FormField.md), [`@UploadedFile()`](../reference/tsoa-next/functions/UploadedFile.md), and [`@UploadedFiles()`](../reference/tsoa-next/functions/UploadedFiles.md), plus runtime-only injected parameters such as [`@Request()`](../reference/tsoa-next/functions/Request.md) and [`@Res()`](../reference/tsoa-next/functions/Res.md). ::: tip If the parameter name is equal to the http message parameter, you may omit the argument to the decorators, otherwise you may provide an argument: ```ts @Query('my-query') myQuery: string; ``` ::: A full list of all the decorators can be found [here](./decorators). ::: warning Caveat Always use a named export (`export class C`) on the controller class in order for tsoa to correctly pick it up. Default exports (`export default class C`) are currently not supported. ::: ## Creating our express server Let's now create an `app.ts` and a `server.ts` file in our source directory like this: ```ts // src/app.ts import express, { json, urlencoded } from 'express' import { RegisterRoutes } from '../build/routes' export const app = express() // Use body parser to read sent json payloads app.use( urlencoded({ extended: true, }), ) app.use(json()) RegisterRoutes(app) ``` ```ts // src/server.ts import { app } from './app' const port = process.env.PORT || 3000 app.listen(port, () => console.log(`Example app listening at http://localhost:${port}`)) ``` ## Building the generated files At this point you may have noticed that TypeScript will not find the `RegisterRoutes` import from `build/routes`. That is because we have not asked tsoa to generate the routes file and OpenAPI spec yet. Let's do that now: ```shell mkdir -p build # Create the build directory if it doesn't exist ``` ::: code-group ```shell [npm] npm exec tsoa -- spec-and-routes ``` ```shell [pnpm] pnpm exec tsoa spec-and-routes ``` ```shell [yarn] yarn exec tsoa spec-and-routes ``` ::: Now your generated files should have been created and you can compile TypeScript and start your server: ::: code-group ```shell [npm] npm exec tsc -- --outDir build --experimentalDecorators ``` ```shell [pnpm] pnpm exec tsc --outDir build --experimentalDecorators ``` ```shell [yarn] yarn exec tsc --outDir build --experimentalDecorators ``` ::: ```shell node build/src/server.js ``` ::: tip You may want to add these scripts to your `package.json` at this point: ```js "main": "build/src/server.js", "scripts": { "build": "tsoa spec-and-routes && tsc", "start": "node build/src/server.js" }, ``` ::: ## What's next? * Manually invoking `tsc` and `tsoa routes` in development isn't very convenient. * Inspecting our first OpenAPI specification and supercharging our feedback loop by serving an up-to-date version of SwaggerUI during development. We can improve that using [live reloading](./live-reloading). * Improving our response for validation errors using proper [error handling](./error-handling) * Using [Descriptions](./descriptions), [Examples](./examples) and [Annotations](./annotations) for advanced validation and better documentation --- --- url: 'https://tsoa-next.dev/generating.md' --- # Generating Routes and OAS Relevant API reference: [`Config`](../reference/tsoa-next/interfaces/Config.md), [`generateRoutes`](../reference/@tsoa-next/cli/functions/generateRoutes.md), [`generateSpec`](../reference/@tsoa-next/cli/functions/generateSpec.md), [`generateSpecAndRoutes`](../reference/@tsoa-next/cli/functions/generateSpecAndRoutes.md), [`ExtendedRoutesConfig`](../reference/@tsoa-next/cli/interfaces/ExtendedRoutesConfig.md), and [`ExtendedSpecConfig`](../reference/@tsoa-next/cli/interfaces/ExtendedSpecConfig.md). ## Using CLI ### Basic Commands ```bash # generate OAS tsoa spec # generate routes tsoa routes # discover configs and update only stale route and OpenAPI outputs tsoa generate # fail when generated outputs are stale without writing files tsoa check # discover config files beneath the current directory tsoa discover # discover config files beneath a path or glob tsoa discover "packages/*" ``` ### Options #### OpenAPI Specification (OAS) generation ``` Usage: tsoa spec [options] Options: --configuration, -c tsoa configuration file; default is tsoa.json in the working directory [string] --discover discover tsoa config files using a path or glob before running the command [string] --host API host [string] --basePath Base API path [string] ``` #### Route generation ``` Usage: tsoa routes [options] Options: --configuration, -c tsoa configuration file; default is tsoa.json in the working directory [string] --discover discover tsoa config files using a path or glob before running the command [string] --basePath Base API path [string] ``` #### Config discovery ``` Usage: tsoa discover [pathOrGlob] ``` * `discover` searches beneath the provided path, or beneath the current working directory when no argument is provided. * Glob inputs are supported, so commands like `tsoa discover "packages/*"` or `tsoa spec --discover "services/*"` will expand matching roots first. * Discovery recognizes these conventional config filenames: * `tsoa.json` * `tsoa.yaml` * `tsoa.yml` * `tsoa.config.js` * `tsoa.config.cjs` * `spec`, `routes`, and `spec-and-routes` can fan out across all discovered configs: ```bash tsoa spec --discover "packages/*" tsoa routes --discover "./services" tsoa spec-and-routes --discover . ``` #### Change-aware generation and CI checks `generate` and `check` discover conventional tsoa config files automatically. Both commands search beneath the current working directory by default, or beneath an optional path or glob: ```bash # Generate routes and OpenAPI specs for every discovered config. # Existing files are written only when their generated content changed. tsoa generate tsoa generate "services/*" # Compare generated content with the files on disk without creating directories # or writing files. The command exits non-zero when any output is missing or stale. tsoa check tsoa check "services/*" ``` Each discovered config used by these combined commands must include both `spec` and `routes` sections. `check` reports the stale output paths and suggests running `tsoa generate`, which makes it suitable as a pull request CI gate. Because config discovery is automatic, `generate` and `check` ignore the `--configuration` (`-c`) and `--discover` options and print a warning when either is supplied. Use the optional `[pathOrGlob]` argument to limit discovery instead. Change-aware generation cannot safely control file writes performed by a custom `routes.routeGenerator`, so `generate` and `check` reject those configs. The existing `routes` and `spec-and-routes` commands continue to support custom route generators. You can find the Reference for the tsoa configuration file [here](../reference/tsoa-next/interfaces/Config.md) For information on the configuration object (`tsoa.json`), you may also be interested in: [`Config` interface reference](../reference/tsoa-next/interfaces/Config.md) [Configuration sample](https://github.com/tsoa-next/tsoa-next/blob/main/tests/tsoa.json) ## Programmatic Import programmatic generation APIs from `tsoa-next/cli`. The root `tsoa-next` entrypoint is runtime-only and should be used for decorators and runtime helpers. ```typescript import { generateRoutes, generateSpec, generateSpecAndRoutes, ExtendedRoutesConfig, ExtendedSpecConfig } from 'tsoa-next/cli' ;(async () => { const specOptions: ExtendedSpecConfig = { basePath: '/api', entryFile: './api/server.ts', specVersion: 3, outputDirectory: './api/dist', controllerPathGlobs: ['./routeControllers/**/*Controller.ts'], } const routeOptions: ExtendedRoutesConfig = { basePath: '/api', entryFile: './api/server.ts', routesDir: './api', } await generateSpec(specOptions) await generateRoutes(routeOptions) // Or generate both outputs from one shared metadata pass: await generateSpecAndRoutes({ configuration: { entryFile: './api/server.ts', controllerPathGlobs: ['./routeControllers/**/*Controller.ts'], spec: { outputDirectory: './api/dist', specVersion: 3.1, }, routes: { routesDir: './api', }, }, }) })() ``` **Note:** If you use tsoa programmatically, please be aware that tsoa's methods can (under rare circumstances) change in minor and patch releases. But if you are using tsoa in a .ts file, then TypeScript will help you migrate to any changes. We reserve this right to change what are essentially our internal methods so that we can continue to provide incremental value to the majority user (our CLI users). The CLI however will only receive breaking changes during a major release. --- --- url: 'https://tsoa-next.dev/live-reloading.md' --- # Live reloading ::: warning COMPATIBILITY NOTE This guide targets [express](https://expressjs.com) and assumes `tsoa-next`'s current support policy: Node.js 22 or newer. We verify support on Node.js 22, 24, and 26 in CI. Examples below include `npm`, `pnpm`, and `yarn` variants where the command differs. We assume your setup is similar to the one recommended for [getting started](/getting-started) ::: Relevant API reference: [`@SpecPath`](../reference/tsoa-next/functions/SpecPath.md), [`SpecPathOptions`](../reference/tsoa-next/interfaces/SpecPathOptions.md), [`SpecRequestContext`](../reference/tsoa-next/interfaces/SpecRequestContext.md), and [`SpecCacheHandler`](../reference/tsoa-next/interfaces/SpecCacheHandler.md). ::: tip We will use [nodemon](https://nodemon.io/) and [ts-node](https://github.com/TypeStrong/ts-node) for live reloading, but any tool that allows us to hook into the reloading process will do. Alternatives may, i.e. be a combination of `tsc -w` and triggering `tsoa spec-and-routes` using [`onchange`](https://www.npmjs.com/package/onchange). ::: **What we will talk about:** \[\[toc]] ## Reloading Code ### Installing nodemon and ts-node ::: code-group ```bash [npm] npm i -D nodemon ts-node concurrently ``` ```bash [pnpm] pnpm add -D nodemon ts-node concurrently ``` ```bash [yarn] yarn add -D nodemon ts-node concurrently ``` ::: ### Creating a nodemon config Now, let's create a `nodemon.json` inside the root folder of our project that looks like this: ```json { "exec": "ts-node src/server.ts", "watch": ["src"], "ext": "ts" } ``` ### Adding a dev script Let's automatically start this setup with your package manager's `dev` script (`npm run dev`, `pnpm dev`, or `yarn dev`), and, while we're at it, add `build` and `start` commands in our `package.json`: ```diff { "name": "starter", "version": "0.0.1", + "scripts": { + "dev": "concurrently \"nodemon\" \"nodemon -x tsoa spec-and-routes\"", + "build": "tsoa spec-and-routes && tsc", + "start": "node build/src/server.js" + }, "dependencies": { // ... } ``` ## Supercharging our developer experience with `@SpecPath` [`@SpecPath(...)`](../reference/tsoa-next/functions/SpecPath.md) lets a controller expose a live spec or docs endpoint without reading `swagger.json` or `openapi.yaml` from disk at request time. That makes it a good fit for development workflows where you want the generated documentation to stay in sync with the same controller metadata your routes already use. ### Installing a docs UI peer Pick the docs UI target you want to use: * Express: `npm i swagger-ui-express` / `pnpm add swagger-ui-express` / `yarn add swagger-ui-express` * Koa: `npm i swagger-ui-koa` / `pnpm add swagger-ui-koa` / `yarn add swagger-ui-koa` * Hapi: `npm i hapi-swagger` / `pnpm add hapi-swagger` / `yarn add hapi-swagger` * Redoc: `npm i redoc` / `pnpm add redoc` / `yarn add redoc` * RapiDoc: `npm i rapidoc` / `pnpm add rapidoc` / `yarn add rapidoc` ### Exposing a controller-scoped docs endpoint Attach one or more `@SpecPath(...)` decorators to an existing controller: ```ts 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 [] } } ``` This gives you: * `GET /users/spec` for JSON * `GET /users/openapi.yaml` for YAML * `GET /users/docs` for Swagger UI Because the docs endpoint is generated from the same runtime metadata as your routes, it stays current as you edit controllers and re-run `tsoa spec-and-routes`. ### Inspecting the Documentation Now, when we navigate to localhost:3000/users/docs, we should see a current reflection of our API. ![SwaggerUI](/docs-images/SwaggerUI.png) ### Sending requests through Swagger UI We can select endpoints, click the "Try it out" button and submit some data by filling out the form. When we hit "Execute", that request will be sent to our server and the response will be displayed below the form. ![SwaggerUI Response](/docs-images/SwUi-Response.png) ### Other built-in targets If you prefer a different UI, change the `target` option: * `@SpecPath('docs', { target: 'redoc' })` * `@SpecPath('docs', { target: 'rapidoc' })` If you need a fully custom response, pass a handler in `target` instead. You can also add `cache` and `gate` in the same options object. --- --- url: 'https://tsoa-next.dev/error-handling.md' --- # Error Handling ::: warning COMPATIBILITY NOTE This guide targets [express](https://expressjs.com) and assumes `tsoa-next`'s current support policy: Node.js 22 or newer. We verify support on Node.js 22, 24, and 26 in CI. Examples in the linked setup guides include `npm`, `pnpm`, and `yarn` variants where the command differs. This guide assumes you followed the [getting started guide](./getting-started) or have a similar setup. ::: Relevant API reference: [`ValidateError`](../reference/tsoa-next/classes/ValidateError.md), [`@Response`](../reference/tsoa-next/functions/Response.md), [`@Res`](../reference/tsoa-next/functions/Res.md), [`TsoaResponse`](../reference/tsoa-next/type-aliases/TsoaResponse.md), and [`Controller`](../reference/tsoa-next/classes/Controller.md). As you may have noticed after following all the steps from the [getting started guide](./getting-started), our server does not allow for invalid parameters, but the response isn't very ideal yet. ![Current Error Response](/docs-images/errors-server.png) For the Client, it looks something like this: ![Client Error Response](/docs-images/errors-client.png) ## Setting up error handling ### Handling Validation Errors Let's first make sure that, whenever the Client triggers a Validation Error, instead of printing the stack trace, instead we show a properly formatted json response. At the end of our `app.ts`, after the call to `RegisterRoutes(app)`, we'll add a global express error handler: ```ts import express, { Response as ExResponse, Request as ExRequest, NextFunction } from 'express' import { ValidateError } from 'tsoa-next' // ... app.use(function errorHandler(err: unknown, req: ExRequest, res: ExResponse, next: NextFunction): ExResponse | void { if (err instanceof ValidateError) { console.warn(`Caught Validation Error for ${req.path}:`, err.fields) return res.status(422).json({ message: 'Validation Failed', details: err?.fields, }) } if (err instanceof Error) { return res.status(500).json({ message: 'Internal Server Error', }) } next() }) ``` Now, the same request will respond like this: ![Client Error with handler](/docs-images/errors-json-client.png) Additionally, our console will show: ![Server Error with handler](/docs-images/errors-json-server.png) ### Handling missing routes In order to handle missing urls more gracefully, we can add a "catch-all" route handler: ```ts // app.ts import express, { Response as ExResponse, Request as ExRequest, NextFunction } from 'express' // ... RegisterRoutes(app) app.use(function notFoundHandler(_req, res: ExResponse) { res.status(404).send({ message: 'Not Found', }) }) app.use(function errorHandler( // ... ``` ## Specifying error response types for OpenAPI If you check out the Documentation endpoint, you'll notice that we don't have any documentation for our Errors yet. Since TypeScript does not check throwing Errors, tsoa can't infer the type of response we're sending out in these cases. ::: warning Use the `@Response` decorator exported by `tsoa-next`, not Express's `Response` type. Aliasing the tsoa-next import is fine, but it still needs to resolve to the tsoa-next decorator. ::: However, we have a way for you to manually specify these returns: ```ts import { Body, Controller, Post, Route, Response, SuccessResponse } from 'tsoa-next' import { User } from './user' import { UsersService, UserCreationParams } from './usersService' interface ValidateErrorJSON { message: string details: { [name: string]: unknown } } @Route('users') export class UsersController extends Controller { // more code here @Response(422, 'Validation Failed') @SuccessResponse('201', 'Created') // Custom success response @Post() public async createUser(@Body() requestBody: UserCreationParams): Promise { this.setStatus(201) // set return status 201 new UsersService().create(requestBody) return } } ``` This should make our docs show something like this: ![SwaggerUI showing our 422 Response](/docs-images/err-422-swui.png) ::: tip OpenAPI allows matching status codes such as '2xx' or matching all codes using 'default'. tsoa will support this: ```ts @Response('default', 'Unexpected error') @Get('Response') public async getResponse(): Promise { return new ModelService().getModel() } ``` ::: ## Typechecked alternate responses In recent versions of tsoa, we have the option to inject a framework-agnostic responder function into our function that we can call to formulate a response that does not comply with the return type of our controller method/status code and headers (which is used for the success response). This is especially useful to reply with an error response without the risk of type mismatches associated with throwing errors. In order to inject one/more responders, we can use the `@Res()` decorator: ```ts import { Route, Controller, Get, Query, Res, TsoaResponse } from 'tsoa-next' @Route('/greeting') export class GreetingsController extends Controller { /** * @param notFoundResponse The responder function for a not found response */ @Get('/') public async greet(@Query() name?: string, @Res() notFoundResponse: TsoaResponse<404, { reason: string }>): Promise { if (!name) { return notFoundResponse(404, { reason: "We don't know you yet. Please provide a name" }) } return `Hello, ${name}` } } ``` --- --- url: 'https://tsoa-next.dev/descriptions.md' --- # Descriptions While tsoa can extract a lot of information from your TypeScript type annotations, that can only get us so far in terms of documenting our code. In order stay true to our efforts to avoid code duplication, tsoa uses JSDoc based annotations whenever we want to describe information which is not part of the type system. ::: tip tsoa does not check if you provide descriptions. We recommend using a linter (we love [Spectral](https://stoplight.io/open-source/spectral)) to ensure your specifications aren't just correct, but also contain descriptions and correct [examples](./examples). ::: A great example for this are descriptions. You'd most likely agree that endpoint descriptions either in text or markdown are very helpful for consumers get a better sense of an API Endpoint through a short description as part of a rendered documentation. But developers like you also benefit from JSDoc, which is often displayed directly in your editor when hovering over a method you may not be familiar with. Spoiler: tsoa makes both of these things possible. ## Endpoint descriptions One of the most helpful kind of descriptions are method descriptions, or, in HTTP terminology, endpoint descriptions. ```ts {3-6} @Route("users") export class UsersController extends Controller { /** * Retrieves the details of an existing user. * Supply the unique user ID from either and receive corresponding user details. */ @Get("{userId}") public async getUser( @Path() userId: number, @Query() name?: string ): Promise { return new UsersService().get(userId, name); } } ``` By hovering over the name of the method, we can already see the result in our editor: ![Method description](/docs-images/jsdoc-method.png) But that's only half of the benefit: ![SwaggerUI endpoint descriptions](/docs-images/swui-endpoint-description.png) The OAS reflects this change as well, and so will the documentation rendered from that spec! ## Parameter descriptions But why stop there? [JSDoc also offers parameter descriptions](https://jsdoc.app/tags-param.html), let's see that in action: ```ts {6,7} @Route("users") export class UsersController extends Controller { /** * Retrieves the details of an existing user. * Supply the unique user ID from either and receive corresponding user details. * @param userId The user's identifier * @param name Provide a username to display */ @Get("{userId}") public async getUser( @Path() userId: number, @Query() name?: string ): Promise { return new UsersService().get(userId, name); } } ``` ## Model descriptions We can also render descriptions at the model level (models are interfaces or classes or type aliases): ```ts {1-6} /** * User objects allow you to associate actions performed * in the system with the user that performed them. * The User object contains common information across * every user in the system regardless of status and role. */ export interface User { id: number; email: string; name: string; status?: "Happy" | "Sad"; phoneNumbers: string[]; } ``` ::: tip With the introduction of Type Aliases in tsoa 3, you can use this as a very powerful pattern. Let's assume for a second that our API handles Users identified by a UUID. Usually, uuids are sent as strings, however, ideally, we want to make sure we say uuid when we want uuids. That said, duplicating the description all over the code is a lot of effort, let's see how we can do better: ```ts {1,2,3,4,5} /** * Stringified UUIDv4. * See [RFC 4112](https://tools.ietf.org/html/rfc4122) * @pattern [0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-4[0-9A-Fa-f]{3}-[89ABab][0-9A-Fa-f]{3}-[0-9A-Fa-f]{12} * @format uuid */ export type UUID = string; ``` ```ts /** * User objects allow you to associate actions performed in the system with the user that performed them. * The User object contains common information across every user in the system regardless of status and role. */ export interface User { id: UUID; email: string; name: string; status?: "Happy" | "Sad"; phoneNumbers: string[]; } ``` Now, we define `UUID` as a reusable type alias. Modern editors will nicely display the information text when we hover over references ![JSDoc Type Alias](/docs-images/jsdoc-alias.png) tsoa will translate this to a reusable component that can be referenced every time you use that type alias: ```yaml components: schemas: UUID: type: string description: "Stringified UUIDv4.\nSee [RFC 4112](https://tools.ietf.org/html/rfc4122)" pattern: "[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-4[0-9A-Fa-f]{3}-[89ABab][0-9A-Fa-f]{3}-[0-9A-Fa-f]{12}" User: description: "User objects allow you to associate actions performed in the system with the user that performed them.\nThe User object contains common information across every user in the system regardless of status and role." properties: id: $ref: "#/components/schemas/UUID" ``` Which will look like like this when rendered: ![Rendered](/docs-images/swui-alias.png) ## Property descriptions ::: warning You may expect to see a description for the `id` if you set one. However, since it'll be transformed to a reference to the UUID schema, the description must be ignored, since any properties that are placed next to *$ref* (OpenAPI's mechanism to link to the UUID schema) must be ignored. For more info, check out the relevant parts of the [OpenAPI Specification](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#schemaObject) and [JSON Schema Core](https://tools.ietf.org/html/draft-wright-json-schema-00#section-7) ::: ```ts {8,9,10} /** * User objects allow you to associate actions performed in the system with the user that performed them. * The User object contains common information across every user in the system regardless of status and role. */ export interface User { id: UUID; /** * The email the user used to register his account */ email: string; name: string; status?: "Happy" | "Sad"; phoneNumbers: string[]; } ``` ## Summaries tsoa uses short descriptions provided via the JSDoc ![@summary](https://jsdoc.app/tags-summary.html) annotation and will use it as the summary in the OpenAPI doc: ```ts {5} /** * A very long, verbose, wordy, long-winded, tedious, verbacious, tautological, * profuse, expansive, enthusiastic, redundant, flowery, eloquent, articulate, * loquacious, garrulous, chatty, extended, babbling description. * @summary A concise summary. */ @Get('SummaryMethod') public async summaryMethod(): Promise { return new ModelService().getModel(); } ``` --- --- url: 'https://tsoa-next.dev/examples.md' --- # Examples Relevant API reference: [`@Example`](../reference/tsoa-next/functions/Example.md), [`@Response`](../reference/tsoa-next/functions/Response.md), [`@SuccessResponse`](../reference/tsoa-next/functions/SuccessResponse.md), and [`Controller`](../reference/tsoa-next/classes/Controller.md). ## Runnable example apps For end-to-end sample apps and framework-specific setups, use the companion [tsoa-next/playground](https://github.com/tsoa-next/playground) repository. That repo is the dedicated home for runnable `tsoa-next` scenarios across multiple server integrations as examples are added there. This guide focuses on OpenAPI examples and JSDoc example metadata inside a codebase. Reach for the playground repo when you want a full application you can clone, install, and run. Study after study shows that examples are a crucial part of learning new APIs ([1](https://www.cs.mcgill.ca/~martin/papers/software2009a.pdf), [2](https://sigdoc.acm.org/cdq/how-developers-use-api-documentation-an-observation-study/), [3](https://ase.cpsc.ucalgary.ca/wp-content/uploads/2018/05/A-Study-of-the-Effectiveness-of-Usage-Examples-in-REST-API-Documentation.pdf)). While certain issues, like type mismatches can be avoided by inferring examples from the JSON Schema (like the examples SwaggerUI automatically generates\*), it's often a lot more intuitive if we provide certain examples ourselves. \* Which is limited as well, i.e. patterns will be ignored, and just sending the string "string" every time is somewhat suboptimal if that string actually carries meaning. ::: tip tsoa does not (yet) check your JSDoc examples. Incorrect examples will not break your compilation, because OpenAPI [explicitly allows anything](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#fixed-fields-20). You may also just want to demonstrate tsoa's validation :smirk: We recommend using a linter (we love [Spectral](https://stoplight.io/open-source/spectral)) to ensure your specifications aren't just correct, but also contain [descriptions](./descriptions) and correct examples. ::: ::: warning OpenAPI 2 only allows one example per model/property/parameter. If you defined more than one example in OpenAPI 2, tsoa will only apply first one as value. OpenAPI 3 examples is now supported! ::: ## Response examples In order to provide an example response, tsoa offers a [`@Example()`](../reference/tsoa-next/functions/Example.md) Decorator. ::: tip Providing the type you're writing the example for as a type argument `T` to ```ts @Example(example: T) ``` is not necessary, but may help you catch bugs. ::: This decorator is used to specify a response for the default response, but you can add examples for other responses ([`@Response()`](../reference/tsoa-next/functions/Response.md), used for additional responses, often caused by [errors](./error-handling#specifying-error-response-types-for-openapi) by providing them as the third argument as well. ### Default response ```ts {3-9} @Route("users") export class UsersController extends Controller { @Example({ id: "52907745-7672-470e-a803-a2f8feb52944", name: "tsoa user", email: "hello@tsoa.com", phoneNumbers: [], status: "Happy", }) @Get("{userId}") public async getUser( @Path() userId: UUID, @Query() name: string ): Promise { return new UsersService().get(userId, name); } } ``` ### Additional Responses ```ts {9-17} @Route("users") export class UsersController extends Controller { /** * Add a new user. Remember that the demo API will not persist this data. * */ @Post() @SuccessResponse("201", "Created") // Custom success response @Response(422, "Validation Failed", { message: "Validation failed", details: { requestBody: { message: "id is an excess property and therefore not allowed", value: "52907745-7672-470e-a803-a2f8feb52944", }, }, }) public async createUser( @Body() requestBody: UserCreationParams ): Promise { this.setStatus(201); // set return status 201 new UsersService().create(requestBody); return; } } ``` ## Parameter examples ::: warning You may expect to see an example for a type reference (to a type alias, interface or a class) if you set one. However, since it'll be transformed to a reference (*$ref*) to the schema, the example must be ignored, since any properties that are placed next to *$ref* (OpenAPI's mechanism to link to the UserCreationParams schema) must be ignored. For more info, check out the relevant parts of the [OpenAPI Specification](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#schemaObject) and [JSON Schema Core](https://tools.ietf.org/html/draft-wright-json-schema-00#section-7) ::: ```ts {4-5} @Route("users") export class UsersController extends Controller { /** * @example userId "52907745-7672-470e-a803-a2f8feb52944" * @example userId "e77ef155-bd12-46f0-8559-bf55f6dd4c63" */ @Get("{userId}") public async getUser( @Path() userId: UUID, @Query() name: string ): Promise { return new UsersService().get(userId, name); } } ``` ## Model examples ::: warning Both OpenAPI 2 and 3 supports only single example in model. If you use more than one example, it will only apply the first one. ::: ```ts {5} /** * Stringified UUIDv4. * See [RFC 4112](https://tools.ietf.org/html/rfc4122) * @pattern [0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-4[0-9A-Fa-f]{3}-[89ABab][0-9A-Fa-f]{3}-[0-9A-Fa-f]{12} * @example "52907745-7672-470e-a803-a2f8feb52944" */ export type UUID = string; ``` ::: warning Any example with object or array should be in correct JSON-format. Otherwise, tsoa will throws error while generating OAS. ::: ```ts {6-10} /** * User objects allow you to associate actions performed in the system with the user that performed them. * The User object contains common information across every user in the system regardless of status and role. * * * @example { * "id": "52907745-7672-470e-a803-a2f8feb52944", * "name": "John Doe", * "phoneNumbers": [] * } */ export interface User { id: UUID; /** * The email the user used to register his account */ email?: string; name: string; status?: "Happy" | "Sad"; phoneNumbers: string[]; } ``` ## Property examples ::: warning You may expect to see an example for the `id` if you set one. However, since it'll be transformed to a reference to the UUID schema, the example must be ignored, since any properties that are placed next to *$ref* (OpenAPI's mechanism to link to the UUID schema) must be ignored. For more info, check out the relevant parts of the [OpenAPI Specification](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#schemaObject) and [JSON Schema Core](https://tools.ietf.org/html/draft-wright-json-schema-00#section-7) ::: ```ts {11-13} export interface User { id: UUID; /** * The email the user used to register his account */ email?: string; name: string; /** * @example "Happy" */ status?: "Happy" | "Sad"; phoneNumbers: string[]; } ``` --- --- url: 'https://tsoa-next.dev/annotations.md' --- # JSON Schema / tsoa keyword annotations Under the hood, OpenAPI heavily relies on JSON Schema Draft 00 for all the data model specifications. JSON Schema Draft 00 defines data types that are not implemented in TypeScript. A great example are integers. If we want to communicate that a number must be an integer, tsoa will specify this in the OAS and validate incoming requests against that. ::: warning As always, *$ref* restrictions apply ::: In general, the JSDoc notation is very similar each time: ``` @ * ? ``` Examples: ```typescript {3,4,8,12} interface CustomerDto { /** * @isInt we would kindly ask you to provide a number here * @minimum 18 minimum age is 18 */ age: number; /** * @minItems 1 at least 1 category is required */ tags: string[]; /** * @pattern ^(.+)@(.+)$ please provide correct email */ email: string; } ``` ::: tip For parameters, use the `@ * ?` syntax in your JSDoc (similar to [descriptions](#parameter-descriptions) or [examples](#parameter-examples)) ::: ## List of supported keywords (with arguments) [Click here for the list of keywords supported by OpenAPI 3](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#properties) ### Generic * `@default` * `@format` ::: danger Formats will generally not be validated, except for `format: date(time)`, which will automatically be generated for TS type `Date`. ::: ### Date * `@isDateTime ` for setting custom error messages * `@isDate ` for setting custom error messages * `@minDate ` * `@maxDate ` ### Numeric * `@isInt ` **tsoa special** since TS does not know integer as a type * `@isFloat ` **tsoa special** since TS does not know float as a type * `@isLong ` * `@isDouble ` * `@minimum ` * `@maximum ` * `@exclusiveMinimum ` * `@exclusiveMaximum ` For generated specs, Swagger 2.0 and OpenAPI 3.0 emit boolean `exclusiveMinimum` / `exclusiveMaximum` modifiers alongside `minimum` / `maximum`, while OpenAPI 3.1 emits numeric `exclusiveMinimum` / `exclusiveMaximum` values directly. ### String * `@isString ` for setting custom error messages * `@minLength ` * `@maxLength ` * `@pattern ` ### Array * `@isArray ` for setting custom error messages * `@minItems ` * `@maxItems ` * `@uniqueItems ` ### Boolean * `@isBoolean ` for setting custom error messages --- --- url: 'https://tsoa-next.dev/custom-middlewares.md' --- # Custom Middlewares The `@Middlewares` decorator is used to apply custom middleware to an endpoint in your TypeScript code. This middleware intercepts incoming HTTP requests before they reach the endpoint and allows you to perform additional operations or modifications. It provides support for Express, Koa, and Hapi middlewares. Relevant API reference: [`@Middlewares`](../reference/tsoa-next/functions/Middlewares.md), [`@Request`](../reference/tsoa-next/functions/Request.md), [`Controller`](../reference/tsoa-next/classes/Controller.md), [`@Route`](../reference/tsoa-next/functions/Route.md), and [`@Get`](../reference/tsoa-next/functions/Get.md). ## Example ```ts import type { NextFunction, Request, Response } from 'express' import { Controller, Get, Middlewares, Request as TsoaRequest, Route } from 'tsoa-next' async function customMiddleware(req: Request, _res: Response, next: NextFunction) { req.headers['x-middleware-hit'] = 'true' next() } @Route('examples') export class ExampleController extends Controller { @Get('custom-middleware') @Middlewares(customMiddleware) public async exampleGetEndpoint(@TsoaRequest() req: Request): Promise<{ middlewareHit: boolean }> { return { middlewareHit: req.header('x-middleware-hit') === 'true', } } } ``` ## Execution Flow When an HTTP request is made to the endpoint decorated with `@Middlewares`, the execution flow is as follows: The request first goes through the custom middleware function specified in the `@Middlewares` decorator. Inside the middleware function, you can perform any necessary operations or modifications on the request or response objects. After completing the middleware logic, you must call the `next()` function to pass the request to the next middleware or the endpoint itself. Finally, the request reaches the exampleGetEndpoint method, where you can handle the request and provide the appropriate response. If multiple middlewares are specified, they are executed in the order they are passed to `@Middlewares(...)`. ## TypeScript Requirements Using custom middleware requires decorators to be enabled in TypeScript: ```jsonc { "compilerOptions": { // ... "experimentalDecorators": true, // ... } } ``` `emitDecoratorMetadata` is not required by `tsoa-next` for `@Middlewares(...)`. Only enable it when your own middleware, DI container, or validation stack depends on design-time metadata. --- --- url: 'https://tsoa-next.dev/custom-validation.md' --- # Custom Validation with class-validator Sometimes the built-in tsoa-next validator is not enough for an application-specific workflow. In that case you can attach your own validation middleware, for example with `class-validator`. Relevant API reference: [`@Middlewares`](../reference/tsoa-next/functions/Middlewares.md), [`ValidateError`](../reference/tsoa-next/classes/ValidateError.md), [`@Body`](../reference/tsoa-next/functions/Body.md), [`@Post`](../reference/tsoa-next/functions/Post.md), and [`@SuccessResponse`](../reference/tsoa-next/functions/SuccessResponse.md). This chapter shows a middleware-based `class-validator` approach for Express. The `@Middlewares` decorator accepts middleware functions as positional arguments: ```ts @Middlewares(validateBody(RequestClass)) ``` ## Install class-validator package Install `class-validator` and `class-transformer` as described in their repos: ## Write custom middleware The example validation middleware using `class-validator`: ```ts import { ClassConstructor, plainToInstance } from 'class-transformer'; import { validateSync } from 'class-validator'; import { NextFunction, Request, Response } from 'express'; import { ValidateError } from 'tsoa-next'; export function validateBody(targetClass: ClassConstructor) { return async (req: Request, _res: Response, next: NextFunction) => { const instance = plainToInstance(targetClass, req.body); const errors = validateSync(instance, { forbidUnknownValues: true, validationError: { target: false } }); const fieldsErrors: { [name: string]: { message: string; value: string } } = {}; if (errors.length > 0) { errors.forEach(error => { if (error.constraints) { fieldsErrors[error.property] = { message: Object.values(error.constraints).join(', '), value: error.value }; } if (error.children) { error.children.forEach(errorNested => { if (errorNested.constraints) { fieldsErrors[errorNested.property] = { message: Object.values(errorNested.constraints!).join(', '), value: errorNested.value }; } }) } }); next(new ValidateError(fieldsErrors, 'Validation failed')); return; } next(); }; } ``` ## Annotate a request class with class-validator ```ts import { Length } from 'class-validator' class RequestClass { @Length(1, 2000) public text!: string } ``` ## Usage in a controller ```ts import { Body, Controller, Middlewares, Post, Route, SuccessResponse, } from 'tsoa-next' import { validateBody } from '../middleware/ValidationMiddleware' @Route('post') export class PostController extends Controller { @SuccessResponse('200', 'Post created') @Post() @Middlewares(validateBody(RequestClass)) public async create(@Body() request: RequestClass): Promise { console.log(`validated request: ${request.text}`) } } ``` `class-validator` and `class-transformer` may need `emitDecoratorMetadata` depending on the decorators and transformation features you use. That requirement comes from those libraries, not from `@Middlewares(...)` itself. --- --- url: 'https://tsoa-next.dev/external-validators.md' --- # External validators with `@Validate` The new external schema decorator is [`@Validate(...)`](../reference/tsoa-next/functions/Validate.md). If you are looking for `@Validator`, the decorator name in `tsoa-next` is `@Validate`. Relevant API reference: [`@Validate`](../reference/tsoa-next/functions/Validate.md), [`@Body`](../reference/tsoa-next/functions/Body.md), [`@BodyProp`](../reference/tsoa-next/functions/BodyProp.md), [`@Query`](../reference/tsoa-next/functions/Query.md), [`@Queries`](../reference/tsoa-next/functions/Queries.md), [`@Path`](../reference/tsoa-next/functions/Path.md), [`@Header`](../reference/tsoa-next/functions/Header.md), [`@FormField`](../reference/tsoa-next/functions/FormField.md), [`@UploadedFile`](../reference/tsoa-next/functions/UploadedFile.md), [`@UploadedFiles`](../reference/tsoa-next/functions/UploadedFiles.md), and [`File`](../reference/tsoa-next/interfaces/File.md). \[\[toc]] ## What `@Validate` changes [`@Validate(...)`](../reference/tsoa-next/functions/Validate.md) makes an external schema the runtime source of truth for one decorated parameter. * TypeScript types still drive OpenAPI generation * The external schema replaces built-in runtime validation for the decorated parameter subtree * Routes that do not use `@Validate(...)` keep their existing validation behavior ## Supported targets In this release, `@Validate(...)` is supported on controller method parameters that use: * [`@Body()`](../reference/tsoa-next/functions/Body.md) * [`@BodyProp()`](../reference/tsoa-next/functions/BodyProp.md) * [`@Query()`](../reference/tsoa-next/functions/Query.md) * [`@Queries()`](../reference/tsoa-next/functions/Queries.md) * [`@Path()`](../reference/tsoa-next/functions/Path.md) * [`@Header()`](../reference/tsoa-next/functions/Header.md) * [`@FormField()`](../reference/tsoa-next/functions/FormField.md) * [`@UploadedFile()`](../reference/tsoa-next/functions/UploadedFile.md) * [`@UploadedFiles()`](../reference/tsoa-next/functions/UploadedFiles.md) ## Supported libraries * `zod` * `joi` * `yup` * `superstruct` * `io-ts` Install only the validator library your application uses. ::: code-group ```bash [npm] npm install zod npm install joi npm install yup npm install superstruct npm install io-ts fp-ts io-ts-types ``` ```bash [pnpm] pnpm add zod pnpm add joi pnpm add yup pnpm add superstruct pnpm add io-ts fp-ts io-ts-types ``` ```bash [yarn] yarn add zod yarn add joi yarn add yup yarn add superstruct yarn add io-ts fp-ts io-ts-types ``` ::: ## Supported decorator forms All supported validator libraries can be used with any of these forms: ```ts @Validate(schema) @Validate('zod', schema) @Validate({ kind: 'zod', schema }) ``` When you pass only the schema, `tsoa-next` will try to infer the validator kind from the schema object and its import source. If inference is ambiguous, use the explicit `kind` forms. ## Common example model The examples below use a shared TypeScript shape so you can see what stays the same while runtime validation changes. ```ts type ExternalLiteralUnion = 'active' | 'disabled' type ExternalObject = { name: string status: ExternalLiteralUnion tags: string[] } ``` ## Zod Zod works well with the inferred form: ```ts import { Body, Controller, Post, Route, Validate } from 'tsoa-next' import { z } from 'zod' type ExternalLiteralUnion = 'active' | 'disabled' type ExternalObject = { name: string status: ExternalLiteralUnion tags: string[] } const ZodBodySchema = z.object({ name: z.string().min(3, 'validation.external.zod.name.min'), status: z.enum(['active', 'disabled']), tags: z.array(z.string()).min(1, 'validation.external.zod.tags.min'), }) @Route('external-validation') export class ExternalValidationController extends Controller { @Post('zod') public zod(@Body() @Validate(ZodBodySchema) payload: ExternalObject): ExternalObject { return payload } } ``` ## Joi Joi can be used with an explicit validator kind: ```ts import * as Joi from 'joi' import { Body, Controller, Post, Route, Validate } from 'tsoa-next' type ExternalLiteralUnion = 'active' | 'disabled' type ExternalObject = { name: string status: ExternalLiteralUnion tags: string[] } const JoiBodySchema = Joi.object({ name: Joi.string().min(3).required(), status: Joi.string().valid('active', 'disabled').required(), tags: Joi.array().items(Joi.string()).min(1).required(), }) @Route('external-validation') export class ExternalValidationController extends Controller { @Post('joi') public joi(@Body() @Validate('joi', JoiBodySchema) payload: ExternalObject): ExternalObject { return payload } } ``` Joi is also useful for multipart fields and uploaded files: ```ts import * as Joi from 'joi' import { Controller, File, Post, Route, UploadedFile, Validate } from 'tsoa-next' @Route('assets') export class AssetsController extends Controller { @Post('upload') public upload(@UploadedFile('asset') @Validate('joi', Joi.any()) asset: File): File { return asset } } ``` For more upload details, see [Uploading files](./file-upload). ## Yup Yup works with the object form: ```ts import * as yup from 'yup' import { Body, Controller, Post, Route, Validate } from 'tsoa-next' type ExternalLiteralUnion = 'active' | 'disabled' type ExternalObject = { name: string status: ExternalLiteralUnion tags: string[] } const schema = yup .object({ name: yup.string().required().min(3), status: yup.mixed().oneOf(['active', 'disabled']).required(), tags: yup.array(yup.string().required()).min(1).required(), }) .required() @Route('external-validation') export class ExternalValidationController extends Controller { @Post('yup') public yupBody(@Body() @Validate({ kind: 'yup', schema }) payload: ExternalObject): ExternalObject { return payload } } ``` ## Superstruct Superstruct works with the explicit kind form: ```ts import { array, object, size, string } from 'superstruct' import { Body, Controller, Post, Route, Validate } from 'tsoa-next' type ExternalLiteralUnion = 'active' | 'disabled' type ExternalObject = { name: string status: ExternalLiteralUnion tags: string[] } const SuperstructBodySchema = object({ name: size(string(), 3, 50), status: string(), tags: size(array(string()), 1, 10), }) @Route('external-validation') export class ExternalValidationController extends Controller { @Post('superstruct') public superstruct(@Body() @Validate('superstruct', SuperstructBodySchema) payload: ExternalObject): ExternalObject { return payload } } ``` ## io-ts `io-ts` works well when you want a codec to be authoritative at runtime while preserving a first-class TypeScript alias through `TypeOf`. ```ts import * as t from 'io-ts' import { withMessage } from 'io-ts-types' import { Body, Controller, Post, Route, Validate } from 'tsoa-next' interface PositiveFloatBrand { readonly PositiveFloat: unique symbol } const PositiveFloat = withMessage( t.brand( t.number, (n): n is t.Branded => Number.isFinite(n) && n > 0, 'PositiveFloat', ), () => 'validation.wager.amount.mustBePositiveFloat', ) const WagerCodec = t.type({ amount: PositiveFloat, outcome: t.Int, }) type Wager = t.TypeOf @Route('wagers') export class WagersController extends Controller { @Post() public createWager(@Body() @Validate({ kind: 'io-ts', schema: WagerCodec }) wager: Wager): Wager { return wager } } ``` ## Validation hooks Generated `RegisterRoutes(...)` functions accept an optional validation context. This is useful when you want external validator messages translated or reformatted before they are returned to clients. ```ts RegisterRoutes(app, { validation: { translate: (key, params) => translateMessage(key, params), errorFormatter: failure => failure, }, }) ``` ## Practical guidance * Keep your TypeScript types and external schemas aligned. OpenAPI follows the TypeScript type, but runtime validation follows the external schema. * Make sure the application imports controller modules before `RegisterRoutes(...)` runs, so decorator metadata is available at runtime. * If you use custom route templates, keep the runtime validation metadata plumbing intact so controller class, method name, and parameter index are still available during validation. * Use the inferred form when it is obvious, and switch to the explicit forms when you want documentation clarity or the schema kind is not easy to infer. --- --- url: 'https://tsoa-next.dev/di.md' --- # Dependency injection or IOC By default all the controllers are created by the auto-generated routes template using an empty default constructor. If you want to use dependency injection and let the DI-framework handle the creation of your controllers, we need set up an IoC Module tsoa can call. Relevant API reference: [`Config`](../reference/tsoa-next/interfaces/Config.md), [`RoutesConfig`](../reference/tsoa-next/interfaces/RoutesConfig.md), [`IocContainer`](../reference/tsoa-next/interfaces/IocContainer.md), [`IocContainerFactory`](../reference/tsoa-next/type-aliases/IocContainerFactory.md), and [`Controller`](../reference/tsoa-next/classes/Controller.md). To tell `tsoa-next` to use your DI-container you have to reference your module exporting the DI-container in the [`Config`](../reference/tsoa-next/interfaces/Config.md) file (for example `tsoa.json`): ```js { "entryFile": "...", "spec": { ... }, "routes": { "routesDir": "...", "middleware": "...", "iocModule": "src/ioc", ... } } ``` ## IoC Module Now you can create a module that exports either a container or a function as `iocContainer`. Containers must conform to the following interface. ```ts interface IocContainer { get(controller: { prototype: T }): T } ``` Functions must conform to the following signature, where `request` is your web framework's request object. ```ts type IocContainerFactory = (request: unknown) => IocContainer ``` ### Example Container instance: ```ts // src/ioc.ts import { Container } from 'di-package' // Assign a container to `iocContainer`. const iocContainer = new Container() // export according to convention export { iocContainer } ``` Factory function: ```ts // src/ioc.ts import { IocContainer, IocContainerFactory } from 'tsoa-next' import { Container } from 'di-package' // Or assign a factory function to `iocContainer`. const iocContainer: IocContainerFactory = function (request: Request): IocContainer { const container = new Container() container.bind(request) return container } // export according to convention export { iocContainer } ``` ::: tip If you want to use a DI framework other than the examples below, adding it isn't hard. If you set an iocModule, tsoa will call this module (to get a `FooController`) with: ```ts import { iocContainer } from './the/path/to/the/module/from/tsoa.json' iocContainer.get(FooController) ``` If you wrap your DI's API or even a ControllerFactory to accept this call and respond with a FooController, it'll work. ::: ## InversifyJS Configure controller and service bindings explicitly with the native [InversifyJS container API](https://inversify.io/docs/fundamentals/binding/). Controllers must use transient scope because `Controller` stores response-specific status and headers. Services may use a longer scope when they do not hold request state. ```ts // src/ioc.ts import { Container } from 'inversify' import { UsersController } from './users/usersController' import { FooService } from './users/fooService' const iocContainer = new Container() // Keep controllers transient so response state cannot leak between requests. iocContainer.bind(UsersController).toSelf().inTransientScope() iocContainer.bind(FooService).toSelf().inSingletonScope() export { iocContainer } ``` Use Inversify's `@injectable()` and `@inject()` decorators for constructor injection: ```ts // src/users/usersController.ts import { inject, injectable } from 'inversify' import { Controller, Route } from 'tsoa-next' import { FooService } from './fooService' @injectable() @Route('foo') export class UsersController extends Controller { public constructor(@inject(FooService) private readonly fooService: FooService) { super() } } ``` ```ts // src/users/fooService.ts import { injectable } from 'inversify' @injectable() export class FooService { // ... } ``` The repository's legacy binding-decorator fixture remains pinned to `inversify@6.2.2` with `inversify-binding-decorators@4.0.0`; that decorator package does not declare compatibility with Inversify 7 or 8. New integrations should prefer the native bindings above. If you migrate an existing application to Inversify 8, follow Inversify's [v6-to-v8 migration guide](https://inversify.io/docs/guides/migrating-from-v6/) rather than forcing the legacy decorator package through an override. ## TSyringe Here's an example using [TSyringe](https://github.com/microsoft/tsyringe). ```ts // src/lib/tsyringeTsoaIocContainer.ts // Target this file in your tsoa.json's "iocModule" property import { IocContainer } from 'tsoa-next' import { container } from 'tsyringe' export const iocContainer: IocContainer = { get: (controller: { prototype: T }): T => { return container.resolve(controller as never) }, } ``` ```ts // src/services/FooService.ts import { singleton } from 'tsyringe' // ... @singleton() export class FooService { // ... } ``` ```ts // src/controllers/FooController.ts import { Controller, Route } from 'tsoa-next' import { injectable } from 'tsyringe' import { FooService } from '../services/FooService' // ... @injectable() @Route('foo') export class FooController extends Controller { constructor(private fooService: FooService) { super() } // ... } ``` ## typescript-ioc Here is some example code to setup the controller with [typescript-ioc](https://github.com/thiagobustamante/typescript-ioc). `./controllers/fooController.ts` ```ts import { Route } from 'tsoa-next'; import { Inject, Singleton } from "typescript-ioc"; @Route('foo') export class FooController { @Inject private fooService: FooService ... } @Singleton export class FooService { } ``` The controllers need to be included in the application in order to be linked. `index.ts` ```ts import "./controllers/fooController.ts" ... ``` --- --- url: 'https://tsoa-next.dev/authentication.md' --- # Authentication Authentication is done using a middleware handler along with `@Security('name', ['scopes'])` decorator in your controller. The scheme name is user-defined: `jwt`, `api_key`, `session`, or `tsoa_auth` are all valid as long as you use the same name in `spec.securityDefinitions`, `@Security(...)`, and your authentication module. Relevant API reference: [`@Security`](../reference/tsoa-next/functions/Security.md), [`@NoSecurity`](../reference/tsoa-next/functions/NoSecurity.md), [`@Request`](../reference/tsoa-next/functions/Request.md), [`@Res`](../reference/tsoa-next/functions/Res.md), [`@Response`](../reference/tsoa-next/functions/Response.md), and [`TsoaResponse`](../reference/tsoa-next/type-aliases/TsoaResponse.md). First, define the security definitions for OpenAPI, and also configure where the authentication middleware handler is. In this case, it is in the `authentication.ts` file. ```js { "spec": { "securityDefinitions": { "api_key": { "type": "apiKey", "name": "access_token", "in": "query" }, "jwt": { "type": "oauth2", "authorizationUrl": "http://swagger.io/api/oauth/dialog", "flow": "implicit", "scopes": { "write:pets": "modify things", "read:pets": "read things" } } }, ... }, "routes": { "authenticationModule": "./authentication.ts", ... } } ``` In the middleware, export the function based on which library (Express, Koa, Hapi) you are using. You only create one function per runtime and handle the security types inside it. The `securityName` and `scopes` come from the annotation you put above your controller function. \* The `securityDefinitions` key and the `securityName` you check in your authentication module must match exactly. `tsoa-next` does not reserve or special-case any particular name. `./authentication.ts` ```ts import * as express from "express"; import * as jwt from "jsonwebtoken"; export function expressAuthentication( request: express.Request, securityName: string, scopes?: string[] ): Promise { if (securityName === "api_key") { let token; if (request.query && request.query.access_token) { token = request.query.access_token; } if (token === "abc123456") { return Promise.resolve({ id: 1, name: "Ironman", }); } else { return Promise.reject({}); } } if (securityName === "jwt") { const token = request.body.token || request.query.token || request.headers["x-access-token"]; return new Promise((resolve, reject) => { if (!token) { reject(new Error("No token provided")); } jwt.verify(token, "[secret]", function (err: any, decoded: any) { if (err) { reject(err); } else { // Check if JWT contains all required scopes for (const scope of scopes ?? []) { if (!decoded.scopes.includes(scope)) { reject(new Error("JWT does not contain required scope.")); } } resolve(decoded); } }); }); } } import * as hapi from "@hapi/hapi"; export function hapiAuthentication( request: hapi.Request, securityName: string, scopes?: string[] ): Promise { // See above } import { Request } from "koa"; export function koaAuthentication( request: Request, securityName: string, scopes?: string[] ): Promise { // See above } ``` `./controllers/securityController.ts` ```ts import { Get, Request, Res, Response, Route, Security, TsoaResponse } from "tsoa-next"; @Route("secure") export class SecureController { @Response<{ message: string }>("default", "Unexpected error") @Security("api_key") @Get("UserInfo") public async userInfo(@Request() request: { user: { id: number; name: string } }): Promise<{ id: number; name: string }> { return Promise.resolve(request.user); } @Response<{ message: string }>("default", "Unexpected error") @Security("jwt", ["admin"]) @Get("EditUser") public async editUser( @Request() request: { user?: { id: number; name: string } }, @Res() notFoundResponse: TsoaResponse<404, { message: string }> ): Promise<{ id: number; name: string }> { if (!request.user) { return notFoundResponse(404, { message: "Not found" }); } return request.user; } } ``` ## Default API-wide security If most of your API shares the same requirement, you can apply it once at the spec level with `spec.rootSecurity` and then override it on individual controllers or actions with `@Security(...)` or `@NoSecurity()`. ```js { "spec": { "rootSecurity": [{ "api_key": [] }] } } ``` --- --- url: 'https://tsoa-next.dev/decorators.md' --- # Decorators Please note that this section only covers decorators that are not described separately, such as [`@Response`](./error-handling) or the core parameter decorators introduced in [Getting started](./getting-started). For a full overview, please check out the [API Reference](../reference/). Relevant API reference: [`@Security`](../reference/tsoa-next/functions/Security.md), [`@NoSecurity`](../reference/tsoa-next/functions/NoSecurity.md), [`@Tags`](../reference/tsoa-next/functions/Tags.md), [`@OperationId`](../reference/tsoa-next/functions/OperationId.md), [`@Deprecated`](../reference/tsoa-next/functions/Deprecated.md), [`@Validate`](../reference/tsoa-next/functions/Validate.md), [`@SpecPath`](../reference/tsoa-next/functions/SpecPath.md), [`@Hidden`](../reference/tsoa-next/functions/Hidden.md), [`@Request`](../reference/tsoa-next/functions/Request.md), [`@RequestProp`](../reference/tsoa-next/functions/RequestProp.md), [`@Inject`](../reference/tsoa-next/functions/Inject.md), [`@Produces`](../reference/tsoa-next/functions/Produces.md), and [`@Consumes`](../reference/tsoa-next/functions/Consumes.md). ## Security The [`@Security`](../reference/tsoa-next/functions/Security.md) 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): ```ts @Security('jwt', ['write:pets', 'read:pets']) @Security('api_key') @Get('OauthOrAPIkey') public async GetWithOrSecurity(@Request() request: express.Request): Promise { } ``` or having to pass all of them (AND): ```ts @Security({ jwt: ['write:pets', 'read:pets'], api_key: [], }) @Get('OauthAndAPIkey') public async GetWithAndSecurity(@Request() request: express.Request): Promise { } ``` ## NoSecurity Use [`@NoSecurity()`](../reference/tsoa-next/functions/NoSecurity.md) when a controller or action should clear inherited or API-wide security requirements. ```ts 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 { return 'private' } @Get('public') @NoSecurity() public async publicEndpoint(): Promise { return 'public' } } ``` ## Tags Tags are defined with the [`@Tags('tag1', 'tag2', ...)`](../reference/tsoa-next/functions/Tags.md) decorator in the controllers and/or in the methods like in the following examples. ```ts 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 { 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. ```js { "spec": { "tags": [ { "name": "User", "description": "Operations about users", "externalDocs": { "description": "Find out more about users", "url": "http://swagger.io" } } ], ... }, "routes": { ... } } ``` ## OperationId Set [`operationId`](../reference/tsoa-next/functions/OperationId.md) 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. ```ts @Get() @OperationId('findDomain') public async find(): Promise { } ``` ## Deprecated OpenAPI allows you to deprecate [operations](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.2.md#user-content-operationdeprecated), [parameters](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.2.md#user-content-parameterdeprecated), and [schemas](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.2.md#user-content-schemadeprecated). 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`](../reference/tsoa-next/functions/Deprecated.md) 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 ```ts @Get() @Deprecated() public async find(): Promise { } ``` ### Parameters (OpenAPI 3+ only) ```ts @Get("v2") public async findV2( @Query() text: string, @Deprecated() @Query() dontUse?: string ): Promise { } ``` ```ts interface QueryParams { text: string; sort?: string; page?: number; } @Get("v2") public async findV2( @Queries() queryParams: QueryParams ): Promise { } ``` ### Schemas (OpenAPI 3+ only) ```ts 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(...)`](../reference/tsoa-next/functions/Validate.md). 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 ```ts 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](./external-validators). ## SpecPath Use [`@SpecPath(...)`](../reference/tsoa-next/functions/SpecPath.md) 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 `//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-routes` workflow or a routes config that embeds `runtimeSpecConfig` * 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-express` for Express * `swagger-ui-koa` for Koa * `hapi-swagger` for Hapi * `redoc` for Redoc * `rapidoc` for RapiDoc * Custom handlers can return either a `string` or a `Readable` * Use `@SpecPath(path, options?)` to configure [`SpecPathOptions`](../reference/tsoa-next/interfaces/SpecPathOptions.md) such as `target`, `cache`, and an optional `gate` * `gate` can be a boolean or a function that receives the [`SpecRequestContext`](../reference/tsoa-next/interfaces/SpecRequestContext.md) and 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 custom [`SpecCacheHandler`](../reference/tsoa-next/interfaces/SpecCacheHandler.md) * `@SpecPath(...)` routes are auxiliary and are not added to the generated OpenAPI document ```ts 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/spec` serves the OpenAPI document as JSON * `GET /users/openapi.yaml` serves the same document as YAML * `GET /users/docs` serves Swagger UI if the runtime-specific peer dependency is installed You can also provide a custom handler and external cache implementation: ```ts import { Readable } from 'node:stream' import { Controller, Get, Route, SpecCacheHandler, SpecPath, SpecRequestContext } from 'tsoa-next' const cacheStore = new Map() 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: ```ts @SpecPath('docs', { gate: context => { const headers = (context.request as { headers?: Record } | 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`](../reference/tsoa-next/functions/Hidden.md) on methods to exclude an endpoint from the generated OpenAPI Specification document. ```ts @Get() @Hidden() public async find(): Promise { } ``` Use [`@Hidden`](../reference/tsoa-next/functions/Hidden.md) on controllers to exclude all of their endpoints from the generated OpenAPI Specification document. ```ts import { Controller, Get, Hidden, Post, Route } from 'tsoa-next' @Route('hidden') @Hidden() export class HiddenController extends Controller { @Get() public async find(): Promise {} @Post() public async create(): Promise {} } ``` 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. ```ts @Get() public async find( @Query() normalParam: string, @Query() @Hidden() defaultSecret = true, @Query() @Hidden() optionalSecret?: string ): Promise { } ``` ## Request To access the request object of express in a controller method use the [`@Request`](../reference/tsoa-next/functions/Request.md) decorator: ```typescript // 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`](../reference/tsoa-next/functions/Request.md) decorator: ```typescript // 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(...)`](../reference/tsoa-next/functions/RequestProp.md) when the value already lives on the underlying runtime request object. Use [`@Inject()`](../reference/tsoa-next/functions/Inject.md) when a parameter is supplied entirely by your own route template or wrapper code and should be omitted from spec generation. ::: ## RequestProp [`@RequestProp(...)`](../reference/tsoa-next/functions/RequestProp.md) binds a single property from the underlying runtime request object. ```ts 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`](../reference/tsoa-next/functions/Produces.md) 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: ```typescript @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 { 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`](../reference/tsoa-next/functions/Produces.md) 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(...)`](../reference/tsoa-next/functions/Consumes.md) when an action accepts a non-default request body media type. ```ts 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, } } } ``` --- --- url: 'https://tsoa-next.dev/faq.md' --- # FAQ ## Can I use OpenAPI 3 or 3.1 instead of OpenAPI 2 (formerly Swagger)? Yes. Set `spec.specVersion` to `3` or `3.1` in your `tsoa.json` file. See more config options in the [`Config`](../reference/tsoa-next/interfaces/Config.md) API reference. ## How do I use tsoa with koa, hapi, or other frameworks? Set the middleware property in your tsoa config. Out of the box, express, hapi and koa are supported. You can also provide a custom template, for more information, please check out [the guide](./templates.md) ## How to ensure no additional properties come in at runtime By default, OpenAPI allows for models to have [`additionalProperties`](https://swagger.io/docs/specification/data-models/dictionaries/). If you would like to ensure at runtime that the data has only the properties defined in your models, set the `noImplicitAdditionalProperties` option in [`Config`](../reference/tsoa-next/interfaces/Config.md) to either `"silently-remove-extras"` or `"throw-on-extras"`. Caveats: * The following types will always allow additional properties due to the nature of the way they work: * The `any` type * An indexed type (which explicitly allows additional properties) like `export interface IStringToStringDictionary { [key: string] : string }` * If you are using tsoa for an existing service that has consumers... * you will need to inform your consumers before setting `noImplicitAdditionalProperties` to `"throw-on-extras"` since it would be a breaking change (due to the fact that request bodies that previously worked would now get an error). * Regardless, `"noImplicitAdditionalProperties" : "silently-remove-extras"` is a great choice for both legacy AND new APIs (since this mirrors the behavior of C# serializers and other popular JSON serializers). ## Dealing with duplicate model names If you have multiple models with the same name, you may get errors indicating that there are multiple matching models. If you'd like to designate a class/interface as the 'canonical' version of a model, add a jsdoc element marking it as such: ```ts /** * @tsoaModel */ export interface MyModel { ... } ``` ## How can I get the most from my OAS? Now that you have a OpenAPI Specification (OAS) (swagger.json), you can use all kinds of amazing tools that generate documentation, client SDKs, and more [here](http://openapi.tools). ## How to override limit for validating large arrays (with more than 20 elements) By default [Express](https://github.com/expressjs/express) uses [qs](https://github.com/ljharb/qs) as parser internally, and its have default limitation for validating 20 elements in array to override this you must add following configuration to your express config: ```ts const app = express() app.set('query parser', function (str) { return qs.parse(str, { arrayLimit: Infinity }) }) app.use(bodyParser.json()) app.use(Router()) ``` Please note that you must place it on top of other middleware. --- --- url: 'https://tsoa-next.dev/file-upload.md' --- # Uploading files Relevant API reference: [`File`](../reference/tsoa-next/interfaces/File.md), [`@FormField`](../reference/tsoa-next/functions/FormField.md), [`@UploadedFile`](../reference/tsoa-next/functions/UploadedFile.md), and [`@UploadedFiles`](../reference/tsoa-next/functions/UploadedFiles.md). ## Install the runtime upload middleware For Express: ::: code-group ```bash [npm] npm install multer npm install -D @types/multer ``` ```bash [pnpm] pnpm add multer pnpm add -D @types/multer ``` ```bash [yarn] yarn add multer yarn add -D @types/multer ``` ::: For Koa: ::: code-group ```bash [npm] npm install @koa/multer ``` ```bash [pnpm] pnpm add @koa/multer ``` ```bash [yarn] yarn add @koa/multer ``` ::: ## Using the `@UploadedFile` / `@UploadedFiles` decorators The built-in upload decorators use tsoa-next's exported [`File`](../reference/tsoa-next/interfaces/File.md) interface. Use [`@FormField()`](../reference/tsoa-next/functions/FormField.md) for the non-file multipart fields that arrive alongside the upload. ```ts import { Controller, File, FormField, Post, Route, UploadedFile, UploadedFiles } from 'tsoa-next' @Route('files') export class FilesController extends Controller { @Post('single') public async uploadSingle(@FormField() title: string, @UploadedFile('asset') asset: File): Promise<{ title: string; originalName: string }> { return { title, originalName: asset.originalname, } } @Post('many') public async uploadMany(@UploadedFiles('assets') assets: File[]): Promise<{ count: number }> { return { count: assets.length, } } } ``` ## Default storage behavior Generated Express and Koa routes create a default multer instance when you use upload decorators. By default that instance keeps uploaded files in memory. If you want uploads written to disk or handled by a custom multer configuration, pass your own multer instance into `RegisterRoutes(...)`. ## Custom multer configuration Express example: ```ts import express, { json, urlencoded } from 'express' import multer from 'multer' import { RegisterRoutes } from '../build/routes' const app = express() app.use(urlencoded({ extended: true })) app.use(json()) RegisterRoutes(app, { multer: multer({ dest: 'uploads/' }), }) ``` Koa example: ```ts import Router from '@koa/router' import multer from '@koa/multer' import { RegisterRoutes } from '../build/routes' const router = new Router() RegisterRoutes(router, { multer: multer({ dest: 'uploads/' }), }) ``` There is also a legacy top-level `multerOpts` config field in `tsoa.json`, but it is deprecated. Prefer passing a concrete multer instance into `RegisterRoutes(...)`. ## Manual multipart handling If you choose to bypass `@UploadedFile(...)` and call multer yourself inside a controller using `@Request()`, you are also responsible for documenting that request shape yourself. In that case, merge the multipart request details into `spec.spec` in `tsoa.json` so the generated OpenAPI document still describes the endpoint accurately. --- --- url: 'https://tsoa-next.dev/path-mapping.md' --- ### Path mapping TypeScript's [`paths` compiler option](https://www.typescriptlang.org/tsconfig/paths.html) remaps import specifiers for type resolution. It does not rewrite emitted imports, so your runtime or bundler must understand the same aliases. `baseUrl` is not required for `paths` and is deprecated in TypeScript 6. Prefer paths relative to the `tsconfig.json` file: ```js { "compilerOptions": { "paths": { "@app/*": ["./src/*"] } } } ``` If you have a project that uses this functionality, you can configure the internal generators either by: * letting `tsoa-next` read compiler options from a `tsconfig.json` * overriding specific values with `compilerOptions` in your `tsoa` config `tsconfig.json` is an input source, not the final authority. The precedence is: 1. TypeScript internal defaults 2. resolved `tsconfig.json` 3. explicit `compilerOptions` in `tsoa` config If `tsconfig` is omitted, `tsoa-next` looks for `tsconfig.json` starting from the loaded `tsoa` config directory. If `tsconfig` is provided, it is resolved relative to that config file. ```js { "tsconfig": "./tsconfig.json", "spec": { ... }, "routes": { ... }, "compilerOptions": { "paths": { "exampleLib": ["./path/to/example/lib"] } } } ``` You can also continue to provide compiler options directly when you do not want to rely on `tsconfig.json`. ```js { "spec": { ... }, "routes": { ... }, "compilerOptions": { "paths": { "exampleLib": ["./path/to/example/lib"] } } } ``` --- --- url: 'https://tsoa-next.dev/templates.md' --- # Overriding route template If you want functionality that tsoa doesn't provide, then one powerful (but potentially costly approach) is to provide tsoa with a custom handlebars template to use when generating the routes.ts file. ::: danger Using a custom template means that you will have a more difficult time migrating to new versions of tsoa since your template interacts with the tsoa internals. So, to get the newest and best features of tsoa, please use one of provided templates by selecting your chosen `"middleware"` (i.e. "koa", "express", or "hapi") and by omitting `"middlewareTemplate"`. ::: *Okay, but why would you want to override the route template?* * Are you using a server framework that we don't yet support? If so, then [please open an issue first](https://github.com/tsoa-next/tsoa-next/issues). It's likely that we will try to accept your custom template as one of the new standard options. If we can't support the new framework, then we'll recommend a custom route template. * Do you have a very specific requirement? Have you already opened an issue and have the tsoa maintainers opted not to support this feature? Then a custom template might solve your needs best. Route templates are generated from predefined handlebar templates. You can override and define your own template to use by defining it in your tsoa.json configuration. Route paths are generated based on the middleware type you have defined. ```js { "entryFile": "...", "spec": { ... }, "routes": { "routesDir": "...", "middleware": "express", "middlewareTemplate": "custom-template.ts", ... } } ``` --- --- url: 'https://tsoa-next.dev/routes.md' --- # Consuming generated routes Relevant API reference: [`Config`](../reference/tsoa-next/interfaces/Config.md) and [`@Route`](../reference/tsoa-next/functions/Route.md). You have two options for how to tell tsoa where it can find the controllers that it will use to create the auto-generated `routes.ts` file. ## Using automatic controllers discovery You can tell `tsoa-next` to use automatic controller discovery by providing one or more [minimatch globs](http://www.globtester.com/) in the top-level `controllerPathGlobs` field of your [`Config`](../reference/tsoa-next/interfaces/Config.md) file (for example `tsoa.json`). Pros: * New developers can add a controller without having to know how tsoa "crawls" for the controllers. As long as their controller is caught by the glob that you provide, the controller will be added to the OpenAPI documentation and to the auto-generated `routes.ts` file. Cons: * It can be slightly slower than the alternative explicit-import approach because tsoa needs to expand and load the configured globs. As you can see from the controllers globs patterns below, you can provide multiple globs of various patterns: ```js { "entryFile": "...", "controllerPathGlobs": [ "./dir-with-controllers/*", "./recursive-dir/**/*", "./custom-filerecursive-dir/**/*.controller.ts" ], "routes": { "routesDir": "...", "middleware": "..." } } ``` ## Manually telling tsoa which controllers to use in the app entry file If you omit `controllerPathGlobs`, tsoa can crawl the application entry file and follow controller imports that have the `@Route` decorator. Pros: * Route generation will usually be faster because tsoa follows your explicit imports instead of expanding globs. Cons: * New developers on your team might add a controller and not understand why the new controller was not exposed to the router or the OpenAPI generation. If that is a problem for you, prefer `controllerPathGlobs`. ```typescript import methodOverride from 'method-override' import express from 'express' import bodyParser from 'body-parser' import { RegisterRoutes } from './routes' // ######################################################################## // controllers need to be referenced in order to get crawled by the generator import './users/usersController' // ######################################################################## const app = express() app.use(bodyParser.urlencoded({ extended: true })) app.use(bodyParser.json()) app.use(methodOverride()) RegisterRoutes(app) app.listen(3000) ``` --- --- url: 'https://tsoa-next.dev/upgrading.md' --- # Upgrading from tsoa 2.5 [Jump to the breaking changes](#breaking-changes) > Historical note: the pull request links in this guide intentionally point to [`lukeautry/tsoa`](https://github.com/lukeautry/tsoa), where these changes originally landed. ## New Features ### Support for type aliases This release comes with proper support for type alias definitions. They can range from simple scenarios ```ts /** * A Word shall be a non-empty string * @minLength 1 */ type Word = string ``` to more complex scenarios like unions and intersections of aliases ```ts type IntersectionAlias = { value1: string; value2: string } & TypeAliasModel1 // or type OneOrTwo = TypeAliasModel1 | TypeAliasModel2 ``` or even generic type aliases: ```ts type GenericAlias = T | string type ForwardGenericAlias = GenericAlias | T ``` Please note that this means that tsoa does not only generate the specification (OpenAPI v3 and Swagger2\*), but will also validate the input against the types including the jsDoc annotations. \* There may be certain scenarios where we may not be able to generate Swagger 2 from your TypeScript, tsoa will log warnings to inform you about any issues we are aware of. ### Support for mapped types > TypeScript 2.1 introduced mapped types, a powerful addition to the type system. In essence, mapped types allow you to create new types from existing ones by mapping over property types. Each property of the existing type is transformed according to a rule that you specify. The transformed properties then make up the new type. > \- Marius Schulz, https://mariusschulz.com/blog/mapped-types-in-typescript tsoa now works with the ts type checker to resolve mapped types. We will actively try to support all cases, however the test suite for now only covers the utility mapped types typescript ships with, like: ```ts /** * Make all properties in T optional */ type Partial = { [P in keyof T]?: T[P] } /** * Make all properties in T required */ type Required = { [P in keyof T]-?: T[P] } /** * Make all properties in T readonly */ type Readonly = { readonly [P in keyof T]: T[P] } /** * From T, pick a set of properties whose keys are in the union K */ type Pick = { [P in K]: T[P] } ``` ### Support for conditional types As of version 2.8, TypeScript supports conditional types. The syntax is very close to the ternary operator and enables expression of 2 (or more) different types based on a condition. Please refer to the [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/advanced-types.html#conditional-types) for details. ```ts type Diff = T extends U ? never : T // Remove types from T that are assignable to U ``` tsoa now works with the ts type checker to resolve conditional types. We will actively try to support most cases, however the test suite for now only covers the utility types typescript ships with, like: ```ts /** * Exclude from T those types that are assignable to U */ type Exclude = T extends U ? never : T /** * Extract from T those types that are assignable to U */ type Extract = T extends U ? T : never /** * Exclude null and undefined from T */ type NonNullable = T extends null | undefined ? never : T ``` ### Support for combinations and utility types The combination of mapped and conditional types allow for powerful utility types like the `Omit` type. ``` /** * Construct a type with the properties of T except for those in type K. */ type Omit = Pick>; ``` ### Support for `Record<>` [#662](https://github.com/lukeautry/tsoa/pull/662) ([Eywek](https://github.com/Eywek)) ### Enums: See [#594](https://github.com/lukeautry/tsoa/pull/594) for the Spec and [#599](https://github.com/lukeautry/tsoa/pull/599) and [#593](https://github.com/lukeautry/tsoa/pull/593) ### Null Keyword: See [#601](https://github.com/lukeautry/tsoa/pull/601) ### Ability to use a colon delimiter instead of bracelets in path [#602](https://github.com/lukeautry/tsoa/pull/602)([itamarco](https://github.com/itamarco)) ### added @example support for parameters / properties [#616](https://github.com/lukeautry/tsoa/pull/616) ([jfrconley](https://github.com/jfrconley)) ### feat: ignore class methods [#643](https://github.com/lukeautry/tsoa/pull/643) ([Eywek](https://github.com/Eywek)) ### feat: handle enum members [#656](https://github.com/lukeautry/tsoa/pull/656) ([Eywek](https://github.com/Eywek)) ### Handle indexed types [#636](https://github.com/lukeautry/tsoa/pull/636) ([Eywek](https://github.com/Eywek)) ### handle `typeof` [#635](https://github.com/lukeautry/tsoa/pull/635) ([Eywek](https://github.com/Eywek)) ### `@format` support for type aliases [#620](https://github.com/lukeautry/tsoa/pull/620) ([jfrconley](https://github.com/jfrconley)) ## Bug Fixes * correctly propagate field name in validateModel [@fantapop](https://github.com/fantapop) * Aliased void Api Response types document 200 response instead of 204 [#629](https://github.com/lukeautry/tsoa/pull/629) ([WoH](https://github.com/WoH)) * ValidateError should extend Error [#661](https://github.com/lukeautry/tsoa/pull/661) ([aldenquimby](https://github.com/aldenquimby)) * Upgrade koa-router to @koa/router, fix type errors [#646](https://github.com/lukeautry/tsoa/pull/646) ([michaelbeaumont](https://github.com/michaelbeaumont)) * Remove object type [#642](https://github.com/lukeautry/tsoa/pull/642) ([dimitor115](https://github.com/dimitor115)) * Fix adding static properties to model definition [#639](https://github.com/lukeautry/tsoa/pull/639) ([dimitor115](https://github.com/dimitor115)) ## Breaking changes ### Null vs. undefined Unless you declare a type to accept `null`, we will no longer mark your optional properties as `nullable: true` or `x-nullable: true`. This applies to validation aswell, so while sending `null` instead of sending `undefined` / no properties on an object was fine, now it's not any more. Sending `undefined` instead of, i.e. `string | null` is also rejected by the validation. ### Naming In order to support type aliases and avoid name clashes, the names for the generated component schemas / definitions may have changed (generic interfaces are affected mostly). If you rely on the component names generated from tsoa, this is a breaking change. Because tsoa supported some type aliases in the past and now generated definitions differently, this may break your code. If you relied on tsoa not supporting type aliases properly to avoid issues, this may break your code. Proceed with caution and report issues. ### Improve nested object validation See [#574](https://github.com/lukeautry/tsoa/pull/574) and [#575](https://github.com/lukeautry/tsoa/pull/575). These SHOULD not be breaking changes, but since it affects validation, better safe than sorry. ### Change default behavior when no host is defined: Explicitly set your host in case you want to have absolute urls. This is a breaking change for those who were using OpenAPI 3, but it actually brings tsoa into parity with how we were handling the `host` property in Swagger 2. Previously OpenAPI 3 users had to result to passing `null` which we all felt was strange. Now omitting `host` will cause tsoa to assume the url should be relative. ### Remove .. in fieldErrors When detecting illegal additional properties (if you are using tsoa setting `additionalProperties: 'throw-on-extras'`), the key on the error would contain an additional dot. ```js { "TestModel..additionalProp: : { ... } } ``` This is now fixed and the key is `TestModel.additionalProp`. ### Use Spec instead of Swagger (`tsoa swagger` is still available for now, but will be removed eventually) [#664](https://github.com/lukeautry/tsoa/pull/664) ([WoH](https://github.com/WoH)) ```diff Calling the tsoa command - tsoa swagger + tsoa spec - tsoa swagger-and-routes + tsoa spec-and-routes Manually calling spec generation - await generateSwaggerSpec(swaggerConfig, routesConfig, compilerOptions, config.ignore); + await generateSpec(openapiConfig, compilerOptions, config.ignore); ``` tsoa.json: ```js { "swagger": {} } ``` becomes ```js { "spec": {} } ``` * Move shared config to top level [#628](https://github.com/lukeautry/tsoa/pull/628) ([WoH](https://github.com/WoH)) Instead of duplicating config and handling a lot of edge cases, the new config is a lot simpler. Config settings, that impact both routes and spec are now located at the top level of the config object. ```json { "entryFile": "./tests/fixtures/express/server.ts", "noImplicitAdditionalProperties": "silently-remove-extras", "routes": {}, "spec": {} } ``` This means if your settings are different (for example the entry file), you'll have to call the `generateRoutes()` and `generateSpec()` yourself. Note that these methods now have a simpler config aswell: ```diff - await generateSwaggerSpec(swaggerConfig, routesConfig, compilerOptions, config.ignore); + await generateSpec(openapiConfig, compilerOptions, config.ignore); ``` ```diff - await generateRoutes(routesConfig, swaggerConfig, compilerOptions, config.ignore); + await generateRoutes(routesConfig, compilerOptions, config.ignore); ``` EntryFile and noImplicitAdditionalProperties can now be set on the swagger/routesConfig. Also, boolean settings for noImplicitAdditionalProperties have been removed: #503 Valid settings are now: `'throw-on-extras' | 'silently-remove-extras' | 'ignore'`, everything else falls back to `'ignore'`. **For reference, see the TS interface of the entire config [here](../reference/tsoa-next/interfaces/Config.md)** ### TypeScript Unions are now implemented as `anyOf` in OpenAPI [#671](https://github.com/tsoa-next/tsoa-next/issues/671) --- --- url: 'https://tsoa-next.dev/ar.md' --- --- --- url: 'https://tsoa-next.dev/es.md' --- --- --- url: 'https://tsoa-next.dev/hi.md' --- --- --- url: 'https://tsoa-next.dev/reference.md' --- # tsoa-next ## Packages * [@tsoa-next/cli](@tsoa-next/cli/index.md) * [@tsoa-next/runtime](@tsoa-next/runtime/index.md) * [tsoa-next](tsoa-next/index.md) --- --- url: 'https://tsoa-next.dev/zh-hans.md' ---