Appearance
Upgrading to tsoa 7
This guide compares tsoa 7 with tsoa 6.6.0, the last 6.x release on npm.
What we will talk about:
Checklist
- Run Node.js 22.12 or later, 24 or 26.
- Install the packages for your framework.
- Replace imports of
ExpressTemplateService,HapiTemplateServiceorKoaTemplateServicewith their subpaths. - Remove
multerOptsfromtsoa.jsonand pass a multer instance. - Rename
@Queryto@QueryParamand@Queriesto@QueryParamsin your controllers. - If your
tsoa.jsonhas nospec.specVersion, set it to2to keep Swagger 2.0 output, or move to the new default, OpenAPI 3.1. - Regenerate your routes and your spec, then compare the spec with the old one. Most changes come from the tsconfig.json tsoa now reads.
- Read Validation and Responses if clients depend on exact error messages or response bodies.
Requirements
- TypeScript 5.7 or later, or TypeScript 6.
- Express 4.22.3 or later, or 5.1 or later; Koa with
@types/koa2 or 3; Hapi 21; or Fastify 5. - Node.js 22.12 or later, 24 or 26. tsoa 6 allowed Node.js 18 and later. npm warns on other versions; Yarn 1 refuses to install.
Packages and imports
Install your framework packages yourself
@tsoa/runtime used to depend on Express, Hapi, @hapi/boom and @types/koa, so every project installed all three frameworks. They are now optional peer dependencies. Install the ones for your framework:
| Framework | Packages |
|---|---|
| Express | express, @types/express (4 or 5) |
| Koa | koa, @types/koa (2 or 3) |
| Hapi | @hapi/hapi, @hapi/boom |
Hapi projects must list @hapi/boom in their own package.json, because the generated routes import it.
@tsoa/runtime and @tsoa/cli no longer depend on @types/multer. If your code uses Express.Multer.File, install it yourself:
shell
yarn add -D @types/multerWith TypeScript 6, or with types set in your tsconfig.json, also list multer there, because TypeScript only loads the global Express.Multer types from packages it includes.
See Framework packages.
Import template services from a subpath
@tsoa/runtime and tsoa no longer export ExpressTemplateService, HapiTemplateService or KoaTemplateService. Custom route templates and classes that extend a template service import them from the framework's subpath:
diff
- import { ExpressTemplateService } from '@tsoa/runtime';
+ import { ExpressTemplateService } from '@tsoa/runtime/express';The same applies to @tsoa/runtime/hapi and @tsoa/runtime/koa. The generated routes import from the new subpaths, so regenerate them.
@tsoa/runtime now has an exports map. Deep imports work only under @tsoa/runtime/dist/....
Code that uses tsoa's internals
These changes affect only code that extends tsoa or imports its internal files:
@tsoa/clino longer hasmetadataGeneration/typeResolverormetadataGeneration/transformer/*. UseMetadataGenerator.resolveTypeandMetadataGenerator.resolveTypeNode.- The
Swaggernamespace changed.Swagger.SchemaandSwagger.Parameterare removed; useSwagger.Schema2,Swagger.Schema3,Swagger.Schema31,Swagger.Parameter2orSwagger.Parameter3.Swagger.Spec3is nowSpec30 | Spec31.Swagger.Header3no longer hastypeandformat; set them on itsschema.discriminatoris a string inSchema2and an object inSchema3. Tsoa.Typehas a new member,Tsoa.TupleType. Aswitchthat checks everydataTypeneeds a'tuple'case.ValidationService.validateBuffernow takes(name, value, fieldErrors, parent)and reports a field error instead of throwing.- The generated route templates changed. If you keep a copy as
routes.middlewareTemplate, start again from the new template.
Decorators
@Query and @Queries are renamed
The parameter decorators @Query and @Queries are now @QueryParam and @QueryParams. @Query is now a method decorator for the HTTP QUERY method (RFC 10008), next to @Get and @Post.
diff
- import { Get, Query, Route } from 'tsoa';
+ import { Get, QueryParam, Route } from 'tsoa';
@Get()
- public async list(@Query() page?: number) {}
+ public async list(@QueryParam() page?: number) {}tsoa keeps no alias for the old names, so TypeScript reports each place you missed. The generated spec and routes do not change.
Configuration
multerOpts is removed
multerOpts was deprecated in 6.4.0. Remove it from tsoa.json and pass a multer instance to the generated routes:
diff
// tsoa.json
"routes": {
- "multerOpts": { "dest": "/tmp" }
}ts
import multer from "multer";
RegisterRoutes(app, { multer: multer({ dest: "/tmp" }) });tsoa 7 ignores multerOpts without a warning. Without an instance, the routes use a default one limited to 8 MB per file.
tsoa reads your tsconfig.json
tsoa 6 compiled your controllers with only the compilerOptions from tsoa.json. tsoa 7 reads the nearest tsconfig.json, searching up from the directory you run tsoa in, and applies tsoa.json's compilerOptions on top. paths, moduleResolution, customConditions and the strict flags now take effect.
This can change your spec:
- With
strictorstrictNullCheckson, tsoa keepsnullandundefinedthat tsoa 6 dropped inside mapped types. For example, astring | nullproperty ofPartial<T>is nownullable. - An invalid
tsconfig.jsonnow stops generation with TypeScript's error.
To keep tsoa 6's output, set "compilerOptions": { "strictNullChecks": false } in tsoa.json.
TypeScript 6 defaults
On TypeScript 6:
typesdefaults to none instead of every package undernode_modules/@types. If a model uses a global type that nothing imports, list its package incompilerOptions.types.- A model that extends
Errorgains an optionalcauseproperty from TypeScript 6's default lib.
Spec version
Without spec.specVersion, tsoa now generates OpenAPI 3.1 instead of Swagger 2.0, both from the tsoa CLI and from generateSpec(). To keep Swagger 2.0, set it:
json
"spec": {
"specVersion": 2
}Spec generation
Type resolution uses the TypeScript checker
tsoa now takes each type from the TypeScript checker instead of rebuilding it from the source text. Specs for written types such as interfaces, aliases, generics, Pick, Omit, Partial, Record and enums do not change. These do:
- Component names of type arguments tsoa could not name. Template literal types, tuples and other types tsoa 6 named
anynow get their own names.Box<[string, number]>becomesBox__string.number__. This changes those$refs and the names in clients generated from your spec. - Objects from TypeScript's own libraries. A class or interface declared only in TypeScript's libraries must have a supported mapping or a
toJSONmethod.Set,Map,RegExp, typed arrays,WeakRef,PropertyDescriptorand error subtypes such asTypeErrorfail generation. tsoa 6 failed on most of them as well, with a less useful message.Date,Buffer,Error, and models withtoJSONsuch asURLstill work. - Inferred object types. For a type tsoa infers from a value, such as a method without a return type that returns
{ count: 1 }, ortypeof someConstant, tsoa 7 marks each property as optional with the value as itsdefault. tsoa 6 marked them as required. Write the return type to keep them required. - Error messages for a return type end with
in 'Controller.method' return type.
Several types now generate where tsoa 6 failed: tuples (OpenAPI 3.1 only), template literal types such as `user_${string}`, typeof value, indexed access such as Params[Kind.A], Uppercase<'a' | 'b'>, DeepPartial<T>-style recursive mapped types, and models that reference each other while inheriting.
Discriminated unions
In OpenAPI 3 and 3.1, a union of models that share one required string-literal property now generates oneOf with a discriminator instead of anyOf:
ts
interface Cat {
kind: "cat";
meows: boolean;
}
interface Dog {
kind: "dog";
barks: boolean;
}
type Pet = Cat | Dog;Validation uses the tag to choose the member. See Validation.
@pattern
- An invalid regular expression in
@patternnow stops generation. tsoa 6 wrote it to the spec as it was. - A pattern wrapped in single backticks may contain spaces:
@pattern `^[a-z ]+$` Letters and spaces. tsoa 6 cut it at the first space.
Other spec changes
@FormField()takes its parameter's type.@FormField() n: numberis now a number in the spec and in validation. tsoa 6 always used a string.@Body() body: Readablegets the raw request stream. Its spec isapplication/octet-streamwithformat: binary. tsoa 6 treated it as a JSONbytestring.@example 0,@example falseand@example ""now appear in the spec.@deprecatedand@exampleon a type alias now appear in its schema.@titlesets a schema'stitle.
Validation
Requests that tsoa 6 accepted may now fail:
- Discriminated unions validate only the member the tag selects. An error names the failing field, such as
body.barks, instead of a summary of every member. A missing or unknown tag gives one error on the tag property. - Enums never match an array or an object:
["admin"]no longer matches"admin". In a body, the string"1"matches the number1, or"true"matchestrue, only whenbodyCoercionis on. - Self-referencing models are checked at every depth.
minItemsandmaxItemsof 0 are enforced.Bufferaccepts a string, a buffer, an array of bytes or the JSON form{ "type": "Buffer", "data": [...] }. Any other value is a field error. tsoa 6 passed it toBuffer.from, which could allocate a buffer of any size from{ "length": n }or throw a 500.T | nullkeepsnull. tsoa 6 turnednullintofalseforboolean | null.
Error responses change too:
- Field errors for
@Body()and@BodyProp()no longer include the rejectedvalue, only themessage. - Errors for unions and intersections are summarized and cut to 1000 characters, and the value they echo is cut to the same length.
- The keys of errors inside arrays carry the full path, such as
body.children.$0.children.$0.name. tsoa 6 dropped the part above the array.
Express responses
| Controller returns | tsoa 6 | tsoa 7 |
|---|---|---|
a Buffer or Uint8Array | JSON {"type":"Buffer","data":[...]} | the bytes, as application/octet-stream |
a string, with Content-Type: text/plain | the string as JSON, in quotes | the string as it is |
| a stream that fails before sending data | the Node process stops | the error goes to next() and your error handler |
Strings without a Content-Type, numbers, objects and null are sent as before.
An optional @Body() is undefined when the request has no body. tsoa 6 passed the {} that body-parser leaves. A required @Body() still gets {}.
If you move to Express 5 as well, read the Express 5 notes.
Security
An empty entry in a route's security list now allows anonymous access (see Optional authentication). So that no route opens by mistake, generation fails for two @Security decorators that tsoa 6 accepted:
@Security({ api_key: makeScopes() }), or any object whose scopes come from a function call. tsoa 6 generated an empty entry for it,security: [{}].@Security('__proto__').
Other forms generate as before, including names and scopes held in constants.
New features
OpenAPI 3.1
Set "specVersion": 3.1. OpenAPI 3.1 describes tuples with prefixItems.
Optional authentication
@Security({}) next to a scheme makes authentication optional:
ts
@Security("bearerAuth")
@Security({})
@Get()
public async list(@Request() request: express.Request) {}A request without credentials continues with request.user undefined, provided your authentication function rejects with MissingCredentialsError (exported from tsoa) when the credentials are absent. Any other rejection, such as an invalid token, still refuses the request, with 401 by default.
Raw binary request bodies
A @Body() parameter typed as Readable from stream receives the request stream, without parsing or validation. Write the type as Readable; stream.Readable, aliases and unions are not recognized. Express and Koa reject the request if a body parser has already read the stream.
Other additions
@Queries()accepts type aliases and nested object types.spec.disableBasePathPrefixSlashdrops the/between the server URL andbasePath(OpenAPI 3 and later).425 Too Earlyis a known status code.- Spread syntax works in values tsoa reads, such as
@Example({ ...base, id: 1 }). - An empty
@returnstag no longer stops generation. - The generated routes build their per-route data once instead of on every request.