OpenAPI
StreetJS auto-generates an OpenAPI 3.1 specification from your route decorators. No separate schema files, no code generation step — the spec is derived directly from @Controller, @Get, @Post, and @ApiOperation decorators at runtime.
Setup
Call app.openApiSpec() after registering all controllers, then serve it on a route:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { streetApp } from 'streetjs';
import { UserController } from './controllers/user.controller.js';
import { ProductController } from './controllers/product.controller.js';
const app = streetApp({ port: 3000 });
app.registerController(UserController);
app.registerController(ProductController);
// Generate spec after all controllers are registered
const spec = app.openApiSpec();
// Serve at /openapi.json
app.use(async (ctx, next) => {
if (ctx.path === '/openapi.json' && ctx.method === 'GET') {
ctx.json(spec);
return;
}
await next();
});
await app.listen();
1
2
3
4
5
6
curl http://localhost:3000/openapi.json | jq .info
# {
# "title": "StreetJS API",
# "version": "1.0.0",
# "description": "Auto-generated by StreetJS Framework"
# }
@ApiOperation decorator
Annotate route handlers to add summary, description, and tags to the spec:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import { Controller, Get, Post, Put, Delete, ApiOperation } from 'streetjs';
import type { StreetContext } from 'streetjs';
@Controller('/api/products')
export class ProductController {
@Get('/')
@ApiOperation({
summary: 'List all products',
description: 'Returns a paginated list of products. Use ?page and ?limit query params.',
tags: ['products'],
})
async list(ctx: StreetContext): Promise<void> {
ctx.json({ items: [], total: 0 });
}
@Get('/:id')
@ApiOperation({ summary: 'Get product by ID', tags: ['products'] })
async getOne(ctx: StreetContext): Promise<void> {
ctx.json({ id: ctx.params['id'] });
}
@Post('/')
@ApiOperation({ summary: 'Create a product', tags: ['products'] })
async create(ctx: StreetContext): Promise<void> {
ctx.json({}, 201);
}
@Put('/:id')
@ApiOperation({ summary: 'Update a product', tags: ['products'] })
async update(ctx: StreetContext): Promise<void> {
ctx.json({});
}
@Delete('/:id')
@ApiOperation({ summary: 'Delete a product', tags: ['products'] })
async remove(ctx: StreetContext): Promise<void> {
ctx.send(204);
}
}
Generated spec structure
The generated spec follows OpenAPI 3.1:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
{
"openapi": "3.1.0",
"info": {
"title": "StreetJS API",
"version": "1.0.0",
"description": "Auto-generated by StreetJS Framework"
},
"paths": {
"/api/products": {
"get": {
"summary": "List all products",
"description": "Returns a paginated list of products.",
"tags": ["products"],
"responses": { "200": { "description": "OK" } }
},
"post": {
"summary": "Create a product",
"tags": ["products"],
"responses": { "201": { "description": "Created" } }
}
},
"/api/products/{id}": {
"get": {
"summary": "Get product by ID",
"tags": ["products"],
"parameters": [
{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }
],
"responses": { "200": { "description": "OK" } }
}
}
},
"tags": [
{ "name": "products" }
]
}
Path parameters (:id) are automatically converted to {id} OpenAPI format and added to the parameters array.
Serving a Swagger UI
Pair the spec with Swagger UI for an interactive API explorer:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Serve Swagger UI HTML
app.use(async (ctx, next) => {
if (ctx.path === '/docs' && ctx.method === 'GET') {
ctx.html(`<!DOCTYPE html>
<html>
<head>
<title>API Docs</title>
<meta charset="utf-8">
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist/swagger-ui.css">
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js"></script>
<script>
SwaggerUIBundle({
url: '/openapi.json',
dom_id: '#swagger-ui',
presets: [SwaggerUIBundle.presets.apis, SwaggerUIBundle.SwaggerUIStandalonePreset],
layout: 'StandaloneLayout'
});
</script>
</body>
</html>`);
return;
}
await next();
});
Visit http://localhost:3000/docs for the interactive explorer.
Exporting the spec to a file
Generate the spec at build time for use with external tools:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// scripts/generate-openapi.ts
import 'reflect-metadata';
import { writeFileSync } from 'node:fs';
import { streetApp } from 'streetjs';
import { UserController } from '../src/controllers/user.controller.js';
import { ProductController } from '../src/controllers/product.controller.js';
const app = streetApp({ port: 3000 });
app.registerController(UserController);
app.registerController(ProductController);
const spec = app.openApiSpec();
writeFileSync('openapi.json', JSON.stringify(spec, null, 2));
console.log('OpenAPI spec written to openapi.json');
1
2
3
node --loader ts-node/esm scripts/generate-openapi.ts
# or after building:
node dist/scripts/generate-openapi.js