Blog

tsoa: API documentation for Node.js

JavaScript is the wild west, but even the wildest developer should keep a few things in mind. Sebastian Springer reveals which ones they are.

Mar 22, 2023

JavaScript is known as the Wild West of web development. But despite the absence of law and order, there are a few things that even the wildest developers should stick to. These include consistent structure, documentation, and testing. We will take a look at how that works for API documentation with tsoa for Node.js.

Of course, all of this is standard in decent applications, yet you find far too little of it in far too many applications. That’s why we’re going to take a look at tsoa, a tool that brings several useful extensions to Node applications: TypeScript, API documentation, and dependency injection, to name just a few.

 

tsoa — which stands for “TypeScript and Open API” — is a framework that builds upon other frameworks. It cannot be used independently and is therefore most similar to Nest. tsoa currently supports Express, hapi, and Koa as basic frameworks.

 

All beginnings are easy

You don’t have to pay attention to anything special during setup. As with almost every Node.js application, you start with an empty directory where you create a new package.json file with npm init -y. Then, make sure all dependencies are installed. You can do this with the two commands npm add tsoa express swagger-ui-express and npm add -D typescript @types/node @types/express @ types/swagger-ui-express. In the last step, configure TypeScript (with npx tsc –init) and tsoa. For the latter, create a file named tsoa.json in your application’s root directory. Listing 1 contains this file’s source code.

 

Become a part of our community and get the latest news about API Conference!

 

Listing 1: tsoa configuration

 

{
  "entryFile": "src/index.ts",
  "noImplicitAdditionalProperties": "throw-on-extras",
  "controllerPathGlobs": ["src/**/*.controller.ts"],
  "spec": {
    "outputDirectory": "build",
    "specVersion": 3

  },
  "routes": {
    "routesDir": "build"
  }
}

 

With this configuration, tsoa assumes that your application’s source code is in a directory named src and that its starting point is the index.ts file. The controller files, which you use to define your endpoints, end in .controller.ts. tsoa generates the output in the build directory. This build process is necessary simply because your application is developed in TypeScript, and Node can’t run this source code directly without tripping over lots of syntax errors. In contrast, getting started with the application has few surprises, as you can see in Listing 2.

 

Listing 2: Getting started with the application

 

import express, { Request, Response } from 'express';
import { RegisterRoutes } from '../build/routes';
import swaggerUi from 'swagger-ui-express';

const app = express();

app.use(express.json());

app.use(
  '/api',
  swaggerUi.serve,
  async (request: Request, response: Response) => {
    return response.send(
      swaggerUi.generateHTML(await 
import('../build/swagger.json'))
    );
  }
);

RegisterRoutes(app);
 
app.listen(8080, () =>
  console.log('server is listening to http://localhost:8080')
);

 

Basically, you implement an ordinary application with tsoa, and in our case, Express. You can register any middleware functions there, such as the JSON bodyparser middleware.

 

A special feature in this example is the definition of the api endpoint which you use to deliver the OpenAPI documentation using the swagger-ui-express package. The basis for this is the swagger.json file that tsoa generates for you.

 

Another tsoa-specific extension of the entry point to the application is the call to RegisterRoutes. This replaces the definition of routes and the integration of the Express router, respectively. When translating TypeScript to JavaScript, you can tell tsoa to co-define the routes and your applications’ endpoints, giving you the swagger.json file and the RegisterRoutes function. To make this work, you can either enter the necessary commands on the command line or extend your package.json as seen in Listing 3. With these two scripts, you can build your application with npm run build and launch it with npm start.

 

Listing 3: Additional script entries in package.json

 

{
...
  "scripts": {
    "build": "tsoa spec-and-routes && tsc",
    "start": "node src/index.js"
  },
...
}

 

tsoa in action

Normally, in Express, you define your routes using a set of methods from the App object or a separate router. These functions are named after HTTP methods and are called get, post, put, and delete. You pass these functions the desired path where the route should apply, and a callback function that Express executes when it receives a request for that method-path combination. If you want to generate API documentation, you can either generate it yourself in the form of a swagger.json file or rely on packages like swagger-jsdoc that allow you to manage API documentation in code. However, in both cases, you have to write the entire documentation yourself and its relationship to the code is loose at best. tsoa takes a different approach here and uses TypeScript decorators to allow you to define the required meta-information in code.

Create data types

For API documentation, you need a schema. You can define that with TypeScript types. This article’s example implements a simple interface for managing people’s data. For this purpose, you define a simple interface, as seen in Listing 4.

 

 

Listing 4: Data types for the interface

 

export interface Person {
  id: number;
  firstName: string;
  lastName: string;
  birthDate: string;
}

export type CreatePerson = Omit<Person, 'id'>;

 

An object of the type Person consists of the properties id, firstName, lastName, and birthDate. When you want to create a new data set, you don’t yet know the associated id because assigning this information is the server’s responsibility. So you use TypeScript’s Omit Utility Type to create a new typalias that matches the Person interface structure, but without the id property. With this structure, you can now move on to defining the logic behind your interface.

Encapsulating the business logic

It’s always a good idea to neatly separate your data structures, business logic, and your endpoints’ definitions. Therefore, you should store the code for data management in a separate file called person.service.ts. You can find the associated source code in Listing 5.

 

Listing 5: Implementation of the PersonService

 

import { CreatePerson, Person } from './person';

class PersonService {
  persons: Person[] = [];

  async create(newPerson: CreatePerson): Promise<Person> {
    let nextId = 1;

    if (this.persons.length > 0) {
      nextId = Math.max(...this.persons.map((person) => person.id)) + 1;
    }
    const newPersonWithId = { ...newPerson, id: nextId };
    this.persons.push(newPersonWithId);
    return newPersonWithId;
  }

  async getAll(): Promise<Person[]> {
    return this.persons;
  }

  async getOne(id: number): Promise<Person | undefined> {
    return this.persons.find((person) => person.id === id);
  }

  async update(updatedPerson: Person): Promise<Person> {
    this.persons.map((person) => {
      return person.id === updatedPerson.id ? updatedPerson : 
person;
    });
    return updatedPerson;
  }

  async remove(id: number): Promise<void> {
    this.persons = this.persons.filter((person) => person.id !== id);
  }
}

export default new PersonService();

 

To keep the example simple, the service doesn’t persist the data, it just keeps it in memory. However, this also means that any operations you perform at runtime will be lost after the application restarts. The service’s interfaces return Promise objects, so they are asynchronous. If you want to integrate a database or other potentially asynchronous storage medium, all you have to do is modify the service. Your endpoint remains unaffected.

 

Defining the endpoint

You define the endpoint in a file named person.controller.ts as a TypeScript class. Through the tsoa.json file, you told tsoa to look for endpoint definitions in files ending in .controller.ts when building the application. Listing 6 contains the source code for the controller.

 

Listing 6: Definition of endpoints

 

import {
  Body,
  Controller,
  Delete,
  Get,
  Path,
  Post,
  Put,
  Route,
  SuccessResponse,
} from 'tsoa';
import { CreatePerson, Person } from './person';
import personService from './person.service';  

@Route('persons')
export class PersonController extends Controller {
  @SuccessResponse('201', 'Created')
  @Post()
  async createPerson(@Body() newPerson: CreatePerson): 
Promise<Person> {
    return personService.create(newPerson);
  }

  @Get('{id}')
  async getOnePerson(@Path() id: number): Promise<Person | undefined> {
    return personService.getOne(id);
  }    @Get()

  async getAllPersons(): Promise<Person[]> {
    return personService.getAll();
  }
 
  @Put('{id}')
  async updatePerson(@Body() updatedPerson: Person): 
Promise<Person> {
    return personService.update(updatedPerson);
  }
 
  @SuccessResponse('204', 'No Content')
  @Delete('{id}')
  async deletePerson(@Path() id: number): Promise<void> {
    return personService.remove(id);
  }
}

 

As you can see, the controller code nearly contains more decorators than actual source code. This is intentional, because tsoa uses these decorators to both generate API documentation and control the application. For this to work, you need to make sure that experimental decorators (experimentalDecorators) are enabled in your TypeScript configuration.

 

But let’s start with the class. This derives from tsoa’s controller class and receives its basic functionality. You also provide a controller class with the @Route decorator, which you will pass the base path of this controller. The PersonController is responsible for everything below the /persons path. You will also provide the individual methods with tsoa decorators. The most important are the decorators for the respective HTTP method, i.e. post, get, put and delete.

 

Here you can add further path specifications, as you can see in the case of getOnePerson. If you use curly braces, you can define a variable that’s filled on a request containing the ID of the record to be read. You can access this with the @Path decorator and use the value in the method.

 

If you want to create a new record, you usually don’t submit the data via the URL path or the query string, but in the request body. You can access this with the @Body decorator. By default, tsoa’s controller methods use response code 200. If you want one of your methods to deviate from this, you can use the @SuccessResponse decorator.

 

 

Assembling

Finally, when you’ve implemented your application to the point where you’re ready for a first test, you can build the code with the npm run build command and start your application with npm start. After that, tsoa and the TypeScript compiler will take action, generating route definitions from your annotations and translating the TypeScript source code into valid JavaScript code that you can run with Node. Once your application is launched, you can access the API documentation via http://localhost:8080/api and manage your application’s people records at http://localhost/persons.

Conclusion

tsoa addresses some issues that aren’t directly addressed by other frameworks like Express or Koa. The framework doesn’t reinvent the wheel, but it builds upon established solutions and extends them. This way, you can extend your Express application with TypeScript and a reasonable API documentation in just a few steps. If you divide your application into controllers and services, as seen in the example, with tsoa you move mainly in the controller code and your business logic remains untouched by the framework.

All News & Updates of API Conference:

Behind the Tracks

API Management

A detailed look at the development of APIs

API Development

Architecture of APIs and API systems

API Design

From policies and identities to monitoring

API Platforms & Business

Web APIs for a larger audience & API platforms related to SaaS