Convert TypeScript classes into Swagger docs and Express API


Keywords
express, koa, openapi2, swagger, typescript
License
MIT
Install
npm install ts2swagger@0.0.33

Documentation

TypeScript to Swagger

Automatically create Swagger documentation and Express endpoints from TypeScript classes. The tool supports

  • Automatically inferring types from TypeScript function declarations
  • Generating callable service endpoint for Express
  • Reading class, interface and method and JSDoc comments into Swagger docs
  • Defining parameter and return value Models using TypeScript
  • Defining types returned at specific error codes
  • Grouping functions using Tags
  • Request and Response as private Service object members (does not pollute interface signature)
  • Rewrites only functions, not entire files
  • Optional values support
  • Private service methods

How it works?

At command line you run

ts2swagger <directory>

Then the tool will

  1. Locate all classes with JSDoc comment @service <serviceid>
  2. Locate all intefaces or classes with JSDoc comment /** model true */
  3. It will create Swagger documentation specified with @swagger <filename>
  4. It will locate all functions having comment @service <serviceid> and create express endpoint in them

It is possible to automate creation of interface with tools like nodemon etc.

Installing

npm -g i ts2swagger

Service class JSDoc params

  • @swagger <filename> output Swagger (OpenAPI 2) file
  • @title <document title> title of the service
  • @service <serviceid> used to identify service
  • @endpoint <path> path where service is located
  • @version <versionnumber> for example 1.0.0 or something
/**
 * Freeform test of the API comes here
 *
 * @swagger /src/swagger/sample.json
 * @title The title of the Doc
 * @service myserviceid
 * @endpoint /sometest/v1/
 * @version 1.0.1
 */
export class MyService {
  constructor(private req: express.Request, private res: express.Response) {}
  ping(message: string): string {
    return `you sent ${message}`;
  }
  async getDevices(): Promise<Device[]> {
    return [];
  }
}

It will compile that file into Swagger JSON file /src/swagger/service.json and write the Express endpoint if it finds a file with a function which is declared using the service ID

inteface/class flags

In the service / interface parameters are mapped to GET, POST, PUT, DELETE or PATCH methods automatically by detecting their value and with some information from the JSDoc comments.

  • @alias <path> the url where this method is located
  • @queryparam <name> is the first parameter to be placed to GET query var ?varname=value
  • @method <httpMethod> used http method GET, POST, PUT, DELETE or PATCH
  • @tag <name> group where this function belongs
  • @tagdescription <text> description for the tag (multiple occurrence override each other)
  • @error <code> <model> HTTP response code and message which is returned
  /**
   *
   * @alias user
   * @method delete
   * @param id set user to some value
   * @tag user
   * @tagdescription System users
   * @error 401 MyErrorClass
   * @error 404 MyErrorClass
   */
  deleteUser(id:string) : TestUser {
    return {name:'foobar'}
  }

Private methods

You can declare methods private in TypeScript and they will not be part of the

  private getUser() {
    return this.req.user
  }

Model intefaces / class flags

If interface is returning models

  • @model true indicate model is ment to be used with services

example

/**
 * @model true
 */
export class Device {
  id: number;
  name: string;
  description?: string;
}

Express bootstrap definition

import * as express from "express";
const app = express();
/**
 * @service myserviceid
 */
function bootstrap(app: any, server: (req, res) => MyService) {
  // The code will be generated in here
}

After running ts2swagger the function bootstrap will be overwritten to have contents

function bootstrap(app: any, server: (req, res) => MyService) {
  // Service endpoint for ping
  app.get("/sometest/v1/ping/:message/", async function(req, res) {
    try {
      res.json(await server(req, res).ping(req.params.message));
    } catch (e) {
      res.status(e.statusCode || 400);
      res.json(e);
    }
  });
}

It is almost ready to be used as a service, but you have to start it by adding line

bootstrap(app, (req, res) => new MyService(req, res));
// and start listening to some port with express
app.listen(1337);
console.log("listening on port 1337");

The if you navigate to http://localhost:1337/sometest/v1/ping/hello you get

"you sent hello"

You will also get Swagger documentation in JSON

Handling Errors

Define Error model, which must have statusCode set to the HTTP status code value.

/**
 * @model true
 */
export class ErrorNotFound {
  statusCode = 404;
  message?: string;
}

That class can then be thrown in the service handler

if (somethingwrong) throw ErrorNotFound();

The Inteface declaration has error defined like this

  /**
   * @error 403 ErrorForbidden
   * @error 404 ErrorNotFound
   */
  async hello(name:string) : Promise<string> {
    if(name==='foo') throw { errorCode:404, message:'User not found'}
    return `Hello ${name}!!!`
  }

Upload parameters

File upload must be handled manually at the moment, no server code is generated for it. Swagger documentation can have three variables

  • @upload which defines the name of the uploaded file variable in form data
  • @uploadmeta is name for text field which can contain encoded JSON metadata
  • @uploadmetadesc Documentation for the uploadmeta contents
  /**
   * @method post
   * @upload file
   * @uploadmeta into
   * @uploadmetadesc send JSON encoded string here...
   */
  async upload(): Promise<any> {
    // handle upload params manually
  }

Full Generated Frontend Sample

This is example whith is bootstrap from src/backend sample code and includes function named bootstrap which is generated from the src/backend/sample.ts and used as both

  1. Functional backend service
  2. Swagger Document served by swagger-ui-express

Also the public folder is served as static so you can begin developing the client app based on the service.

NOTE: only the function bootstrap in the example will be overwritten by ts2swagger, you can manually edit other parts of the code as a programmer.

import * as express from "express";
import { MyService } from "./sample";

const app = express();

const bodyParser = require("body-parser");
app.use(bodyParser.json());
app.use(express.static("public"));
const swaggerUi = require("swagger-ui-express");

// sample server...
app.use(
  "/api-docs",
  swaggerUi.serve,
  swaggerUi.setup(require("../../swagger/sample.json"))
);

type serverFactory = (req, res) => MyService;

/**
 * @service myserviceid
 */
function bootstrap(app: any, server: serverFactory) {
  // Automatically generated endpoint for ping
  app.get("/sometest/v1/ping/:message/", async function(req, res) {
    try {
      res.json(await server(req, res).ping(req.params.message));
    } catch (e) {
      res.status(e.statusCode || 400);
      res.json(e);
    }
  });
  // Automatically generated endpoint for sayHello
  app.get("/sometest/v1/hello/:name/", async function(req, res) {
    try {
      res.json(await server(req, res).sayHello(req.params.name));
    } catch (e) {
      res.status(e.statusCode || 400);
      res.json(e);
    }
  });
  // Automatically generated endpoint for getDevices
  app.get("/sometest/v1/getDevices/", async function(req, res) {
    try {
      res.json(await server(req, res).getDevices());
    } catch (e) {
      res.status(e.statusCode || 400);
      res.json(e);
    }
  });
  // Automatically generated endpoint for upload
  app.post("/sometest/v1/upload/", async function(req, res) {
    try {
      res.json(await server(req, res).upload());
    } catch (e) {
      res.status(e.statusCode || 400);
      res.json(e);
    }
  });
}

// initialize the API endpoint
bootstrap(app, (req, res) => new MyService(req, res));

if (!module.parent) {
  app.listen(1337);
  console.log("listening on port 1337");
}

License

MIT.