A multi-framework TypeScript playground for exploring how tsoa-next can be used with:
- ✨ Express
- ✨ Koa
- ✨ Hapi
- ✨ generated OpenAPI specs
- ✨ served OpenAPI specs and docs UIs
- ✨ generated framework routes
- ✨ framework-specific middleware decorators
- ✨ external validation adapters
- ✨ Playwright API verification
This repo is intentionally built as a broad exploration surface rather than a minimal demo. It is meant to be a practical "smorgasbord" of patterns you can inspect, run, and adapt.
- Shared controllers that work across all supported server targets.
- Framework-specific middleware controllers that reuse a common base class while binding framework-native middleware types.
- External validation examples for all supported adapters in this playground:
zodjoiyupsuperstructio-ts
SpecPathexamples for generated spec serving, built-in docs UIs, custom response handlers, and request-aware route gating.- Root and per-method authentication, OR/AND credentials and scope checks.
- Request-scoped dependency injection without a DI framework.
- Body-property and native-request bindings, typed response callbacks, headers and media types.
- PUT, PATCH, DELETE, HEAD and OPTIONS, hidden/deprecated operations and extensions.
- Single and multiple multipart uploads on Express, Koa and Hapi.
- CLI discovery, change-aware generation/checks, template checking and programmatic metadata reuse.
tsoaCLI generation through three root configs:- Generated route files for each middleware target.
- Generated OpenAPI specs for each middleware target.
- Server-mounted spec explorer endpoints for raw spec delivery and Swagger UI.
- Playwright tests that exercise the shared API surface on all three frameworks and each framework-specific middleware showcase on its matching server.
- Node.js
>= 22 - npm
>= 10
fp-tsis installed directly in this repo because theio-tsvalidation showcase needs it at runtime and relying on transitive installation is fragile.- This playground currently stays on
joi@17.13.3so the installed validator matches the publishedtsoa-nextpeer range.
npm install
npm run generate
npm testTo run one server at a time:
npm run serve:express
npm run serve:koa
npm run serve:hapiRun exactly one of these when you want to explore a single framework locally:
npm run serve:expressBase URL:http://127.0.0.1:3101npm run serve:koaBase URL:http://127.0.0.1:3102npm run serve:hapiBase URL:http://127.0.0.1:3103
Each server mounts all shared controllers plus its own framework-specific middleware controller. The existing catalog/order/shipping/validation/middleware/spec examples stay public through @NoSecurity(). /v1/security/root demonstrates API-wide spec.rootSecurity; it requires x-api-key: playground-key. These are demo credentials, not application authentication.
Once a server is running, these endpoints work on all three frameworks:
/docsVisual docs landing page for that server target./docs/swaggerSwagger UI mounted by the shared spec explorer layer./spec/openapi.yamlThe generated OpenAPI YAML for that framework./spec/openapi.jsonThe generated OpenAPI JSON for that framework./v1/specPathSummary endpoint for the controller-local@SpecPath(...)showcase./v1/specPath/specBuilt-in JSONSpecPathtarget./v1/specPath/yamlBuilt-in YAMLSpecPathtarget./v1/specPath/customStringCustom string-producingSpecPathhandler with in-memory caching./v1/specPath/customUncachedStringCustom string-producing handler that runs for every request./v1/specPath/customStreamCustom uncached stream-producingSpecPathhandler./v1/specPath/customCachedStreamCustom stream-producingSpecPathhandler backed by a custom cache./v1/specPath/gatedJSONSpecPathtarget that only resolves when the request includesx-allow-spec: true./v1/specPath/swaggerUiBuilt-in Swagger UISpecPathtarget./v1/specPath/redocUiBuilt-in RedocSpecPathtarget./v1/specPath/rapidocUiBuilt-in RapiDocSpecPathtarget.
The showcase controller also declares /v1/specPath/disabled, but that route is intentionally excluded from registration through gate: false.
These only work on the matching server because the middleware decorators and runtime types differ by framework:
- Express only:
http://127.0.0.1:3101/v1/middleware/express/trace - Koa only:
http://127.0.0.1:3102/v1/middleware/koa/trace - Hapi only:
http://127.0.0.1:3103/v1/middleware/hapi/trace
The raw /spec/openapi.* endpoints map to different generated files depending on which server you start:
- Express serves
src/specs/expressApi.yaml - Koa serves
src/specs/koaApi.yaml - Hapi serves
src/specs/hapiApi.yaml
npm run generateGenerates specs and routes for all three server targets.npm run generate:expressRunstsoa spec-and-routes -c tsoa.express.yaml.npm run generate:koaRunstsoa spec-and-routes -c tsoa.koa.yaml.npm run generate:hapiRunstsoa spec-and-routes -c tsoa.hapi.yaml.npm run typecheckRuns TypeScript validation across the repo.npm testRegenerates artifacts and runs the Playwright API suite.npm run buildRegenerates artifacts and compiles the repo.
These three files define the generation targets and are the entrypoint for understanding how middleware-specific generation differs:
Each one controls:
- the middleware type
- the route output directory
- the selected built-in middleware template
- the generated spec output file
- the controller discovery globs
The servers then mount a shared spec explorer layer that exposes:
/spec/openapi.yaml/spec/openapi.json/docs/docs/swagger
These are generated into all three server variants:
- catalogLookupController.ts
- shippingQuoteController.ts
- orderDraftController.ts
- externalValidationShowcaseController.ts
- specPathShowcaseController.ts
They demonstrate:
@Route,@Get,@Post- path, query, and header binding
- body validation and response typing
- use-case-oriented controller documentation
- external schema validation with
@Validate(...) - generated spec publishing with
@SpecPath(...)
These are intentionally separate because middleware signatures differ by framework:
They all inherit shared behavior from:
This lets the repo show a useful inheritance pattern:
- one shared base for business behavior
- one derived controller per framework for middleware decoration
External validation schemas and payload types live in:
That file is the central place to compare the shape and ergonomics of each supported external validator.
The controller-level SpecPath examples live in:
They demonstrate:
- built-in JSON and YAML spec publishing
- built-in Swagger UI, Redoc, and RapiDoc targets
- custom string and stream handlers
- request-aware spec gating and statically disabled routes
- memory and custom-cache behavior
Custom Handlebars authoring examples are retained here:
The main servers use the library's built-in templates, which include authentication, IoC, response callbacks, uploads and spec serving. The retained custom templates are separate authoring examples, exercised by template-check and generation tests. They show the original public API and spec-serving implementation; they are not substitutes for the full built-in template feature set.
Generated specs:
Generated routes:
Spec explorer implementation:
The Playwright test project lives in:
It verifies:
- shared controller behavior on Express, Koa, and Hapi
- external validator endpoints across all frameworks
SpecPathJSON/YAML/custom/UI targets across all frameworks- served OpenAPI YAML and JSON endpoints across all frameworks
- the docs hub and Swagger UI across all frameworks
- middleware showcase endpoints on the matching framework
src/
controllers/
express/
hapi/
koa/
support/
lib/
models/
server/
servers/
services/
specs/
templates/
tests/
If you want to explore the repo interactively, these are good first stops:
- Open externalValidationShowcaseController.ts and compare it with validationShowcase.ts.
- Open one middleware controller and compare it to middlewareShowcaseBase.ts.
- Run
npm run generateand inspect how controllerGen.ts differs across Express, Koa, and Hapi. - Run one server and hit the routes manually.
- Run
npm testand use the test suite as an executable map of the playground’s features.
This repo is not trying to be the smallest possible tsoa-next example.
It is trying to be:
- approachable for someone evaluating
tsoa-next - broad enough to compare framework integrations
- concrete enough to copy patterns into a real service
- explicit enough to show where generation, controllers, middleware, validation, and tests connect
If you want to understand how tsoa-next can power a real Node API surface across multiple frameworks, this repo is meant to give you:
- 🧱 controller examples
- 🧪 validation examples
- 🔌 middleware examples
- 🗺️ generated route examples
- 📄 generated spec examples
- ✅ executable verification
The shared controllers run on every server. Explicit @Head examples live in the Express and Koa controller directories; Hapi automatically serves HEAD requests for GET routes and does not accept explicit HEAD registration:
- featureShowcaseController.ts:
/v1/features/greetingaccepts{ "name": "Ada" }and reports the request-scopedx-request-id./body-propertybinds onlyname;/requestcompares@Requestand@RequestPropusing the ownplaygroundRequestIddata property added by each server’s middleware./response?conflict=trueuses a typed 409 callback and response header./mediaconsumesapplication/vnd.playground+jsonand producestext/plain./verbsdemonstrates PUT/PATCH/DELETE/HEAD/OPTIONS./legacyis deprecated;/hiddenworks at runtime but is omitted from the spec. - securityShowcaseController.ts:
/v1/security/rootuses root API-key security;/publicclears it./eitheraccepts either the API key or a bearer token withread;/bothneeds both;/scopedneeds bearerwritescope. Demo bearer headers areauthorization: Bearer playground-tokenandx-scopes: read write. Missing credentials return 401 and missing scopes return 403. - uploadShowcaseController.ts:
/v1/uploads/singleaccepts multiparttitleandasset;/manyaccepts repeatedassetsfields. Responses include filenames and content so the examples show actual upload parsing. Express/Koa use memory-storage multer; Hapi uses its native multipart handling. - authentication.ts exports each framework's authentication function; ioc.ts supplies a new controller/service for each request.
For example, with Express running:
curl -H 'x-api-key: playground-key' http://127.0.0.1:3101/v1/security/root
curl -H 'authorization: Bearer playground-token' -H 'x-scopes: read write' http://127.0.0.1:3101/v1/security/scoped
curl -H 'content-type: application/json' -H 'x-request-id: demo-123' -d '{"name":"Ada"}' http://127.0.0.1:3101/v1/features/greeting
curl -F title=Notes -F asset=@README.md http://127.0.0.1:3101/v1/uploads/singleexamples/generation contains a small controller and conventional JSON configs for OpenAPI 2, 3 and 3.1. Each selects a different additional-property policy: ignore retains extra body fields; silently-remove-extras removes them; throw-on-extras rejects them. The default templates generate JSON specs and Express routes. Tests also exercise the same policies and versions with Koa/Hapi, plus YAML/YML and JS/CJS configuration loading.
# Inspect the conventional configs without generating output.
npx tsoa discover examples/generation/configs
# Generate only changed output; generated example files stay under the ignored output directory.
npm run examples:generate
# Check for missing/stale output without writing it.
npm run examples:check
# Parse/render the retained custom Express template and check TypeScript syntax without writing.
npm run examples:template-check
# Generate Express/Koa/Hapi from an object config, reusing returned metadata.
npm run examples:programmatic
# Use a standalone generator that owns its route writes.
npm run examples:standalonetemplate-check checks the selected render's TypeScript syntax; it does not type-check the application or cover unrendered branches. Edit routes.middlewareTemplate in custom-template.json to check another template. Ordinary custom-template generation remains available with tsoa spec-and-routes -c examples/generation/custom-template.json.
programmatic.ts imports generation from tsoa-next/cli, while controllers import runtime decorators from tsoa-next. It preserves returned metadata identity when generating the other frameworks. CLI/config tests exercise failures at the selected dependency, then recover through an explicit retry; unused compiler/YAML/renderer integrations are not required by unrelated operations.
npm test runs the existing API suite plus featureShowcaseSpec.ts, generationExamplesSpec.ts, docsBrowserSpec.ts and specPathSpec.ts. Browser tests render Swagger UI/Redoc/RapiDoc and verify the explorer actually fetches its spec. Chromium is needed for browser tests; install it with npx playwright install chromium when setting up a fresh machine.
standaloneGenerator.cjs shows a custom generator producing a small /custom-generation route for the selected framework. Its config defaults to Express; tests run it on all three. It owns its output writes, so tsoa generate/check intentionally reject this config; use spec-and-routes or routes instead.