Documenting a REST API in Typescript

last updated: January 20, 2022

REST is an architectural style with a set of recommended practices for submitting requests and returning responses. To understand the structure of the requests or responses, as a developer we rely on looking up the REST API's documentation.

Each REST API is slightly different from one another, there is no strict single way of doing things. Dependant on a particular REST API's context or architecture it will be structured to best fulfil its function. This is one of the strengths of REST, in that it provides enough flexibility to adapt to many use cases, but as a developer we cant assume one REST API is the same as the next and this is why having clear and up-to-date documentation becomes very important.

When developing a REST API we want the creation of the documentation to be as easy and frictionless as possible, so that we can concentrate on the business case for a REST API and provide the needed requirements.

This is where tsoa comes into play. tsoa is a library that lets us automatically generate an openapi specification in which can provide a standard, language agnostic interface which can be utilized to provide documentation and client libraries if required.

What we will build

We will build a simple RESTAPI server with Express and Typescript. It will have a couple of routes to interact with a 'note' resource and it will also have a /docs route which will provide the auto generated OpenAPI documentation.

Getting started


# Create a new folder for the project
mkdir rest-api-project
cd rest-api-project

# Create a package.json and initialize git
npm init -y
git init

# Add our dependencies
npm i tsoa express body-parser
npm i --save-dev typescript ts-node-dev @types/node @types/express @types/body-parser

# Create tsconfig.json
npx tsc --init

Configure tsoa

tsoa.json
{
  "entryFile": "src/index.ts",
  "noImplicitAdditionalProperties": "throw-on-extras",
  "controllerPathGlobs": ["src/**/*Controller.ts"],
  "spec": {
    "outputDirectory": "src/docs",
    "specVersion": 3
  },
}

The tsoa config file helps to tell tsoa where the entry point of our application is and where to look for our controllers so that it will automatically discover them.

Then we choose which directory the automatically generated OpenAPI specification will be out put to, in this case we will create a directory src/docs.

Defining a model

Lets define a Note interface in src/notes/note.ts

src/notes/note.ts
export interface Note {
  id: number;
  title: string;
  text: string;
  tags?: string[];
}

The next step is to create a service that will interact with our models, this helps to have a layer between our controllers and the models.

src/notes/noteService.ts
import { Note } from "./note";

export type NoteCreationParams = Pick<Note, "title" | "text" | "tags">;

export class NoteService {
    public get(id: number): Note {
        return {id, title: "best note ever", text: "this note contains a short sentence.", tags: ["demo"]}
    }

    public create(noteCreationParams: NoteCreationParams): Note {
        return {
            id: Math.floor(Math.random() * 10000),
            ...noteCreationParams
        }
    }
}

Defining our controller

The next step is to define our notes controller which will contain all of the routes related to notes that our REST API will offer.

We have create a get and create method in our noteService, so we will need to define routes that interact with those methods of the service.

There will be some tsoa specific decorator syntax in this file, we will have to enable decorators in our tsconfig.json file.

tsconfig.json
# add this line to our tsconfig.json file

"experimentalDecorators": true
src/notes/noteController.ts
import {
    Body,
    Controller,
    Get,
    Path,
    Post,
    Route,
    SuccessResponse,
  } from "tsoa";
import { Note } from "./note";
import { NoteCreationParams, NoteService } from "./noteService";


  @Route("notes")
  export class NoteController extends Controller {
      /**
       * @summary Get a note by id
       *
       * @param noteId
       */
    @Get("{noteId}")
    public async getNote(
      @Path() noteId: number,
    ): Promise<Note> {
      return new NoteService().get(noteId);
    }

    /**
     * @summary Create a note
     *
     * @param requestBody
     */
    @SuccessResponse("201", "Created")
    @Post()
    public async createNote(
      @Body() requestBody: NoteCreationParams
    ): Promise<void> {
      this.setStatus(201);
      new NoteService().create(requestBody);
      return;
    }
  }

Exposing our endpoints

The last piece of the puzzle is to define the Express router which will connect our endpoints to the controllers.

tsoa can be configured to automatically generate the routes too, but for this example we are going to manually define them.

src/notes/noteRouter
import express from "express";
import { NoteController } from './noteController';
import { NoteCreationParams } from "./noteService";

const noteRouter = express.Router();

const noteController = new NoteController();

noteRouter.get("/:id", async (req, res) => {
    let id: number = Number(req.params.id)
    const response = await noteController.getNote(id)
    return res.send(response)
})

noteRouter.post("/", async (req, res) => {
    let newNote: NoteCreationParams = {
        title: req.body.title,
        text: req.body.text,
        tags: req.body.tags
    }
    await noteController.createNote(newNote);
    return res.status(201).send()
})

Create our docs

We will be using to tsoa to automatically create our OpenAPI spec but we will use redoc to display the spec in a nicely styled html page.

src/docs/index.html
<!DOCTYPE html>
<html>
  <head>
    <title>Docs</title>
    <!-- needed for adaptive design -->
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link
      href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700"
      rel="stylesheet"
    />

    <!--
    Redoc doesn't change outer page styles
    -->
    <style>
      body {
        margin: 0;
        padding: 0;
      }
    </style>
  </head>
  <body>
    <redoc spec-url="/swagger.json"></redoc>
    <script src="https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js"></script>
  </body>
</html>
src/docs/docRouter
import express from "express";
import path from "path";

const docRouter = express.Router();

docRouter.get("/swagger.json", (req, res) => {
  return res.sendFile(path.join(__dirname + "/../docs/swagger.json"));
});

docRouter.get("/docs", (req, res) => {
  return res.sendFile(path.join(__dirname + "/../docs/index.html"));
});

export { docRouter };

The docRouter exposes 2 endpoints. The first is the generated OpenAPI specification in json format, and the second is the redoc html page which will display a nicely styled documentation page for our REST API.

Creating our express server

src/app.ts
import express from "express";
import bodyParser from "body-parser";
import { docRouter } from "./docs/docRouter";
import { noteRouter } from "./notes/noteRouter";

export const app = express();

// Use body parser to read sent json payloads
app.use(
  bodyParser.urlencoded({
    extended: true,
  })
);
app.use(bodyParser.json());

// Routes
app.use("/", docRouter);
app.use("/v1", noteRouter);
src/index.ts
import { app } from "./app";

const port = process.env.PORT || 3000;

app.listen(port, () =>
  console.log(`Server listening at http://localhost:${port}`)
);
package.json
# add these script to package.json

"scripts": {
    "prebuild": "tsoa spec",
    "build": "tsc",
    "start": "node build/index.js"
},

The prebuild script will run the tsoa spec command at build time which generates our OpenAPI specification, and will keep our documentation up to date.

And that's it! You should now have a REST API which generates beautiful documentation for you.

Hi! I'm Niall McKenna

Technologist, Maker & Software Developer