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
{
"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
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.
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.
# add this line to our tsconfig.json file
"experimentalDecorators": true
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.
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.
<!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>
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
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);
import { app } from "./app";
const port = process.env.PORT || 3000;
app.listen(port, () =>
console.log(`Server listening at http://localhost:${port}`)
);
# 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.