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